Система GameData

Как использовать пользовательские файлы gamedata для сигнатур, смещений, адресов, патчей и виртуальных таблиц.

Обзор

Система GameData позволяет плагинам получать доступ к внутренним компонентам игры через сигнатуры, смещения, адреса, патчи и виртуальные таблицы. Это позволяет перехватывать функции, читать память и изменять поведение игры без жёстко закодированных адресов.

Файлы GameData - это конфигурационные файлы на основе JSON, которые содержат платформозависимые шаблоны и смещения. Система автоматически обрабатывает различия платформ (Windows/Linux) и обновления игры, используя сканирование сигнатур вместо жёстко закодированных адресов.

Подходы к API

s2sdk предоставляет два способа работы с gamedata:

  1. Функциональный API - Ручное управление дескрипторами с помощью LoadGameConfigFile / CloseGameConfigFile
  2. Объектно-ориентированный API - RAII-обёртка с использованием класса GameConfig (автоматическая очистка)

Загрузка и закрытие файлов GameData (функциональный API)

Используйте LoadGameConfigFile для загрузки пользовательского файла gamedata и CloseGameConfigFile по завершении.

Получение расположения плагина

Каждый язык предоставляет способ доступа к директории плагина:

ЯзыкМетод
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

Файлы GameData используют формат JSON с платформозависимыми конфигурациями. Вот базовая структура:

{
  "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"
    }
  }
}

Использование класса GameConfig (объектно-ориентированный API)

Класс GameConfig - это RAII-обёртка, которая автоматически управляет ресурсами gamedata. Он создаётся с помощью LoadGameConfigFile и автоматически вызывает CloseGameConfigFile при уничтожении.

Методы GameConfig

Класс GameConfig предоставляет следующие методы:

  • 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;
    }
}

Сигнатуры

Сигнатуры - это байтовые шаблоны, используемые для поиска функций или данных в игровых бинарных файлах. Они устойчивы к незначительным обновлениям игры.

Формат сигнатуры

Сигнатуры используют шестнадцатеричные байты с ? в качестве подстановочных знаков:

  • 48 89 5C 24 - Точные байты
  • ? или ?? - Любой одиночный байт (подстановочный знак)
  • Шестнадцатеричные значения, разделённые пробелами

Генерация сигнатур

Вы можете генерировать сигнатуры с помощью:

  1. IDA Pro с плагином sigmaker
  2. Binary Ninja со скриптами генерации сигнатур
  3. Ghidra с пользовательскими скриптами

Использование сигнатур

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": {
    "HealthOffset": {
      "win64": 3216,
      "linuxsteamrt64": 3240
    },
    "VelocityOffset": {
      "win64": 1024,
      "linuxsteamrt64": 1040
    }
  }
}

Использование смещений

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);

Адреса

Адреса объединяют сигнатуры с вычислениями смещений для определения конкретных позиций в памяти. Они поддерживают три типа разрешения:

Типы адресов

  1. offset - Добавить смещение к адресу сигнатуры
  2. read - Прочитать указатель по адресу сигнатуры + смещение, следовать N раз
  3. read_offs32 - Прочитать 32-битное относительное смещение, вычислить абсолютный адрес

Определение адресов

Адреса определяются как массивы шагов действий для каждой платформы:

{
  "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
        }
      ]
    }
  }
}

Использование адресов

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");

Патчи

Патчи позволяют автоматически изменять игровые бинарные файлы во время выполнения. Они находят код с помощью сигнатур и заменяют байты.

Определение патчей

Патчи ссылаются на адрес (из разделов Addresses или Signatures) и указывают байты патча для записи:

{
  "Patches": {
    "VScriptEnable": {
      "address": "VScriptInitialization",
      "win64": "BE 02",
      "linuxsteamrt64": "83 FE 02"
    },
    "NoValidation": {
      "address": "ValidationCheck",
      "win64": "EB",
      "linuxsteamrt64": "90 90"
    }
  }
}
  • address - Имя адреса/сигнатуры из файла gamedata
  • win64 / linuxsteamrt64 - Шестнадцатеричные байты для записи (байты патча)

Получение информации о патчах

Хотя патчи применяются автоматически, вы всё ещё можете запрашивать данные о патчах:

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) используются для поиска виртуальных функций в объектах C++. Используйте их для поиска указателей на функции в виртуальных таблицах классов.

Определение виртуальных таблиц

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

Использование виртуальных таблиц

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");

