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.
The s2sdk provides two ways to work with gamedata:
Functional API - Manual handle management with LoadGameConfigFile / CloseGameConfigFile
Object-Oriented API - RAII wrapper using the GameConfig class (automatic cleanup)
Recommended: Use the GameConfig class for automatic resource management (RAII). It automatically closes the gamedata file when the object is destroyed, preventing memory leaks.
Use LoadGameConfigFile to load a custom gamedata file and CloseGameConfigFile when done.
Path Array:LoadGameConfigFile takes an array of file paths to search for the gamedata file. To load from your plugin's directory, construct the full path using your plugin's location.
Each language provides a way to access the plugin's directory:
Language
Method
C#
this.GetLocation()
C++
this->GetLocation()
Python
self.location
Go
plugify.Plugin.Location
JavaScript
this.location
Lua
self.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;
}
}
}
#include <plugify/cpp_plugin.hpp>
#include <filesystem>
#include "s2sdk.hpp"
using namespace s2sdk;
class Sample : public plg::IPluginEntry {
private:
void* gameData = nullptr;
public:
void OnPluginStart() override {
// Construct path to gamedata file in plugin directory
std::filesystem::path pluginPath = this->GetLocation();
std::filesystem::path gamedataPath = pluginPath / "my_gamedata.json";
// Load gamedata file (requires vector of paths)
plg::vector<plg::string> paths = { gamedataPath.string() };
gameData = LoadGameConfigFile(paths);
if (gameData == nullptr) {
PrintToServer("Failed to load gamedata file!\n");
return;
}
// Use gamedata methods...
int offset = GetGameConfigOffset(gameData, "SomeOffset");
PrintToServer(std::format("Offset value: {}\n", offset).c_str());
}
void OnPluginEnd() override {
// Clean up gamedata
if (gameData != nullptr) {
CloseGameConfigFile(gameData);
gameData = nullptr;
}
}
};
import os
from plugify.plugin import Plugin
from plugify.pps import s2sdk as s2
class Sample(Plugin):
def __init__(self):
super().__init__()
self.game_data = None
def plugin_start(self):
# Construct path to gamedata file in plugin directory
plugin_path = self.location
gamedata_path = os.path.join(plugin_path, "my_gamedata.json")
# Load gamedata file (requires list of paths)
self.game_data = s2.LoadGameConfigFile([gamedata_path])
if self.game_data is None:
s2.PrintToServer("Failed to load gamedata file!\n")
return
# Use gamedata methods...
offset = s2.GetGameConfigOffset(self.game_data, "SomeOffset")
s2.PrintToServer(f"Offset value: {offset}\n")
def plugin_end(self):
# Clean up gamedata
if self.game_data is not None:
s2.CloseGameConfigFile(self.game_data)
self.game_data = None
package main
import (
"fmt"
"path/filepath"
"unsafe"
"s2sdk"
"github.com/untrustedmodders/go-plugify"
)
var gameData unsafe.Pointer
func init() {
plugify.OnPluginStart(func() {
// Construct path to gamedata file in plugin directory
pluginPath := plugify.Plugin.Location
gamedataPath := filepath.Join(pluginPath, "my_gamedata.json")
// Load gamedata file (requires slice of paths)
gameData = s2sdk.LoadGameConfigFile([]string{gamedataPath})
if gameData == nil {
s2sdk.PrintToServer("Failed to load gamedata file!\n")
return
}
// Use gamedata methods...
offset := s2sdk.GetGameConfigOffset(gameData, "SomeOffset")
s2sdk.PrintToServer(fmt.Sprintf("Offset value: %d\n", offset))
})
plugify.OnPluginEnd(func() {
// Clean up gamedata
if gameData != nil {
s2sdk.CloseGameConfigFile(gameData)
gameData = nil
}
})
}
import { Plugin } from 'plugify';
import * as s2 from ':s2sdk';
import path from 'path';
export class Sample extends Plugin {
constructor() {
super();
this.gameData = null;
}
pluginStart() {
// Construct path to gamedata file in plugin directory
const pluginPath = this.location;
const gamedataPath = path.join(pluginPath, "my_gamedata.json");
// Load gamedata file (requires array of paths)
this.gameData = s2.LoadGameConfigFile([gamedataPath]);
if (this.gameData === null) {
s2.PrintToServer("Failed to load gamedata file!\n");
return;
}
// Use gamedata methods...
const offset = s2.GetGameConfigOffset(this.gameData, "SomeOffset");
s2.PrintToServer(`Offset value: ${offset}\n`);
}
pluginEnd() {
// Clean up gamedata
if (this.gameData !== null) {
s2.CloseGameConfigFile(this.gameData);
this.gameData = null;
}
}
}
local plugify = require 'plugify'
local Plugin = plugify.Plugin
local s2 = require 's2sdk'
local Sample = {}
setmetatable(Sample, { __index = Plugin })
function Sample:new()
local obj = {}
setmetatable(obj, { __index = self })
obj.game_data = nil
return obj
end
function Sample:plugin_start()
-- Construct path to gamedata file in plugin directory
local plugin_path = self.location
local gamedata_path = plugin_path .. "/my_gamedata.json"
-- Load gamedata file (requires table of paths)
self.game_data = s2:LoadGameConfigFile({gamedata_path})
if self.game_data == nil then
s2:PrintToServer("Failed to load gamedata file!\n")
return
end
-- Use gamedata methods...
local offset = s2:GetGameConfigOffset(self.game_data, "SomeOffset")
s2:PrintToServer(string.format("Offset value: %d\n", offset))
end
function Sample:plugin_end()
-- Clean up gamedata
if self.game_data ~= nil then
s2:CloseGameConfigFile(self.game_data)
self.game_data = nil
end
end
local M = {}
M.Sample = Sample
return M
Important: The gamedata file should be placed in your plugin's data directory. The filename passed to LoadGameConfigFile should be relative to that directory.
The GameConfig class is a RAII wrapper that automatically manages gamedata resources. It's constructed using LoadGameConfigFile and automatically calls CloseGameConfigFile when destroyed.
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;
}
}
#include <plugify/cpp_plugin.hpp>
#include "s2sdk.hpp"
using namespace s2sdk;
class Sample : public plg::IPluginEntry {
private:
std::unique_ptr<GameConfig> gameConfig;
public:
void OnPluginStart() override {
// Constructor automatically loads gamedata
gameConfig = std::make_unique<GameConfig>("my_gamedata.json");
if (!gameConfig || !gameConfig->IsValid()) {
PrintToServer("Failed to load gamedata file!\n");
return;
}
// Use member methods directly on the object
int offset = gameConfig->GetOffset("SomeOffset");
PrintToServer(std::format("Offset value: {}\n", offset).c_str());
void* signature = gameConfig->GetSignature("SignatureName");
PrintToServer(std::format("Signature at: {:X}\n",
reinterpret_cast<uintptr_t>(signature)).c_str());
void* address = gameConfig->GetAddress("GlobalVars");
PrintToServer(std::format("Address at: {:X}\n",
reinterpret_cast<uintptr_t>(address)).c_str());
}
void OnPluginEnd() override {
// Destructor automatically closes gamedata - no manual cleanup needed!
gameConfig.reset();
}
};
from plugify.plugin import Plugin
from plugify.pps import s2sdk as s2
class Sample(Plugin):
def __init__(self):
super().__init__()
self.game_config = None
def plugin_start(self):
# Constructor automatically loads gamedata
self.game_config = s2.GameConfig("my_gamedata.json")
if not self.game_config or not self.game_config.is_valid():
s2.PrintToServer("Failed to load gamedata file!\n")
return
# Use member methods directly on the object
offset = self.game_config.get_offset("SomeOffset")
s2.PrintToServer(f"Offset value: {offset}\n")
signature = self.game_config.get_signature("SignatureName")
s2.PrintToServer(f"Signature at: 0x{signature:X}\n")
address = self.game_config.get_address("GlobalVars")
s2.PrintToServer(f"Address at: 0x{address:X}\n")
def plugin_end(self):
# Destructor automatically closes gamedata - no manual cleanup needed!
if self.game_config is not None:
del self.game_config
self.game_config = None
package main
import (
"fmt"
"s2sdk"
"github.com/untrustedmodders/go-plugify"
)
var gameConfig *s2sdk.GameConfig
func init() {
plugify.OnPluginStart(func() {
// Constructor automatically loads gamedata
gameConfig = s2sdk.NewGameConfig("my_gamedata.json")
if gameConfig == nil || !gameConfig.IsValid() {
s2sdk.PrintToServer("Failed to load gamedata file!\n")
return
}
// Use member methods directly on the object
offset := gameConfig.GetOffset("SomeOffset")
s2sdk.PrintToServer(fmt.Sprintf("Offset value: %d\n", offset))
signature := gameConfig.GetSignature("SignatureName")
s2sdk.PrintToServer(fmt.Sprintf("Signature at: 0x%X\n",
uintptr(signature)))
address := gameConfig.GetAddress("GlobalVars")
s2sdk.PrintToServer(fmt.Sprintf("Address at: 0x%X\n",
uintptr(address)))
})
plugify.OnPluginEnd(func() {
// Destructor automatically closes gamedata - no manual cleanup needed!
if gameConfig != nil {
gameConfig.Close()
gameConfig = nil
}
})
}
import { Plugin } from 'plugify';
import * as s2 from ':s2sdk';
export class Sample extends Plugin {
constructor() {
super();
this.gameConfig = null;
}
pluginStart() {
// Constructor automatically loads gamedata
this.gameConfig = new s2.GameConfig("my_gamedata.json");
if (!this.gameConfig || !this.gameConfig.isValid()) {
s2.PrintToServer("Failed to load gamedata file!\n");
return;
}
// Use member methods directly on the object
const offset = this.gameConfig.getOffset("SomeOffset");
s2.PrintToServer(`Offset value: ${offset}\n`);
const signature = this.gameConfig.getSignature("SignatureName");
s2.PrintToServer(`Signature at: 0x${signature.toString(16)}\n`);
const address = this.gameConfig.getAddress("GlobalVars");
s2.PrintToServer(`Address at: 0x${address.toString(16)}\n`);
}
pluginEnd() {
// Destructor automatically closes gamedata - no manual cleanup needed!
if (this.gameConfig !== null) {
this.gameConfig.close();
this.gameConfig = null;
}
}
}
local plugify = require 'plugify'
local Plugin = plugify.Plugin
local s2 = require 's2sdk'
local Sample = {}
setmetatable(Sample, { __index = Plugin })
function Sample:new()
local obj = {}
setmetatable(obj, { __index = self })
obj.game_config = nil
return obj
end
function Sample:plugin_start()
-- Constructor automatically loads gamedata
self.game_config = s2.GameConfig("my_gamedata.json")
if not self.game_config or not self.game_config:is_valid() then
s2:PrintToServer("Failed to load gamedata file!\n")
return
end
-- Use member methods directly on the object
local offset = self.game_config:get_offset("SomeOffset")
s2:PrintToServer(string.format("Offset value: %d\n", offset))
local signature = self.game_config:get_signature("SignatureName")
s2:PrintToServer(string.format("Signature at: 0x%X\n", signature))
local address = self.game_config:get_address("GlobalVars")
s2:PrintToServer(string.format("Address at: 0x%X\n", address))
end
function Sample:plugin_end()
-- Destructor automatically closes gamedata - no manual cleanup needed!
if self.game_config ~= nil then
self.game_config:close()
self.game_config = nil
end
end
local M = {}
M.Sample = Sample
return M
Key Difference: With the OOP approach, you don't need to pass the gamedata handle to each method call. The GameConfig object encapsulates the handle and provides member methods that automatically use it.
Signature Naming: If a signature starts with @ (e.g., @FunctionName), the system will treat it as a symbol name instead of a byte pattern. This is useful for exported functions.
// 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");
// Get single signature
void* address = GetGameConfigSignature(gameData, "SignatureName");
if (address != nullptr) {
PrintToServer(std::format("Found signature at: {:X}\n",
reinterpret_cast<uintptr_t>(address)).c_str());
}
// Search all loaded configs for signature
void* signatureAll = GetGameConfigSignatureAll("SignatureName");
# Get single signature
address = s2.GetGameConfigSignature(game_data, "SignatureName")
if address is not None:
s2.PrintToServer(f"Found signature at: 0x{address:X}\n")
# Search all loaded configs for signature
signature_all = s2.GetGameConfigSignatureAll("SignatureName")
// 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);
// Get single offset
int healthOffset = GetGameConfigOffset(gameData, "HealthOffset");
PrintToServer(std::format("Health offset: {}\n", healthOffset).c_str());
// 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);
# Get single offset
health_offset = s2.GetGameConfigOffset(game_data, "HealthOffset")
s2.PrintToServer(f"Health offset: {health_offset}\n")
# Search all loaded configs for offset
offset_all = s2.GetGameConfigOffsetAll("HealthOffset")
# Use offset with entity data
entity_handle = s2.PlayerSlotToEntHandle(0)
s2.SetEntData(entity_handle, health_offset, 200, 4, True, 0)
// 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");
// Get single address
void* globalVarsAddr = GetGameConfigAddress(gameData, "GlobalVars");
if (globalVarsAddr != nullptr) {
PrintToServer(std::format("GlobalVars at: {:X}\n",
reinterpret_cast<uintptr_t>(globalVarsAddr)).c_str());
// Use the address to read/write memory
// ... pointer dereferencing code ...
}
// Search all loaded configs for address
void* addressAll = GetGameConfigAddressAll("GlobalVars");
# Get single address
global_vars_addr = s2.GetGameConfigAddress(game_data, "GlobalVars")
if global_vars_addr is not None:
s2.PrintToServer(f"GlobalVars at: 0x{global_vars_addr:X}\n")
# Use the address to read/write memory
# ... pointer dereferencing code ...
# Search all loaded configs for address
address_all = s2.GetGameConfigAddressAll("GlobalVars")
address - Name of address/signature from the gamedata file
win64 / linuxsteamrt64 - Hex bytes to write (patch bytes)
Caution: Patches modify game memory directly and are automatically applied on load. Test thoroughly to avoid crashes!
Automatic Application: Patches are automatically applied when the gamedata file is loaded. You don't need to call any functions - the system handles it!
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");
// Get single patch address (where it was applied)
void* patchAddr = GetGameConfigPatch(gameData, "DisableValidation");
if (patchAddr != nullptr) {
PrintToServer(std::format("Patch applied at: {:X}\n",
reinterpret_cast<uintptr_t>(patchAddr)).c_str());
}
// Search all loaded configs for patch
void* patchAll = GetGameConfigPatchAll("DisableValidation");
# Get single patch address (where it was applied)
patch_addr = s2.GetGameConfigPatch(game_data, "DisableValidation")
if patch_addr is not None:
s2.PrintToServer(f"Patch applied at: 0x{patch_addr:X}\n")
# Search all loaded configs for patch
patch_all = s2.GetGameConfigPatchAll("DisableValidation")
// 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");
// Get single vtable
void* vtableAddr = GetGameConfigVTable(gameData, "CBaseEntity");
if (vtableAddr != nullptr) {
PrintToServer(std::format("CBaseEntity vtable at: {:X}\n",
reinterpret_cast<uintptr_t>(vtableAddr)).c_str());
// You can now access virtual function pointers
// vtable[0], vtable[1], etc.
}
// Search all loaded configs for vtable
void* vtableAll = GetGameConfigVTableAll("CBaseEntity");
# Get single vtable
vtable_addr = s2.GetGameConfigVTable(game_data, "CBaseEntity")
if vtable_addr is not None:
s2.PrintToServer(f"CBaseEntity vtable at: 0x{vtable_addr:X}\n")
# You can now access virtual function pointers
# vtable[0], vtable[1], etc.
# Search all loaded configs for vtable
vtable_all = s2.GetGameConfigVTableAll("CBaseEntity")
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!