This chapter explains how to create a GAME plugin for OpenSR using the IOpenSRPlugin interface.
The objective of a GAME plugin is to connect OpenSR to a game and continuously provide normalized simulation telemetry through the OpenSR SDK.
A GAME plugin may retrieve telemetry from:
- UDP streams
- shared memory
- official SDKs
- game APIs
- telemetry DLLs
- TCP streams
- memory reading
- external middleware
- proprietary protocols
The telemetry transport itself is not important to OpenSR.
What matters is that the plugin:
- correctly implements the plugin lifecycle
- continuously updates
OutSimData - submits synchronized frames
- manages its own runtime safely
This chapter uses the BeamNG.Drive plugin as a real-world production example.
The BeamNG plugin is particularly interesting because it combines:
- multiple telemetry sources
- asynchronous UDP streams
- high-frequency motion telemetry
- dashboard telemetry
- runtime settings reload
- pause/resume synchronization
- packet validation
- multi-threaded telemetry acquisition
However, this chapter is not about creating a BeamNG plugin.
It explains how to build a robust OpenSR GAME plugin for any game.
Examples shown from the BeamNG plugin are used only to demonstrate recommended architecture and implementation patterns.
What Is A GAME Plugin
A GAME plugin is an IN plugin.
Its responsibility is to acquire telemetry from a game and convert it into the OpenSR standardized telemetry model (OutSimData).
The GAME plugin acts as the telemetry producer for the entire OpenSR ecosystem.
All downstream systems consume the telemetry generated by the GAME plugin:
- motion systems
- dashboards
- external protocols
- telemetry forwarders
- hardware integrations
- overlays
- logging systems
- OUT plugins
GAME Plugin Responsibilities
A GAME plugin is responsible for:
- detecting the target game
- connecting to telemetry sources
- decoding telemetry packets
- converting telemetry into OpenSR structures
- updating shared buffers
- submitting synchronized frames
- handling pause/resume states
- managing worker threads
- handling runtime reloads
- shutting down cleanly
The OpenSR core never acquires telemetry itself.
The plugin owns the telemetry pipeline.
GAME Plugin Directory Structure
A typical GAME plugin directory looks like this:
GamePlugins/
+-- BeamNGDrive/
+-- OpenSRBeamNGDrivePlugin.osrp
+-- settings.xml
+-- settings.dll (optional)
+-- profile_template.xml (optional)
+-- beamng.png (optional)
+-- additional assets...
The .osrp file is the plugin DLL itself.
The remaining files are optional and are discovered through convention.
Required SDK Headers
Every GAME plugin must include the OpenSR SDK headers.
Minimal includes:
#include "IOpenSRPlugin.h"
#include "OpenSRBuffers.h"
BeamNG example:
Additional includes depend on the telemetry backend used by the game:
#include "SocketUDP.h"
#include <thread>
#include <mutex>
#include <atomic>
#include <memory>
The BeamNG plugin uses UDP sockets and worker threads, but another game could instead use:
- shared memory APIs
- telemetry SDK wrappers
- vendor libraries
- memory scanners
- custom TCP clients
Creating The Plugin Class
A GAME plugin must inherit from osr::IOpenSRPlugin.
Example:
class OpenSRBeamNGDrivePlugin
: public osr::IOpenSRPlugin
{
};
This interface defines the complete lifecycle contract between OpenSR and the plugin.
Mandatory Plugin Metadata
One of the most important parts of a GAME plugin is the metadata contract.
These methods are mandatory.
They are used by:
- the dashboard
- the plugin manager
- plugin grouping
- profile systems
- diagnostics
- telemetry routing
- package identification
A plugin that does not properly expose metadata is considered incomplete.
Required Metadata Methods
A GAME plugin must implement:
void GetPluginName(char* buffer, size_t bufferSize) const override;
void GetAuthor(char* buffer, size_t bufferSize) const override;
void GetVersion(char* buffer, size_t bufferSize) const override;
void GetLicenseType(char* buffer, size_t bufferSize) const override;
void GetDescription(char* buffer, size_t bufferSize) const override;
void GetTargetName(char* buffer, size_t bufferSize) const override;
void GetTargetProcessName(char* buffer, size_t bufferSize) const override;
void GetPackageName(char* buffer, size_t bufferSize) const override;
void GetPluginGroupName(wchar_t* buffer, size_t bufferSize) const override;
int GetType() override;
Metadata Example — BeamNG Plugin
void GetPluginName(char* buffer,
size_t bufferSize) const override
{
strcpy_s(buffer,
bufferSize,
"BeamNG.Drive Plugin");
}
void GetAuthor(char* buffer,
size_t bufferSize) const override
{
strcpy_s(buffer,
bufferSize,
"OpenSR Team");
}
void GetVersion(char* buffer,
size_t bufferSize) const override
{
strcpy_s(buffer,
bufferSize,
"0.1");
}
void GetLicenseType(char* buffer,
size_t bufferSize) const override
{
strcpy_s(buffer,
bufferSize,
"MIT");
}
Metadata Explanation
GetPluginName()
Human-readable plugin name shown in the UI.
Example:
"BeamNG.Drive Plugin"
GetAuthor()
Plugin author or team.
Used by diagnostics and plugin manager interfaces.
GetVersion()
Semantic or internal plugin version.
Should be updated for every release.
GetLicenseType()
License identifier.
Examples:
- MIT
- GPL
- Proprietary
- Apache-2.0
Target Metadata
A GAME plugin must identify its target game.
Example:
void GetTargetName(char* buffer,
size_t bufferSize) const override
{
strcpy_s(buffer,
bufferSize,
"BeamNG.Drive");
}
void GetTargetProcessName(char* buffer,
size_t bufferSize) const override
{
strcpy_s(buffer,
bufferSize,
"BeamNG.drive.exe");
}
Why TargetProcessName Matters
The OpenSR core uses the target process name to:
- validate game state
- detect running sessions
- associate profiles
- manage lifecycle synchronization
This value must match the real executable name.
Package Name
Every plugin must expose a unique package name.
Example:
void GetPackageName(char* buffer,
size_t bufferSize) const override
{
strncpy_s(
buffer,
bufferSize,
"com.opensrteam.gameplugin.beamngdrive",
_TRUNCATE);
}
Important Package Rules
Package names should:
- be globally unique
- never change once released
- use reverse-domain notation
Recommended format:
com.companyname.pluginname
Bad example:
beamngplugin
Good example:
com.opensrteam.gameplugin.beamngdrive
Plugin Type
GAME plugins must return:
int GetType() override
{
return GAME_PLUGIN_TYPE;
}
Never return OUT_PLUGIN_TYPE for a GAME plugin.
Plugin Groups
Plugin grouping allows multiple plugins to share profile families.
Example:
void GetPluginGroupName(
wchar_t* buffer,
size_t bufferSize) const override
{
wcscpy_s(buffer,
bufferSize,
L"Automobilista2Plugins");
}
This is useful when several executables share compatible telemetry structures.
Examples:
- demo versions
- AVX/non-AVX versions
- dedicated server variants
- regional executables
BeamNG does not use grouping and returns an empty string.
Exported Factory Functions
Every plugin DLL must export two functions.
These functions are mandatory.
extern "C" __declspec(dllexport)
osr::IOpenSRPlugin* CreatePlugin()
{
return new OpenSRBeamNGDrivePlugin();
}
extern "C" __declspec(dllexport)
void DestroyPlugin(osr::IOpenSRPlugin* plugin)
{
if (plugin)
delete plugin;
}
Without these exports, the OpenSR loader cannot instantiate the plugin.
Internal Plugin State
A production plugin usually owns:
- worker threads
- sockets
- SDK handles
- telemetry buffers
- synchronization objects
- lifecycle flags
- settings
BeamNG example:
std::thread workerThread;
std::thread motionThread;
std::unique_ptr<SocketUDP> socketMotionSim;
std::unique_ptr<SocketUDP> socketOutGauge;
std::atomic<bool> m_stopRequested = false;
std::atomic<bool> m_isPluginRunning = false;
std::atomic<bool> m_isPluginPaused = false;
Important Architecture Rule
The plugin owns all runtime systems.
The OpenSR core does NOT:
- create plugin threads
- stop plugin threads
- manage sockets
- synchronize telemetry reads
- manage telemetry backends
The plugin is fully responsible for its runtime behavior.
Plugin Lifecycle
The GAME plugin lifecycle is:
CreatePlugin()
?
Init()
?
Start()
?
Pause()/Resume()
?
Stop()
?
Shutdown()
?
DestroyPlugin()
Each stage has a specific responsibility.
Init()
Init() is called once after plugin creation.
Purpose:
- store OpenSR pointers
- store paths
- initialize internal state
- prepare static resources
Init should NOT:
- launch threads
- open sockets
- connect to telemetry
- block execution
BeamNG Init Example
bool OpenSRBeamNGDrivePlugin::Init(
OpenSRContext* context,
void* inBuf,
const wchar_t* pluginPath)
{
// Store OpenSR runtime context.
// Gives access to callbacks, app state,
// paths and synchronization information.
m_pContext = context;
// Cast the generic buffer pointer
// into the expected GAME plugin buffer type.
m_pBufferIn =
static_cast<OpenSRBuffersIN*>(inBuf);
// Store plugin-relative path.
m_pluginPath = pluginPath;
// Build absolute path to settings.xml.
xmlPath = StringUtils::createWString(
m_pContext->osrDocFolder,
L"\\",
pluginPath,
L"\\settings.xml");
return true;
}
OpenSRContext
OpenSRContext is one of the most important SDK structures.
It provides access to:
- application state
- callbacks
- shared paths
- game PID
- synchronization state
- runtime configuration
- user data
The most important callback for GAME plugins is:
submitFrameCallback()
This tells OpenSR:
“A new synchronized simulation frame is ready.”
OpenSRBuffersIN
GAME plugins receive writable telemetry buffers.
The primary writable telemetry frame is:
m_pBufferIn->outSimDataIN
This is where the plugin writes normalized telemetry.
Settings System
Plugins may optionally expose a settings.xml.
BeamNG example:
<settings>
<option name="outgauge ip">127.0.0.1</option>
<option name="outgauge port">4441</option>
<option name="motionsim ip">127.0.0.1</option>
<option name="motionsim port">4444</option>
<option name="outgauge allowed">true</option>
<option name="motionsim allowed">true</option>
</settings>
Why XML Settings Matter
Telemetry integrations often require:
- IP configuration
- ports
- polling frequencies
- transport selection
- protocol options
- SDK modes
- filtering options
Settings allow runtime configuration without recompilation.
Start()
Start() initializes runtime telemetry systems.
Typical responsibilities:
- load settings
- initialize SDKs
- open sockets
- connect telemetry sources
- launch worker threads
- reset runtime flags
BeamNG Start() Example
bool OpenSRBeamNGDrivePlugin::Start()
{
// Protect lifecycle transitions.
// Prevent concurrent Start/Stop calls.
std::lock_guard<std::mutex>
lock(lifecycleMutex);
if (!m_pContext)
return false;
// Reset stop flag before creating threads.
m_stopRequested = false;
// Plugin starts unpaused.
m_isPluginPaused = false;
// Mark plugin runtime active.
m_isPluginRunning = true;
// Reload runtime settings.
LoadPluginSettings(xmlPath, m_settings);
// Create UDP receiver for dashboard telemetry.
socketOutGauge =
std::make_unique<SocketUDP>(
m_settings.outgauge_ip.c_str(),
m_settings.outgauge_port);
if (!socketOutGauge->open())
return false;
// Create UDP receiver for motion telemetry.
socketMotionSim =
std::make_unique<SocketUDP>(
m_settings.motionsim_ip.c_str(),
m_settings.motionsim_port);
if (!socketMotionSim->open())
{
socketOutGauge->close();
return false;
}
// Start dedicated motion thread.
if (m_settings.motionAllowed)
{
motionThread =
std::thread(
&OpenSRBeamNGDrivePlugin
::MotionWorkerThread,
this);
}
// Start dashboard telemetry thread.
if (m_settings.outgaugeAllowed)
{
workerThread =
std::thread(
&OpenSRBeamNGDrivePlugin
::WorkerThread,
this);
}
return true;
}
Why Multiple Threads
Different telemetry systems often operate at different frequencies.
Example:
| Telemetry Type | Typical Frequency |
|---|---|
| dashboard data | 30-100 Hz |
| motion telemetry | 200-1000 Hz |
| SDK callbacks | variable |
| shared memory polling | 60-500 Hz |
Separating telemetry systems into dedicated threads improves:
- timing stability
- responsiveness
- synchronization quality
- failure isolation
- debugging
Worker Thread Architecture
The worker thread continuously:
- validates runtime state
- checks game state
- receives telemetry
- validates packets
- converts telemetry
- updates
OutSimData - submits frames
- throttles CPU usage
BeamNG Worker Loop
while (!m_stopRequested &&
m_pContext &&
m_pContext->isAppRunning)
{
// Validate that the game process still exists.
if (!OpenSRHelper::IsProcessRunningByPIDAndName(
m_pContext->targetGamePId,
TEXT(_TARGET_APP)))
{
break;
}
// Suspend worker while plugin is paused.
CheckForPause();
char bufferOG[1024] = {};
// Receive asynchronous UDP telemetry.
int bytesReceivedOG =
socketOutGauge->recvAsync(
bufferOG,
sizeof(bufferOG));
// Validate packet size before decoding.
if (bytesReceivedOG >= sizeof(OutGaugePack))
{
// Convert raw game packet into OpenSR telemetry.
GetTelemetry(
pack,
*m_pBufferIn->outSimDataIN);
// Notify OpenSR that a synchronized
// simulation frame is ready.
m_pContext->submitFrameCallback(
m_pContext->userData);
}
// Prevent busy-loop CPU saturation.
std::this_thread::sleep_for(
std::chrono::milliseconds(1));
}
Packet Validation
Never trust external telemetry blindly.
Always validate:
- packet size
- signatures
- version fields
- checksums
- packet identifiers
BeamNG motion example:
if (bytesReceived ==
sizeof(MotionSimPack) &&
memcmp(bufferMS, "BNG1", 4) == 0)
{
std::memcpy(
&motionPack,
bufferMS,
sizeof(MotionSimPack));
hasNewData = true;
}
This prevents corrupted telemetry from contaminating the simulation pipeline.
Telemetry Conversion
The most important job of a GAME plugin is telemetry normalization.
The plugin converts game-specific telemetry into standardized OpenSR telemetry.
This is where:
- packet formats
- coordinate systems
- unit systems
- SDK structures
become normalized OutSimData.
Vehicle Telemetry Mapping
BeamNG example:
void OpenSRBeamNGDrivePlugin::GetTelemetry(
OutGaugePack& pack,
OutSimData& data)
{
data.mVehicleData.speed = pack.Speed;
data.mVehicleData.rpm = pack.RPM;
data.mVehicleData.gear =
static_cast<int>(pack.Gear - 1);
data.mVehicleData.throttle =
pack.Throttle;
data.mVehicleData.brake =
pack.Brake;
data.mVehicleData.clutch =
pack.Clutch;
}
This conversion layer should remain isolated and readable.
Avoid mixing telemetry decoding and mapping logic together.
Motion Telemetry Mapping
Motion systems require normalized axes.
Example:
data.mMotionData.localAccelX =
-pack.accX;
data.mMotionData.localAccelY =
pack.accZ;
data.mMotionData.localAccelZ =
pack.accY;
This remapping exists because games rarely use the same coordinate conventions.
The GAME plugin is responsible for converting telemetry into OpenSR conventions.
Frame Submission
After telemetry is updated, the plugin must submit a frame.
m_pContext->submitFrameCallback(
m_pContext->userData);
This is one of the most important SDK calls.
Without frame submission:
- motion systems never update
- dashboards remain frozen
- synchronization barriers never advance
- OUT plugins never receive new telemetry
Important Recommendation
Do not flood the pipeline with unnecessary frames.
BeamNG avoids duplicate submissions:
if (memcmp(&pack,
bufferOG,
sizeof(OutGaugePack)) == 0)
{
update = false;
}
Reducing redundant frame submissions improves:
- CPU usage
- synchronization stability
- downstream plugin performance
Pause Synchronization
OpenSR supports plugin pause/resume synchronization.
Worker threads should respect pause state.
BeamNG shared pause helper:
// This is the shared pause logic that both worker threads will use.
void OpenSRBeamNGDrivePlugin::CheckForPause() {
std::unique_lock<std::mutex> lock(m_pauseMutex);
// Wait while paused and the stop signal has not been received
m_pauseCv.wait(lock, [this] { return !m_isPluginPaused; });
}
Why Pause Handling Matters
Without proper pause synchronization:
- threads continue consuming telemetry
- sockets continue filling buffers
- CPU usage remains active
- shutdown becomes unreliable
- synchronization becomes inconsistent
Production plugins should always implement pause handling.
Pause Detection From Telemetry Silence
Some games stop transmitting telemetry while paused.
The BeamNG plugin detects this through timeouts.
auto elapsed =
std::chrono::duration_cast<
std::chrono::milliseconds>(
std::chrono::steady_clock::now()
- lastPacketTime).count();
if (elapsed > PAUSE_TIMEOUT_MS)
{
m_pBufferIn->outSimDataIN
->mPacketHeader.paused = true;
}
This is an important production pattern.
Telemetry silence often means:
- pause menu
- loading screen
- replay
- disconnected session
- inactive simulation
Runtime Reload Support
Plugins may react to runtime context changes.
BeamNG example:
void OpenSRBeamNGDrivePlugin::OnContextChanged(
osr::OpenSRContextChange reason)
{
if (reason ==
osr::OpenSRContextChange
::SettingsChanged)
{
Stop();
Start();
}
}
This enables:
- live settings reload
- runtime transport changes
- protocol reconfiguration
- telemetry restart without restarting OpenSR
Stop()
Stop() must fully terminate runtime activity.
Responsibilities:
- stop telemetry loops
- wake paused threads
- join worker threads
- close sockets
- release SDK handles
- stop runtime timers
BeamNG Stop Example
void OpenSRBeamNGDrivePlugin::Stop()
{
// Prevent concurrent lifecycle transitions.
std::lock_guard<std::mutex>
lock(lifecycleMutex);
// Signal all worker threads to terminate.
m_stopRequested = true;
// If paused, wake threads so they can exit.
if (m_isPluginPaused)
{
Resume();
}
// Wait for worker termination.
if (workerThread.joinable())
workerThread.join();
if (motionThread.joinable())
motionThread.join();
// Close telemetry sockets.
if (socketOutGauge)
socketOutGauge->close();
if (socketMotionSim)
socketMotionSim->close();
}
Critical Production Rule
Never leave:
- detached threads
- active sockets
- stale callbacks
- running SDKs
- dangling memory access
- active timers
after Stop() completes.
A stopped plugin must be completely inactive.
Recommended Production Architecture
A production-grade GAME plugin should separate responsibilities clearly.
Recommended structure:
MyGamePlugin/
+-- MyGamePlugin.cpp
+-- MyGamePlugin.h
+-- Telemetry/
¦ +-- UDPReceiver.cpp
¦ +-- SharedMemoryReader.cpp
¦ +-- SDKWrapper.cpp
¦ +-- PacketParser.cpp
+-- Mapping/
¦ +-- VehicleMapper.cpp
¦ +-- MotionMapper.cpp
¦ +-- SessionMapper.cpp
+-- Settings/
¦ +-- SettingsLoader.cpp
+-- Utils/
+-- CoordinateSystem.cpp
This becomes extremely important as integrations grow.
Common Mistakes
Blocking Inside Start()
Never perform infinite loops directly inside Start().
Always use worker threads.
Detached Threads
Never detach telemetry threads.
Always join them during Stop().
Missing Packet Validation
Never trust telemetry blindly.
Always validate packet structure.
Forgetting submitFrameCallback()
Updating telemetry without submitting frames results in frozen consumers.
Flooding Frame Submission
Submitting frames unnecessarily increases synchronization overhead.
Ignoring Pause States
Plugins must correctly propagate pause state.
Mixing Decoding And Mapping
Keep transport decoding separate from OpenSR mapping.
Not Handling Game Shutdown
Worker loops should continuously validate game existence.
Final Notes
The most important thing to understand is:
OpenSR does not care how telemetry is acquired.
A GAME plugin may use:
- UDP
- shared memory
- SDK APIs
- memory hooks
- telemetry DLLs
- external middleware
- custom transports
The transport layer is entirely plugin-defined.
What OpenSR expects is:
- correct lifecycle implementation
- valid metadata
- synchronized
OutSimData - proper frame submission
- reliable runtime behavior
- clean shutdown
The BeamNG.Drive plugin demonstrates a robust production-grade implementation of these principles.
