Creating A Game Plugin With Visual Studio

The Project

This section explains how to create a valid OpenSR GAME plugin project in Visual Studio 2019.

The objective is to produce a GAME plugin (.osri) that OpenSR can load automatically.

This example uses:

  • Visual Studio 2019
  • native C++
  • x64 DLL project
  • OpenSR SDK
  • C++17 (recommended)

The example plugin targets:

notepad.exe

and generates fake telemetry at 60 Hz.


1. Install The OpenSR SDK

The OpenSR installer includes an optional SDK package.

During installation:

Install OpenSR SDK

must be checked.

If installed, the SDK is copied into:

Documents/OpenSR/OpenSR_SDK1.0/

Typical SDK structure:

Documents/
└── OpenSR/
    └── OpenSR_SDK1.0/
        ├── includes/
        ├── Example/
        └── docs/

The most important folder is:

OpenSR_SDK1.0/includes/

because it contains:

IOpenSRPlugin.h
OpenSRContext.h
OpenSRBuffers.h
StringUtils.h
OpenSRHelper.h

OpenSRInSimData.h
OpenSROutSimData.h

IPluginSettings.h
SettingsData.h

IPluginUI.h

OpenSRBlobData.h

OpenSRLegacyHelper.h

OpenSRBuffers.cs
OpenSRContext.cs
OutSimData.cs
InSimData.cs


2. Create The Project

Open Visual Studio 2019.

Select:

Create a new project

Choose:

Dynamic-Link Library (DLL)

Language:

C++

Platform:

Windows

Project name example:

NotepadExamplePlugin

Press:

Create

3. Disable Precompiled Headers (Optional)

Inside the wizard:

Uncheck:

Precompiled Header

This is optional.

Keeping PCH enabled is also valid.

Disabling it simplifies first plugin creation.


4. Configure Platform

Open:

Build
→ Configuration Manager

Set:

Active Solution Platform = x64

OpenSR plugins should normally be built in x64.


5. Configure C++ Standard

Open:

Project Properties
→ C/C++
→ Language
→ C++ Language Standard

Recommended:

ISO C++17 Standard (/std:c++17)

C++14 also works for most plugins, but C++17 is recommended.


6. Configure Runtime Library

Open:

Project Properties
→ C/C++
→ Code Generation
→ Runtime Library

Recommended (default):

For Release:

Multi-threaded DLL (/MD)

For Debug:

Multi-threaded Debug DLL (/MDd)

7. Configure SDK Include Paths

Open:

Project Properties
→ C/C++
→ General
→ Additional Include Directories

Add:

$(USERPROFILE)\Documents\OpenSR\OpenSR_SDK1.0\includes

Your plugin must be able to include:

#include "IOpenSRPlugin.h"
#include "OpenSRBuffers.h"
#include "OpenSRContext.h"

8. Add Source Files

NotepadExamplePlugin.h

#pragma once

#include "IOpenSRPlugin.h"
#include "OpenSRBuffers.h"

#include <thread>
#include <atomic>
#include <mutex>
#include <condition_variable>
#include <chrono>
#include <cmath>

#define _TARGET_APP "notepad.exe"

class NotepadExamplePlugin : public osr::IOpenSRPlugin
{
public:

    NotepadExamplePlugin();
    ~NotepadExamplePlugin();

    // =========================================================
    // OpenSR Lifecycle
    // =========================================================

    bool Init(
        OpenSRContext* context,
        void* inBuf,
        const wchar_t* pluginPath) override;

    bool Start() override;

    void Stop() override;

    void Shutdown() override;

    void Pause() override
    {
        m_isPluginPaused = true;
    }

    void Resume() override
    {
        m_isPluginPaused = false;
        m_pauseCv.notify_one();
    }

    bool IsRunning() const override
    {
        return m_isPluginRunning;
    }

    void OnContextChanged(
        osr::OpenSRContextChange reason) override;

    // =========================================================
    // Mandatory Metadata
    // =========================================================

    void GetPluginName(
        char* buffer,
        size_t bufferSize) const override
    {
        strcpy_s(
            buffer,
            bufferSize,
            "Notepad Example Plugin");
    }

    void GetAuthor(
        char* buffer,
        size_t bufferSize) const override
    {
        strcpy_s(
            buffer,
            bufferSize,
            "AuthorName");
    }

    void GetVersion(
        char* buffer,
        size_t bufferSize) const override
    {
        strcpy_s(
            buffer,
            bufferSize,
            "1.0");
    }

    void GetLicenseType(
        char* buffer,
        size_t bufferSize) const override
    {
        strcpy_s(
            buffer,
            bufferSize,
            "MIT");
    }

