GameData System

How to use custom gamedata files for signatures, offsets, addresses, patches, and vtables.

Overview

The GameData system allows plugins to access game internals through signatures, offsets, addresses, patches, and vtables. This enables hooking functions, reading memory, and modifying game behavior without hardcoded addresses.

GameData files are JSON-based configuration files that contain platform-specific patterns and offsets. The system automatically handles platform differences (Windows/Linux) and game updates by using signature scanning instead of hardcoded addresses.

API Approaches

The s2sdk provides two ways to work with gamedata:

  1. Functional API - Manual handle management with LoadGameConfigFile / CloseGameConfigFile
  2. Object-Oriented API - RAII wrapper using the GameConfig class (automatic cleanup)

Loading and Closing GameData Files (Functional API)

Use LoadGameConfigFile to load a custom gamedata file and CloseGameConfigFile when done.

Getting Plugin Location

Each language provides a way to access the plugin's directory:

LanguageMethod
C#this.GetLocation()
C++this->GetLocation()
Pythonself.location
Goplugify.Plugin.Location
JavaScriptthis.location
Luaself.location
c#
c++
python
go
js
lua
using Plugify;
using static s2sdk.s2sdk;

public unsafe class Sample : Plugin
{
    private nint gameData;

    public void OnPluginStart()
    {
        // Construct path to gamedata file in plugin directory
        string pluginPath = this.GetLocation();
        string gamedataPath = Path.Combine(pluginPath, "my_gamedata.json");

        // Load gamedata file (requires array of paths)
        gameData = LoadGameConfigFile(new[] { gamedataPath });

        if (gameData == IntPtr.Zero)
        {
            PrintToServer("Failed to load gamedata file!\n");
            return;
        }

        // Use gamedata methods...
        int offset = GetGameConfigOffset(gameData, "SomeOffset");
        PrintToServer($"Offset value: {offset}\n");
    }

    public void OnPluginEnd()
    {
        // Clean up gamedata
        if (gameData != IntPtr.Zero)
        {
            CloseGameConfigFile(gameData);
            gameData = IntPtr.Zero;
        }
    }
}

JSON Schema Structure

GameData files use JSON format with platform-specific configurations. Here's the basic structure:

{
  "Signatures": {
    "SignatureName": {
      "library": "server",
      "win64": "48 89 5C 24 ? 48 89 6C 24 ? 48 89 74 24 ? 57 41 56 41 57 48 83 EC 40",
      "linuxsteamrt64": "55 48 89 E5 41 57 41 56 41 55 41 54 53 48 83 EC 18"
    }
  },
  "Offsets": {
    "OffsetName": {
      "win64": 123,
      "linuxsteamrt64": 456
    }
  },
  "Addresses": {
    "AddressName": {
      "signature": "SignatureName",
      "win64": [
        {
          "offset": 10
        },
        {
          "read": 2
        }
      ],
      "linuxsteamrt64": [
        {
          "offset": 5
        },
        {
          "read": 2
        }
      ]
    }
  },
  "Patches": {
    "PatchName": {
      "address": "SignatureName",
      "win64": "EB",
      "linuxsteamrt64": "90 90"
    }
  },
  "VTables": {
    "VTableName": {
      "library": "server",
      "table": "CBaseEntity"
    }
  }
}

Using GameConfig Class (Object-Oriented API)

The GameConfig class is a RAII wrapper that automatically manages gamedata resources. It's constructed using LoadGameConfigFile and automatically calls CloseGameConfigFile when destroyed.

GameConfig Methods

The GameConfig class provides the following methods:

  • GetPatch(name) - Get address where a patch was applied
  • GetOffset(name) - Get a named offset value
  • GetAddress(name) - Get a resolved address
  • GetVTable(name) - Get address of a virtual table
  • GetSignature(name) - Get address of a named signature
c#
c++
python
go
js
lua
using Plugify;
using static s2sdk.s2sdk;

public unsafe class Sample : Plugin
{
    private GameConfig? gameConfig;

