Система GameData позволяет плагинам получать доступ к внутренним компонентам игры через сигнатуры, смещения, адреса, патчи и виртуальные таблицы. Это позволяет перехватывать функции, читать память и изменять поведение игры без жёстко закодированных адресов.
Файлы GameData - это конфигурационные файлы на основе JSON, которые содержат платформозависимые шаблоны и смещения. Система автоматически обрабатывает различия платформ (Windows/Linux) и обновления игры, используя сканирование сигнатур вместо жёстко закодированных адресов.
Ключевые преимущества:
Платформонезависимый код (Windows/Linux обрабатываются автоматически)
Устойчивость к обновлениям игры (сигнатуры автоматически находят адреса)
Централизованная конфигурация для операций с памятью
s2sdk предоставляет два способа работы с gamedata:
Функциональный API - Ручное управление дескрипторами с помощью LoadGameConfigFile / CloseGameConfigFile
Объектно-ориентированный API - RAII-обёртка с использованием класса GameConfig (автоматическая очистка)
Рекомендуется: Используйте класс GameConfig для автоматического управления ресурсами (RAII). Он автоматически закрывает файл gamedata при уничтожении объекта, предотвращая утечки памяти.
Используйте LoadGameConfigFile для загрузки пользовательского файла gamedata и CloseGameConfigFile по завершении.
Массив путей:LoadGameConfigFile принимает массив путей к файлам для поиска файла gamedata. Чтобы загрузить из директории вашего плагина, постройте полный путь, используя расположение вашего плагина.
Каждый язык предоставляет способ доступа к директории плагина:
Язык
Метод
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
Важно: Файл gamedata должен быть размещён в директории данных вашего плагина. Имя файла, передаваемое в LoadGameConfigFile, должно быть относительно этой директории.
Класс GameConfig - это RAII-обёртка, которая автоматически управляет ресурсами gamedata. Он создаётся с помощью LoadGameConfigFile и автоматически вызывает CloseGameConfigFile при уничтожении.
GetPatch(name) - Получить адрес, по которому был применён патч
GetOffset(name) - Получить значение именованного смещения
GetAddress(name) - Получить разрешённый адрес
GetVTable(name) - Получить адрес виртуальной таблицы
GetSignature(name) - Получить адрес именованной сигнатуры
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
Ключевое отличие: С ООП-подходом вам не нужно передавать дескриптор gamedata в каждый вызов метода. Объект GameConfig инкапсулирует дескриптор и предоставляет методы-члены, которые автоматически используют его.
Сигнатуры - это байтовые шаблоны, используемые для поиска функций или данных в игровых бинарных файлах. Они устойчивы к незначительным обновлениям игры.
Именование сигнатур: Если сигнатура начинается с @ (например, @FunctionName), система будет обрабатывать её как имя символа вместо байтового шаблона. Это полезно для экспортируемых функций.
// 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)
Выполнение шагов: Каждое действие адреса выполняется последовательно. Например, EntityList сначала добавляет смещение 10, затем читает указатель 2 раза.
// 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")
Патчи позволяют автоматически изменять игровые бинарные файлы во время выполнения. Они находят код с помощью сигнатур и заменяют байты.
Предупреждение: Патчи напрямую изменяют память игры. Неправильные патчи могут привести к сбою сервера. Всегда проверяйте байты перед применением патчей!
win64 / linuxsteamrt64 - Шестнадцатеричные байты для записи (байты патча)
Осторожно: Патчи напрямую изменяют память игры и применяются автоматически при загрузке. Тщательно тестируйте, чтобы избежать сбоев!
Автоматическое применение: Патчи применяются автоматически при загрузке файла gamedata. Вам не нужно вызывать какие-либо функции - система обрабатывает это!
Хотя патчи применяются автоматически, вы всё ещё можете запрашивать данные о патчах:
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")
Виртуальные таблицы (VTables) используются для поиска виртуальных функций в объектах C++. Используйте их для поиска указателей на функции в виртуальных таблицах классов.
// 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")
Получить адрес, по которому был применён патч в конкретной конфигурации.
GetGameConfigPatchAll(name)
Искать патч во всех загруженных конфигурациях.
Примечание: Патчи применяются автоматически при загрузке gamedata. Используйте GetGameConfigPatch() для проверки расположения патчей, а не для их применения.
Используйте read_offs32 для RIP-относительной адресации (распространено в x64). Это читает 32-битное относительное смещение и вычисляет абсолютный адрес:
Это полезно, когда сигнатура указывает на инструкции вроде call [rip + offset] или lea reg, [rip + offset]. Значение read_offs32 - это байтовое смещение, где находится 32-битное относительное смещение.
С этим руководством вы теперь обладаете полными знаниями о системе GameData в s2sdk. Используйте её для создания надёжных, устойчивых к обновлениям плагинов, которые взаимодействуют с внутренними компонентами игры!