    void GetDescription(
        char* buffer,
        size_t bufferSize) const override
    {
        strcpy_s(
            buffer,
            bufferSize,
            "Example OpenSR GAME plugin using Notepad.exe as target process and generating fake telemetry.");
    }

    void GetTargetName(
        char* buffer,
        size_t bufferSize) const override
    {
        strcpy_s(
            buffer,
            bufferSize,
            "Notepad");
    }

    void GetTargetProcessName(
        char* buffer,
        size_t bufferSize) const override
    {
        strcpy_s(
            buffer,
            bufferSize,
            _TARGET_APP);
    }

    void GetPackageName(
        char* buffer,
        size_t bufferSize) const override
    {
        strncpy_s(
            buffer,
            bufferSize,
            "com.authorname.gameplugin.example.notepadplugin",
            _TRUNCATE);
    }

    void GetPluginGroupName(
        wchar_t* buffer,
        size_t bufferSize) const override
    {
        wcscpy_s(
            buffer,
            bufferSize,
            L"");
    }

    int GetType() override
    {
        return GAME_PLUGIN_TYPE;
    }

private:

    // =========================================================
    // Internal Runtime
    // =========================================================

    void WorkerThread();

    void CheckForPause();

    void GenerateFakeTelemetry(
        OutSimData& data,
        float timeSec);

private:

    // =========================================================
    // OpenSR Runtime References
    // =========================================================

    OpenSRContext* m_pContext = nullptr;

    OpenSRBuffersIN* m_pBufferIn = nullptr;

    std::wstring m_pluginPath;

    // =========================================================
    // Threading
    // =========================================================

    std::thread workerThread;

    std::mutex lifecycleMutex;

    std::mutex m_pauseMutex;

    std::condition_variable m_pauseCv;

    // =========================================================
    // Runtime State
    // =========================================================

    std::atomic<bool> m_stopRequested = false;

    std::atomic<bool> m_isPluginRunning = false;

    std::atomic<bool> m_isPluginPaused = false;
};

NotepadExamplePlugin.cpp

#include "pch.h"

#include "NotepadExamplePlugin.h"

#include "OpenSRHelper.h"

#include <Windows.h>
#include <cmath>

// =========================================================
// Exported Factory Functions
// =========================================================

extern "C" __declspec(dllexport)
osr::IOpenSRPlugin * CreatePlugin()
{
    return new NotepadExamplePlugin();
}

extern "C" __declspec(dllexport)
void DestroyPlugin(osr::IOpenSRPlugin * plugin)
{
    if (plugin)
        delete plugin;
}

// =========================================================
// Constructor / Destructor
// =========================================================

NotepadExamplePlugin::NotepadExamplePlugin()
{
}

NotepadExamplePlugin::~NotepadExamplePlugin()
{
    Shutdown();
}

// =========================================================
// Init
// =========================================================

bool NotepadExamplePlugin::Init(
    OpenSRContext* context,
    void* inBuf,
    const wchar_t* pluginPath)
{
    // Store OpenSR runtime context.
    m_pContext = context;

    // Store writable telemetry buffers.
    m_pBufferIn =
        static_cast<OpenSRBuffersIN*>(inBuf);

    // Store plugin path.
    m_pluginPath = pluginPath;

    return true;
}

// =========================================================
// Start
// =========================================================

bool NotepadExamplePlugin::Start()
{
    std::lock_guard<std::mutex>
        lock(lifecycleMutex);

    if (!m_pContext)
        return false;

    // Reset runtime state.
    m_stopRequested = false;

    m_isPluginPaused = false;

    m_isPluginRunning = true;

    // Start telemetry thread.
    workerThread =
        std::thread(
            &NotepadExamplePlugin::WorkerThread,
            this);

    return true;
}

// =========================================================
// Stop
// =========================================================

void NotepadExamplePlugin::Stop()
{
    std::lock_guard<std::mutex>
        lock(lifecycleMutex);

    // Signal worker shutdown.
    m_stopRequested = true;

    // Wake paused thread if needed.
    if (m_isPluginPaused)
    {
        Resume();
    }

    // Wait for thread termination.
    if (workerThread.joinable())
    {
        workerThread.join();
    }

    m_isPluginRunning = false;
}

// =========================================================
// Shutdown
// =========================================================

void NotepadExamplePlugin::Shutdown()
{
    Stop();
}

// =========================================================
// Context Change
// =========================================================

void NotepadExamplePlugin::OnContextChanged(
    osr::OpenSRContextChange reason)
{
    // No runtime reload logic required
    // for this example plugin.
}