    public void OnPluginStart()
    {
        // Constructor automatically loads gamedata
        gameConfig = new GameConfig("my_gamedata.json");

        if (gameConfig == null || !gameConfig.IsValid)
        {
            PrintToServer("Failed to load gamedata file!\n");
            return;
        }

        // Use member methods directly on the object
        int offset = gameConfig.GetOffset("SomeOffset");
        PrintToServer($"Offset value: {offset}\n");

        nint signature = gameConfig.GetSignature("SignatureName");
        PrintToServer($"Signature at: 0x{signature:X}\n");

        nint address = gameConfig.GetAddress("GlobalVars");
        PrintToServer($"Address at: 0x{address:X}\n");
    }

    public void OnPluginEnd()
    {
        // Destructor automatically closes gamedata - no manual cleanup needed!
        gameConfig?.Dispose();
        gameConfig = null;
    }
}

Signatures

Signatures are byte patterns used to locate functions or data in game binaries. They're resilient to minor game updates.

Signature Format

Signatures use hex bytes with ? as wildcards:

  • 48 89 5C 24 - Exact bytes
  • ? or ?? - Any single byte (wildcard)
  • Space-separated hex values

Generating Signatures

You can generate signatures using:

  1. IDA Pro with sigmaker plugin
  2. Binary Ninja with signature generation scripts
  3. Ghidra with custom scripts

Using Signatures

c#
c++
python
// Get single signature
nint address = GetGameConfigSignature(gameData, "SignatureName");
if (address != IntPtr.Zero)
{
    PrintToServer($"Found signature at: 0x{address:X}\n");
}

// Search all loaded configs for signature
nint signatureAll = GetGameConfigSignatureAll("SignatureName");

Offsets

Offsets are numeric values representing memory positions or structure field offsets. They're platform-specific.

Defining Offsets

{
  "Offsets": {
    "HealthOffset": {
      "win64": 3216,
      "linuxsteamrt64": 3240
    },
    "VelocityOffset": {
      "win64": 1024,
      "linuxsteamrt64": 1040
    }
  }
}

Using Offsets

c#
c++
python
// Get single offset
int healthOffset = GetGameConfigOffset(gameData, "HealthOffset");
PrintToServer($"Health offset: {healthOffset}\n");

// Search all loaded configs for offset
int offsetAll = GetGameConfigOffsetAll("HealthOffset");

// Use offset with entity data
int entityHandle = PlayerSlotToEntHandle(0);
SetEntData(entityHandle, healthOffset, 200, 4, true, 0);

Addresses

Addresses combine signatures with offset calculations to locate specific memory positions. They support three resolution types:

Address Types

  1. offset - Add offset to signature address
  2. read - Read pointer at signature + offset, follow N times
  3. read_offs32 - Read 32-bit relative offset, calculate absolute address

Defining Addresses

Addresses are defined as arrays of action steps for each platform:

{
  "Addresses": {
    "GlobalVars": {
      "signature": "GlobalVarsSignature",
      "win64": [
        {
          "offset": 3
        }
      ],
      "linuxsteamrt64": [
        {
          "offset": 3
        }
      ]
    },
    "EntityList": {
      "signature": "EntityListSignature",
      "win64": [
        {
          "offset": 10
        },
        {
          "read": 2
        }
      ],
      "linuxsteamrt64": [
        {
          "offset": 8
        },
        {
          "read": 2
        }
      ]
    },
    "SomeFunction": {
      "signature": "FunctionCallSignature",
      "win64": [
        {
          "read_offs32": 1
        }
      ],
      "linuxsteamrt64": [
        {
          "read_offs32": 1
        }
      ]
    }
  }
}

Using Addresses

c#
c++
python
// Get single address
nint globalVarsAddr = GetGameConfigAddress(gameData, "GlobalVars");
if (globalVarsAddr != IntPtr.Zero)
{
    PrintToServer($"GlobalVars at: 0x{globalVarsAddr:X}\n");

    // Use the address to read/write memory
    // ... pointer dereferencing code ...
}

// Search all loaded configs for address
nint addressAll = GetGameConfigAddressAll("GlobalVars");

Patches