Полный пример: плагин с пользовательскими GameData

Вот полный пример, демонстрирующий все возможности gamedata:

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");
        }
    }
}

Справочник методов

Управление файлами

МетодОписание
LoadGameConfigFile(filename)Загрузить JSON-файл gamedata. Возвращает дескриптор или null при ошибке.
CloseGameConfigFile(handle)Закрыть и освободить файл gamedata.

Сигнатуры

МетодОписание
GetGameConfigSignature(handle, name)Получить адрес именованной сигнатуры из конкретной конфигурации.
GetGameConfigSignatureAll(name)Искать сигнатуру во всех загруженных конфигурациях.

Смещения

МетодОписание
GetGameConfigOffset(handle, name)Получить значение именованного смещения из конкретной конфигурации.
GetGameConfigOffsetAll(name)Искать смещение во всех загруженных конфигурациях.

Адреса

МетодОписание
GetGameConfigAddress(handle, name)Получить разрешённый адрес из конкретной конфигурации.
GetGameConfigAddressAll(name)Искать адрес во всех загруженных конфигурациях.

Патчи

МетодОписание
GetGameConfigPatch(handle, name)Получить адрес, по которому был применён патч в конкретной конфигурации.
GetGameConfigPatchAll(name)Искать патч во всех загруженных конфигурациях.

Виртуальные таблицы

МетодОписание
GetGameConfigVTable(handle, name)Получить адрес виртуальной таблицы из конкретной конфигурации.
GetGameConfigVTableAll(name)Искать виртуальную таблицу во всех загруженных конфигурациях.

Советы и лучшие практики

  1. Всегда закрывайте файлы gamedata - Используйте CloseGameConfigFile() в OnPluginEnd() для предотвращения утечек памяти
  2. Проверяйте сигнатуры - Проверяйте, что возвращённые адреса не равны null перед их использованием
  3. Используйте осмысленные имена - Давайте описательные имена вашим сигнатурам, смещениям и адресам
  4. Документируйте ваши шаблоны - Комментируйте, откуда взялись сигнатуры и на что они указывают
  5. Тестируйте на всех платформах - Убедитесь, что шаблоны для Windows и Linux верны
  6. Держите gamedata в актуальном состоянии - Обновляйте сигнатуры, когда обновления игры их ломают
  7. Используйте verify bytes в патчах - Всегда указывайте verify для предотвращения неправильного применения патчей
  8. Кешируйте адреса - Сохраняйте разрешённые адреса вместо повторного вызова методов Get
  9. Организуйте ваш JSON - Группируйте связанные сигнатуры, смещения и адреса вместе
  10. Версионируйте ваш gamedata - Рассмотрите возможность добавления комментариев с версиями для отслеживания изменений

Устранение неполадок

Сигнатура не найдена

  • Причина: Шаблон не соответствует игровому бинарному файлу
  • Решение: Перегенерируйте сигнатуру, используя IDA/Binary Ninja, проверьте платформозависимые шаблоны

Ошибка проверки патча

  • Причина: Игра была обновлена, и байты изменились
  • Решение: Обновите байты verify и patch для новой версии игры

Возвращён недействительный адрес

  • Причина: Сигнатура не сработала или вычисление смещения неверно
  • Решение: Проверьте работу сигнатуры, проверьте значения offset и read

Файл gamedata не загружается

  • Причина: Файл не найден или недействительный JSON
  • Решение: Проверьте путь к файлу, валидируйте синтаксис JSON

Продвинутые темы

Сигнатуры на основе символов

Для экспортируемых функций используйте префикс @ для ссылки по имени символа:

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

Многоуровневое разрешение адресов

Комбинируйте read со смещениями для следования цепочкам указателей:

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

Это сначала добавляет смещение, затем читает указатель 3 раза (следует 3 разыменованиям указателей).

Адреса с относительным смещением (read_offs32)

Используйте read_offs32 для RIP-относительной адресации (распространено в x64). Это читает 32-битное относительное смещение и вычисляет абсолютный адрес:

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

Это полезно, когда сигнатура указывает на инструкции вроде call [rip + offset] или lea reg, [rip + offset]. Значение read_offs32 - это байтовое смещение, где находится 32-битное относительное смещение.


С этим руководством вы теперь обладаете полными знаниями о системе GameData в s2sdk. Используйте её для создания надёжных, устойчивых к обновлениям плагинов, которые взаимодействуют с внутренними компонентами игры!