// =========================================================
// Pause Synchronization
// =========================================================

void NotepadExamplePlugin::CheckForPause()
{
    std::unique_lock<std::mutex>
        lock(m_pauseMutex);

    m_pauseCv.wait(
        lock,
        [this]
        {
            return !m_isPluginPaused;
        });
}

// =========================================================
// Main Worker Thread
// =========================================================

void NotepadExamplePlugin::WorkerThread()
{
    if (!m_pContext ||
        !m_pBufferIn ||
        !m_pBufferIn->outSimDataIN)
    {
        return;
    }

    // Mark telemetry available.
    m_pBufferIn
        ->outSimDataIN
        ->mPacketHeader
        .reportAvailable = 1;

    auto startTime =
        std::chrono::steady_clock::now();

    while (!m_stopRequested &&
        m_pContext &&
        m_pContext->isAppRunning)
    {
        // Validate target process.
        if (!OpenSRHelper::IsProcessRunningByPIDAndName(
            m_pContext->targetGamePId,
            TEXT(_TARGET_APP)))
        {
            break;
        }

        // Respect OpenSR pause state.
        CheckForPause();

        // Compute running time.
        auto now =
            std::chrono::steady_clock::now();

        float timeSec =
            std::chrono::duration<float>(
                now - startTime).count();

        // Generate fake telemetry.
        GenerateFakeTelemetry(
            *m_pBufferIn->outSimDataIN,
            timeSec);

        // Submit synchronized frame.
        m_pContext->submitFrameCallback(
            m_pContext->userData);

        // 60 Hz update rate.
        std::this_thread::sleep_for(
            std::chrono::milliseconds(16));
    }

    // Mark telemetry unavailable.
    m_pBufferIn
        ->outSimDataIN
        ->mPacketHeader
        .reportAvailable = 0;
}

// =========================================================
// Fake Telemetry Generator
// =========================================================

void NotepadExamplePlugin::GenerateFakeTelemetry(
    OutSimData& data,
    float timeSec)
{
    // =====================================================
    // Packet Header
    // =====================================================

    data.mPacketHeader.paused = false;

    data.mPacketHeader.playerSlotIndex = 0;

    data.mPacketHeader.elapsedTime = timeSec;

    // =====================================================
    // Session
    // =====================================================

    data.mSessionData.sessionTime = timeSec;

    // =====================================================
    // Players
    // =====================================================

    data.mPlayers.carCount = 1;

    strcpy_s(
        data.mPlayers.player[0].carName,
        "Notepad GT3");

    // Fake circular trajectory.
    data.mPlayers.player[0].worldPositionX =
        std::cos(timeSec) * 100.0f;

    data.mPlayers.player[0].worldPositionY =
        std::sin(timeSec) * 100.0f;

    data.mPlayers.player[0].worldPositionZ =
        0.0f;

    // =====================================================
    // Vehicle Data
    // =====================================================

    data.mVehicleData.speed =
        120.0f + std::sin(timeSec) * 20.0f;

    data.mVehicleData.rpm =
        5000.0f + std::sin(timeSec * 2.0f) * 1500.0f;

    data.mVehicleData.maxRpm = 8000.0f;

    data.mVehicleData.gear =
        3 + (int)(std::sin(timeSec) * 2.0f);

    data.mVehicleData.maxGear = 6;

    data.mVehicleData.throttle =
        (std::sin(timeSec) + 1.0f) * 0.5f;

    data.mVehicleData.brake =
        (std::sin(timeSec * 0.5f) + 1.0f) * 0.5f;

    data.mVehicleData.clutch = 0.0f;

    data.mVehicleData.fuelLevel =
        75.0f;

    data.mVehicleData.fuelCapacity =
        100.0f;

    data.mVehicleData.engineTemp =
        92.0f;

    data.mVehicleData.oilTemp =
        105.0f;

    data.mVehicleData.oilPress =
        4.2f;

    data.mVehicleData.turboLevel =
        0.8f;

    data.mVehicleData.absInAction =
        (std::sin(timeSec * 8.0f) > 0.8f);

    data.mVehicleData.tcInAction =
        (std::sin(timeSec * 6.0f) > 0.8f);

    // =====================================================
    // Warning Flags
    // =====================================================

    data.mVehicleData.warningFlag.headlights = 1;

    data.mVehicleData.warningFlag.leftTurnSignal =
        (std::sin(timeSec * 2.0f) > 0.0f);

    data.mVehicleData.warningFlag.rightTurnSignal =
        (std::sin(timeSec * 2.0f) < 0.0f);

    data.mVehicleData.warningFlag.oilWarning = 0;

    data.mVehicleData.warningFlag.pitLimiterActive = 0;

    data.mVehicleData.warningFlag.turboStatus = 1;

    // =====================================================
    // Motion Data
    // =====================================================

    data.mMotionData.velocityX =
        std::cos(timeSec) * 15.0f;

    data.mMotionData.velocityY =
        0.0f;

    data.mMotionData.velocityZ =
        std::sin(timeSec) * 15.0f;

    data.mMotionData.localAccelX =
        std::sin(timeSec * 5.0f);

    data.mMotionData.localAccelY =
        0.1f;

    data.mMotionData.localAccelZ =
        std::cos(timeSec * 5.0f);

    data.mMotionData.pitch =
        std::sin(timeSec) * 5.0f;

    data.mMotionData.roll =
        std::cos(timeSec) * 3.0f;

    data.mMotionData.yaw =
        timeSec * 10.0f;

    data.mMotionData.pitchRate =
        std::cos(timeSec);

    data.mMotionData.rollRate =
        std::sin(timeSec);

    data.mMotionData.yawRate =
        10.0f;

    data.mMotionData.angularVelocityX =
        data.mMotionData.pitchRate;

    data.mMotionData.angularVelocityY =
        data.mMotionData.yawRate;

    data.mMotionData.angularVelocityZ =
        data.mMotionData.rollRate;

    data.mMotionData.angularAccelX =
        std::sin(timeSec * 2.0f);

    data.mMotionData.angularAccelY =
        std::cos(timeSec * 2.0f);

    data.mMotionData.angularAccelZ =
        std::sin(timeSec * 4.0f);

    data.mMotionData.upDirX = 0.0f;

    data.mMotionData.upDirY = 1.0f;

    data.mMotionData.upDirZ = 0.0f;
}