Patches allow automatic modification of game binaries at runtime. They locate code using signatures and replace bytes.

Defining Patches

Patches reference an address (from Addresses or Signatures sections) and specify the patch bytes to write:

{
  "Patches": {
    "VScriptEnable": {
      "address": "VScriptInitialization",
      "win64": "BE 02",
      "linuxsteamrt64": "83 FE 02"
    },
    "NoValidation": {
      "address": "ValidationCheck",
      "win64": "EB",
      "linuxsteamrt64": "90 90"
    }
  }
}
  • address - Name of address/signature from the gamedata file
  • win64 / linuxsteamrt64 - Hex bytes to write (patch bytes)

Retrieving Patch Information

While patches are applied automatically, you can still query patch data:

c#
c++
python
// Get single patch address (where it was applied)
nint patchAddr = GetGameConfigPatch(gameData, "DisableValidation");
if (patchAddr != IntPtr.Zero)
{
    PrintToServer($"Patch applied at: 0x{patchAddr:X}\n");
}

// Search all loaded configs for patch
nint patchAll = GetGameConfigPatchAll("DisableValidation");

VTables

Virtual tables (VTables) are used for virtual function lookups in C++ objects. Use them to find function pointers in class vtables.

Defining VTables

{
  "VTables": {
    "CBaseEntity": {
      "signature": "CBaseEntityVTableSignature",
      "offset": {
        "windows": 0,
        "linux": 0
      }
    },
    "CBasePlayer": {
      "signature": "CBasePlayerVTableSignature",
      "offset": {
        "windows": 8,
        "linux": 8
      }
    }
  }
}

Using VTables

c#
c++
python
// Get single vtable
nint vtableAddr = GetGameConfigVTable(gameData, "CBaseEntity");
if (vtableAddr != IntPtr.Zero)
{
    PrintToServer($"CBaseEntity vtable at: 0x{vtableAddr:X}\n");

    // You can now access virtual function pointers
    // vtable[0], vtable[1], etc.
}

// Search all loaded configs for vtable
nint vtableAll = GetGameConfigVTableAll("CBaseEntity");

Complete Example: Custom Gamedata Plugin

Here's a complete example that demonstrates all gamedata features:

c#
c++
python
go
js
lua
using System;
using Plugify;
using static s2sdk.s2sdk;

public unsafe class GameDataExample : Plugin
{
    private nint gameData;

    public void OnPluginStart()
    {
        // Load custom gamedata
        gameData = LoadGameConfigFile("example.json");

        if (gameData == IntPtr.Zero)
        {
            PrintToServer("[ERROR] Failed to load gamedata!\n");
            return;
        }

        PrintToServer("=== GameData Loaded Successfully ===\n");

        // Demonstrate Signature
        PrintToServer("\n--- Signature Example ---\n");
        nint signature = GetGameConfigSignature(gameData, "SignatureName");
        PrintToServer($"  Signature: 0x{signature:X}\n");

        // Demonstrate Offset
        PrintToServer("\n--- Offset Example ---\n");
        int offset = GetGameConfigOffset(gameData, "OffsetName");
        PrintToServer($"  Offset: {offset}\n");

        // Demonstrate Address
        PrintToServer("\n--- Address Example ---\n");
        nint addr = GetGameConfigAddress(gameData, "AddressName");
        PrintToServer($"  Address: 0x{addr:X}\n");

        // Demonstrate Patch
        PrintToServer("\n--- Patch Example ---\n");
        nint patchAddr = GetGameConfigPatch(gameData, "PatchName");
        PrintToServer($"  Patch: 0x{patchAddr:X} (auto-applied)\n");

        // Demonstrate VTable
        PrintToServer("\n--- VTable Example ---\n");
        nint vtableAddr = GetGameConfigVTable(gameData, "VTableName");
        PrintToServer($"  VTable: 0x{vtableAddr:X}\n");

        PrintToServer("\n=== GameData Demo Complete ===\n");
    }

    public void OnPluginEnd()
    {
        if (gameData != IntPtr.Zero)
        {
            CloseGameConfigFile(gameData);
            gameData = IntPtr.Zero;
            PrintToServer("GameData closed.\n");
        }
    }
}