9. Configure Plugin Extension

IMPORTANT:

GAME plugins use:

.osri

OUT plugins use:

Open:

.osro

Project Properties
→ General
→ Target Extension

Replace:

.dll

with:

.osri

Your final binary should become:

NotepadExamplePlugin.osri

10. Configure Output Directory

OpenSR plugin directories are located in:

Documents/OpenSR/Plugins/

Recommended output directory:

$(USERPROFILE)\Documents\OpenSR\Plugins\NotepadExamplePlugin\

Configure:

Project Properties
→ General
→ Output Directory

Recommended:

$(USERPROFILE)\Documents\OpenSR\Plugins\NotepadExamplePlugin\

11. Mandatory Plugin Assets

Each plugin must provide standardized image assets.

These assets are used by:

  • plugin manager
  • dashboard
  • plugin browser
  • featured views
  • thumbnail previews

Mandatory files:

ImageFilePurpose
icon.pngIcon 32 x 32 (for Sidebar & Header)
thumbnail.pngThumbnail 228 x 86 (for game profiles)
featured.pngImage 512 x 282 (for Info page)

Recommended final plugin directory:

Documents/
└── OpenSR/
    └── Plugins/
        └── NotepadExamplePlugin/
            ├── NotepadExamplePlugin.osri
            ├── icon.png
            ├── thumbnail.png
            └── featured.png

12. Exported Plugin Functions

Every GAME plugin must export:

extern "C" __declspec(dllexport)
osr::IOpenSRPlugin* CreatePlugin()

and:

extern "C" __declspec(dllexport)
void DestroyPlugin(osr::IOpenSRPlugin* plugin)

Without these exports OpenSR cannot instantiate the plugin.


13. Verify Exports

Open:

Developer Command Prompt for VS2019

Run:

dumpbin /exports NotepadExamplePlugin.osri

You MUST see:

CreatePlugin
DestroyPlugin

14. Adding Runtime Settings Support

The Notepad example can also demonstrate runtime settings support.

Create:

settings.xml

inside the plugin directory.

Example:

<settings>
    <option
        name="change telemetry rate"
        tooltip="change rate"
        type="slider"
        range="1,16">16</option>
</settings>

Final directory:

Documents/
└── OpenSR/
    └── Plugins/
        └── NotepadExamplePlugin/
            ├── NotepadExamplePlugin.osri
            ├── settings.xml
            ├── icon.png
            ├── thumbnail.png
            └── featured.png

15. Building The Settings Path

Inside Init() the plugin should build the path to settings.xml.

The SDK provides StringUtils.

Example:

#include "StringUtils.h" // included in the SDK