Method Reference

File Management

MethodDescription
LoadGameConfigFile(filename)Load a gamedata JSON file. Returns handle or null on failure.
CloseGameConfigFile(handle)Close and free a gamedata file.

Signatures

MethodDescription
GetGameConfigSignature(handle, name)Get address of a named signature from specific config.
GetGameConfigSignatureAll(name)Search all loaded configs for signature.

Offsets

MethodDescription
GetGameConfigOffset(handle, name)Get a named offset value from specific config.
GetGameConfigOffsetAll(name)Search all loaded configs for offset.

Addresses

MethodDescription
GetGameConfigAddress(handle, name)Get a resolved address from specific config.
GetGameConfigAddressAll(name)Search all loaded configs for address.

Patches

MethodDescription
GetGameConfigPatch(handle, name)Get address where patch was applied in specific config.
GetGameConfigPatchAll(name)Search all loaded configs for patch.

VTables

MethodDescription
GetGameConfigVTable(handle, name)Get address of a virtual table from specific config.
GetGameConfigVTableAll(name)Search all loaded configs for vtable.

Tips and Best Practices

  1. Always close gamedata files - Use CloseGameConfigFile() in OnPluginEnd() to prevent memory leaks
  2. Validate signatures - Check if returned addresses are not null before using them
  3. Use meaningful names - Name your signatures, offsets, and addresses descriptively
  4. Document your patterns - Comment where signatures came from and what they point to
  5. Test on all platforms - Ensure both Windows and Linux patterns are correct
  6. Keep gamedata updated - Update signatures when game updates break them
  7. Use verify bytes in patches - Always specify verify to prevent incorrect patching
  8. Cache addresses - Store resolved addresses instead of calling Get methods repeatedly
  9. Organize your JSON - Group related signatures, offsets, and addresses together
  10. Version your gamedata - Consider adding version comments to track changes

Troubleshooting

Signature Not Found

  • Cause: Pattern doesn't match game binary
  • Solution: Regenerate signature using IDA/Binary Ninja, check platform-specific patterns

Patch Verify Failed

  • Cause: Game was updated and bytes changed
  • Solution: Update verify and patch bytes for new game version

Invalid Address Returned

  • Cause: Signature failed or offset calculation incorrect
  • Solution: Verify signature works, check offset and read values

Gamedata File Not Loading

  • Cause: File not found or invalid JSON
  • Solution: Check file path, validate JSON syntax

Advanced Topics

Symbol-Based Signatures

For exported functions, use @ prefix to reference by symbol name:

{
  "Signatures": {
    "CreateInterface": {
      "windows": {
        "library": "server",
        "signature": "@CreateInterface"
      },
      "linux": {
        "library": "server",
        "signature": "@CreateInterface"
      }
    }
  }
}

Multi-Level Address Resolution

Combine read with offsets to follow pointer chains:

{
  "Addresses": {
    "DeepPointer": {
      "signature": "BaseSignature",
      "win64": [
        {
          "offset": 10
        },
        {
          "read": 3
        }
      ],
      "linuxsteamrt64": [
        {
          "offset": 8
        },
        {
          "read": 3
        }
      ]
    }
  }
}

This first adds the offset, then reads the pointer 3 times (follows 3 pointer dereferences).

Relative Offset Addresses (read_offs32)

Use read_offs32 for RIP-relative addressing (common in x64). This reads a 32-bit relative offset and calculates the absolute address:

{
  "Addresses": {
    "v8::Isolate::Enter": {
      "signature": "CSScript::ResolveModule",
      "win64": [
        {
          "read_offs32": 59
        }
      ],
      "linuxsteamrt64": [
        {
          "read_offs32": 45
        }
      ]
    }
  }
}

This is useful when the signature points to instructions like call [rip + offset] or lea reg, [rip + offset]. The value of read_offs32 is the byte offset where the 32-bit relative offset is located.


With this guide, you now have complete knowledge of the GameData system in s2sdk. Use it to create robust, update-resistant plugins that interact with game internals!