bool NotepadExamplePlugin::Init(
    OpenSRContext* context,
    void* inBuf,
    const wchar_t* pluginPath)
{
    m_pContext = context;

    m_pBufferIn =
        static_cast<OpenSRBuffersIN*>(inBuf);

    m_pluginPath = pluginPath;

    // Build:
    // Documents/OpenSR/Plugins/NotepadExamplePlugin/settings.xml
    xmlPath = StringUtils::createWString(
        m_pContext->osrDocFolder,
        L"\\Plugins\\",
        pluginPath,
        L"\\settings.xml");

    return true;
}

This is the recommended SDK way to build plugin-relative paths.


16. Loading Settings

The OpenSR SDK examples use TinyXML2 for XML parsing.

Before implementing the settings loader, include:

#include "tinyxml2.h"

and add:

using namespace tinyxml2;

Example runtime settings structure:

struct PluginSettings
{
    int telemetryRate = 16;
};

Example loading function:

bool LoadPluginSettings(
    const std::wstring& filepath,
    PluginSettings& s)
{
    XMLDocument doc;

    FILE* file =
        _wfsopen(
            filepath.c_str(),
            L"rb",
            _SH_DENYNO);

    if (!file)
        return false;

    if (doc.LoadFile(file) != XML_SUCCESS)
    {
        fclose(file);
        return false;
    }

    fclose(file);

    XMLElement* root =
        doc.FirstChildElement("settings");

    if (!root)
        return false;

    for (XMLElement* opt =
            root->FirstChildElement("option");
         opt;
         opt = opt->NextSiblingElement("option"))
    {
        const char* name =
            opt->Attribute("name");

        if (!name)
            continue;

        if (strcmp(name,
                   "change telemetry rate") == 0)
        {
            s.telemetryRate =
                opt->IntText(16);
        }
    }

    return true;
}

Recommended additional include list:

#include "IOpenSRPlugin.h"
#include "OpenSRBuffers.h"
#include "OpenSRContext.h"

#include "tinyxml2.h"

#include <thread>
#include <atomic>
#include <mutex>
#include <condition_variable>

TinyXML2 is lightweight, fast, portable, and recommended for OpenSR plugin settings parsing.


17. Using The Runtime Setting

Inside the worker thread:

std::this_thread::sleep_for(
    std::chrono::milliseconds(
        m_settings.telemetryRate));

This allows runtime modification of telemetry frequency.


18. Responding To Runtime Settings Changes

It is recommended to load plugin settings inside Start().

This guarantees that every runtime restart uses the latest configuration automatically.

Recommended flow:

OnContextChanged()
    ↓
Stop()
    ↓
Start()
    ↓
LoadPluginSettings()

This keeps runtime reload logic simple and centralized.


Recommended Start() Pattern

Example:

bool NotepadExamplePlugin::Start()
{
    std::lock_guard<std::mutex>
        lock(lifecycleMutex);

    if (!m_pContext)
        return false;

    // Reload latest settings every time
    // the plugin starts.
    LoadPluginSettings(
        xmlPath,
        m_settings);

    m_stopRequested = false;

    m_isPluginPaused = false;

    m_isPluginRunning = true;

    workerThread =
        std::thread(
            &NotepadExamplePlugin::WorkerThread,
            this);

    return true;
}

This guarantees that runtime configuration changes are always applied consistently.


Recommended OnContextChanged()

Example:

void NotepadExamplePlugin::OnContextChanged(
    osr::OpenSRContextChange reason)
{
    if (reason ==
        osr::OpenSRContextChange
            ::SettingsChanged)
    {
        // Restart plugin runtime.
        // Settings will automatically
        // be reloaded inside Start().
        Stop();

        Start();
    }
}

This is the recommended production pattern for most GAME plugins because:

  • settings logic remains centralized
  • runtime reload stays predictable
  • no duplicated settings code exists
  • all telemetry systems restart consistently
  • sockets/SDKs are recreated cleanly
  • worker threads restart safely

19. Build The Plugin

Build:

Build
→ Build Solution

Expected result:

Documents/OpenSR/Plugins/NotepadExamplePlugin/
└── NotepadExamplePlugin.osri

20. Launch OpenSR

Launch OpenSR.

Open:

Notepad.exe

OpenSR should:

  • detect the process
  • load the plugin
  • start the worker thread
  • generate telemetry
  • submit frames at runtime

When Notepad closes:

the plugin should stop automatically.


21. Final Recommended Directory

Final recommended plugin directory:

Documents/
└── OpenSR/
    └── Plugins/
        └── NotepadExamplePlugin/
            ├── NotepadExamplePlugin.osri
            ├── settings.xml
            ├── icon.png
            ├── thumbnail.png
            └── featured.png

This is a complete valid OpenSR GAME plugin package.