Чит на креативный режим в Vintage Story через реверс-инжиниринг

Версия игры

Протестировано на Vintage Story v1.21.6 (Stable), .NET 8.0. Мод использует библиотеку Harmony, которая идёт в комплекте с игрой.

Предупреждение

Это техническая статья для изучения устройства игры. Проверяйте описанные приёмы только в одиночной игре, на своём сервере или с разрешения владельца сервера. Соблюдайте правила сервера.

Что мы делаем

Клиентский мод для Vintage Story. Нажимаем Ctrl+B — клиент переключается в состояние Creative: меняются интерфейс, управление движением и поведение при ломании блоков. Нажимаем снова — обратно в локальный Survival. Серверных прав Creative это не даёт: для сервера игрок может оставаться в Survival.

Здесь используется клиентский мод, а не патчинг бинарников. Ниже — фрагменты, а не полный воспроизводимый проект: файл проекта, ссылки на зависимости и окружающий код приведены не целиком.

Вот как это выглядит на реальном сервере:

TL;DR

Декомпилируем VintagestoryLib.dll и разбираем три Harmony-патча: локальное состояние Creative, входящие обновления режима и исходящие запросы, переписанные в Survival. Локальное состояние не равно серверным правам, а видимый на клиенте эффект не доказывает сохранённое изменение на сервере.


Разбираем систему игровых режимов

Клиент-серверная архитектура

Vintage Story использует модель, в которой сервер определяет права для игровых режимов. Вот обычный поток при попытке сменить режим:

Client                                     Server
  |                                          |
  |  Packet_Client (Id=20)                   |
  |  RequestModeChange {                     |
  |    GameMode=2 (Creative)                 |
  |    FreeMove=1, NoClip=1                  |
  |  }                                       |
  | ---------------------------------------> |
  |                                          |  проверяет привилегии:
  |                                          |  PlayerHasPrivilege("gamemode")
  |                                          |  PlayerHasPrivilege("freemove")
  |                                          |
  |  Packet_Server (Id=46)                   |
  |  ModeChange {                            |
  |    GameMode=1 (Survival)  <- отклонено   |
  |    FreeMove=0, NoClip=0                  |
  |  }                                       |
  | <--------------------------------------- |
  |                                          |
  |  "Not allowed to change game mode"       |

Сервер проверяет привилегии, отклоняет запрос и отправляет обратно исходный режим. Так работает обычная смена режима.

Но вот ключевое наблюдение: игровой режим управляет и клиентским поведением — интерфейсом инвентаря, ломанием блоков, управлением полётом и коллизиями. Эти локальные проверки читают поле режима клиента. Само по себе это ничего не говорит о том, какие действия сервер принимает и сохраняет.

Декомпиляция ключевого класса

Декомпиляция VintagestoryLib.dll с помощью ILSpy раскрывает класс, который хранит локальное состояние режима — ClientWorldPlayerData:

public class ClientWorldPlayerData : IWorldPlayerData
{
    private EnumGameMode gameMode;
    private bool freeMove;
    private bool noClip;

    public EnumGameMode CurrentGameMode
    {
        get => gameMode;
        set => gameMode = value;  // <- обычный сеттер, без побочных эффектов
    }

    public bool FreeMove
    {
        get => freeMove;
        set => freeMove = value;  // <- то же самое, пакет не отправляется
    }

    public bool NoClip
    {
        get => noClip;
        set => noClip = value;    // <- то же самое
    }

    // ЭТОТ метод отправляет пакет на сервер:
    public void RequestMode(ClientMain game, float moveSpeed, float pickRange,
        EnumGameMode gameMode, bool freeMove, bool noClip,
        EnumFreeMovAxisLock freeMovePlaneLock, bool renderMetaBlocks)
    {
        Packet_Client packetClient = new Packet_Client
        {
            Id = 20,
            RequestModeChange = new Packet_PlayerMode
            {
                PlayerUID = PlayerUID,
                GameMode = (int)gameMode,
                FreeMove = freeMove ? 1 : 0,
                NoClip = noClip ? 1 : 0,
                // ...
            }
        };
        game.SendPacketClient(packetClient);
    }

    // Вызывается когда сервер отправляет данные о режиме:
    public void UpdateFromPacket(ClientMain game, Packet_PlayerMode mode)
    {
        gameMode = (EnumGameMode)mode.GameMode;
        freeMove = mode.FreeMove > 0;
        noClip = mode.NoClip > 0;
        // ...
    }

    // Другая перегрузка — тоже перезаписывает gameMode:
    public void UpdateFromPacket(ClientMain game, Packet_PlayerData packet)
    {
        gameMode = (EnumGameMode)packet.GameMode;
        freeMove = packet.FreeMove > 0;
        noClip = packet.NoClip > 0;
        // ...
    }
}
Как работают сеттеры

Сеттеры свойств (CurrentGameMode, FreeMove, NoClip) — это обычные присваивания полям. Установка CurrentGameMode = Creative НЕ вызывает отправку пакета серверу. Это делает только явный вызов метода RequestMode(). Сеттер и сетевой вызов полностью разделены.

Это значит, что можно изменить CurrentGameMode локально без отправки запроса режима из сеттера. Но сервер всё ещё может наблюдать действия, которые из этого следуют.

Три пути обмена данными

Из декомпилированного кода видно ровно три пути данных, которые нужно контролировать:

#НаправлениеЧто происходитНаше действие
1ЛокальноУстанавливаем CurrentGameMode напрямуюПереключаем по Ctrl+B
2ВходящиеСервер отправляет Packet_PlayerMode или Packet_PlayerData, перезаписывая gameModeПерехватываем и форсируем Creative
3ИсходящиеКлиент отправляет RequestModeChange (Id=20) серверуПодменяем на Survival перед отправкой

Здесь разобраны эти три пути. Посмотрим на фрагменты мода.


Фрагменты мода

Структура проекта

BHacks/
├── BHacks.csproj          # .NET 8.0, ссылки на VintagestoryLib + 0Harmony
├── modinfo.json           # side: "client" — критично для мультиплеера
├── BHacksModSystem.cs     # Точка входа, обработчик Ctrl+B
└── Patches/
    └── ModeChangePatches.cs  # Три Harmony prefix-патча
{
  "type": "code",
  "modid": "bhacks",
  "name": "BHacks - Creative Mode Toggle",
  "authors": ["Belz"],
  "description": "Press Ctrl+B to toggle creative mode on the client side.",
  "side": "client",
  "version": "1.0.0",
  "dependencies": {
    "game": ""
  }
}
Почему side: client важен

"side": "client" обозначает клиентский мод. Это относится к загрузке и распространению, а не к правам менять серверное состояние или гарантии работы на каждом сервере.

Точка входа

public class BHacksModSystem : ModSystem
{
    private Harmony patcher;
    private ICoreClientAPI clientApi;

    public static bool CreativeModeEnabled = false;
    public static ILogger Log;

    public override void StartClientSide(ICoreClientAPI api)
    {
        clientApi = api;
        Log = api.Logger;

        // Применяем все Harmony-патчи в категории "bhacks"
        if (!Harmony.HasAnyPatches(Mod.Info.ModID))
        {
            patcher = new Harmony(Mod.Info.ModID);
            patcher.PatchCategory(Mod.Info.ModID);
        }

        // Сырое событие KeyDown — надёжнее системы горячих клавиш
        api.Event.KeyDown += OnKeyDown;
    }
}

Обработчик переключения

Когда пользователь нажимает Ctrl+B, мы напрямую изменяем данные мира игрока — без пакета, без запроса, просто присвоение поля:

private void OnKeyDown(KeyEvent e)
{
    if (e.CtrlPressed && e.KeyCode == (int)GlKeys.B)
    {
        CreativeModeEnabled = !CreativeModeEnabled;

        var player = clientApi.World.Player;
        if (player != null)
        {
            var worldData = player.WorldData;

            if (CreativeModeEnabled)
            {
                worldData.CurrentGameMode = EnumGameMode.Creative;
                worldData.FreeMove = true;
                worldData.NoClip = true;
                player.Entity.Controls.IsFlying = true;
                player.Entity.Controls.NoClip = true;
            }
            else
            {
                worldData.CurrentGameMode = EnumGameMode.Survival;
                worldData.FreeMove = false;
                worldData.NoClip = false;
                player.Entity.Controls.IsFlying = false;
                player.Entity.Controls.NoClip = false;
            }
        }

        e.Handled = true;
    }
}

Клиент теперь использует состояние Creative. Полёт, коллизии и ломание блоков могут измениться локально; визуальное исчезновение блока не доказывает, что сервер принял и сохранил его удаление.


Три Harmony-патча

Патч 1: входящий Packet_PlayerMode

Когда сервер рассылает смену режима (пакет Id 46), HandleModeChange вызывает UpdateFromPacket(ClientMain, Packet_PlayerMode). Это основной путь отката — сервер говорит «ты в Survival», и клиент подчиняется.

Мы перехватываем пакет до того, как он попадёт в метод, и перезаписываем поля:

[HarmonyPrefix()]
[HarmonyPatch(typeof(ClientWorldPlayerData), "UpdateFromPacket",
    new Type[] { typeof(ClientMain), typeof(Packet_PlayerMode) })]
public static void BeforeUpdateFromPacketMode(ClientMain game, Packet_PlayerMode mode)
{
    if (BHacksModSystem.CreativeModeEnabled && mode != null)
    {
        mode.GameMode = 2;   // EnumGameMode.Creative
        mode.FreeMove = 1;
        mode.NoClip = 1;
    }
}

Патч 2: входящий Packet_PlayerData

Вторая перегрузка UpdateFromPacket принимает Packet_PlayerData — более крупный пакет состояния игрока. Это ещё один входящий путь, способный перезаписать локальный режим.

Без этого патча входящее обновление состояния игрока может восстановить локальный режим через отдельный путь кода:

[HarmonyPrefix()]
[HarmonyPatch(typeof(ClientWorldPlayerData), "UpdateFromPacket",
    new Type[] { typeof(ClientMain), typeof(Packet_PlayerData) })]
public static void BeforeUpdateFromPacketData(ClientMain game, Packet_PlayerData packet)
{
    if (BHacksModSystem.CreativeModeEnabled && packet != null)
    {
        packet.GameMode = 2;
        packet.FreeMove = 1;
        packet.NoClip = 1;
    }
}
Почему обе перегрузки?

Обе перегрузки могут обновить локальный режим. Патч только для Packet_PlayerMode не затрагивает путь Packet_PlayerData. Ни один из патчей не меняет состояние режима на сервере.

Патч 3: исходящий RequestModeChange

Клиент периодически отправляет пакеты синхронизации режима — при изменении дальности прорисовки (RequestNewViewDistance вызывает RequestMode внутренне), при изменении настроек рендера или когда внутренние горячие клавиши запускают повторную отправку режима. Каждый раз текущие значения gameMode и freeMove сериализуются в Packet_Client с Id = 20.

Запросы режима по этому пути сообщают Survival, хотя локально клиент использует Creative. Это расхождение полей пакета и локального состояния, а не гарантия от обнаружения или логирования.

Обработка серверных обновлений и исходящих запросов

[HarmonyPrefix()]
[HarmonyPatch(typeof(ClientMain), "SendPacketClient")]
public static void BeforeSendPacketClient(Packet_Client packetClient)
{
    if (packetClient == null) return;

    // Id 20 = RequestModeChange
    if (BHacksModSystem.CreativeModeEnabled
        && packetClient.Id == 20
        && packetClient.RequestModeChange != null)
    {
        packetClient.RequestModeChange.GameMode = 1;  // Survival
        packetClient.RequestModeChange.FreeMove = 0;
        packetClient.RequestModeChange.NoClip = 0;
        packetClient.RequestModeChange.PickingRange =
            CollectibleNet.SerializeFloat(4.5f);       // стандартная дальность Survival
    }
}
Почему переписываем, а не блокируем?

Блокировка всего пакета отбросила бы и другие поля запроса режима. Показанный патч меняет выбранные поля; Survival в запросе не доказывает, что действия приняты или незаметны.


Полный поток пакетов

Вот как все три патча работают вместе:

Client                                     Server
  |                                          |
  |  [Ctrl+B нажат]                          |
  |  CurrentGameMode = Creative              |
  |  FreeMove = true, NoClip = true          |
  |  (пакет не отправлен)                    |
  |                                          |
  |  -- некоторое время спустя --            |
  |                                          |
  |  Клиент пытается отправить:              |
  |  RequestModeChange {                     |
  |    GameMode=2, FreeMove=1                |
  |  }                                       |
  |         |                                |
  |    [Патч 3] подменяет:                   |
  |    GameMode=1, FreeMove=0                |
  |         |                                |
  |  Packet_Client (Id=20)                   |
  |  RequestModeChange {                     |
  |    GameMode=1 (Survival)                 |
  |    FreeMove=0, NoClip=0                  |
  |  }                                       |
  | ---------------------------------------> |
  |                                          |  всё нормально, без ошибок
  |  Packet_Server (Id=46)                   |
  |  ModeChange {                            |
  |    GameMode=1 (Survival)                 |
  |    FreeMove=0, NoClip=0                  |
  |  }                                       |
  | <--------------------------------------- |
  |         |                                |
  |    [Патч 1] подменяет:                   |
  |    GameMode=2, FreeMove=1, NoClip=1      |
  |         |                                |
  |  Клиент обрабатывает:                    |
  |  CurrentGameMode = Creative              |
  |                                          |

Исходящий запрос режима сообщает Survival, а клиент использует Creative. Сервер остаётся авторитетным; эти фрагменты не устанавливают, какие эффекты сохраняются и какие отклоняются другими проверками.


Настройка декомпиляции

Всё это потребовало чтения внутреннего кода игры. Вот подход:

1. Массовая декомпиляция с ILSpy CLI — даёт полный C#-проект с IntelliSense:

dotnet tool install -g ilspycmd
ilspycmd -p -o ./decompiled "%APPDATA%/Vintagestory/VintagestoryLib.dll" \
    -r "%APPDATA%/Vintagestory" \
    -r "%APPDATA%/Vintagestory/Lib"

2. grep для поиска паттернов — быстрее, чем кликать по GUI:

grep -rn "RequestModeChange" ./decompiled/ --include="*.cs"
grep -rn "gameMode\s*=" ./decompiled/ --include="*.cs"

3. Документация Harmony для понимания семантики prefix/postfix: harmony.pardeike.net

Декомпилируйте как проект

Используйте флаг -p в ILSpy CLI. Занимает ~10 секунд и даёт полный .csproj с IntelliSense в вашей IDE. Гораздо быстрее, чем прыгать по GUI-декомпилятору пытаясь проследить цепочки вызовов.


4. Сборка существующего проекта

dotnet build BHacks.sln -c Debug

Результат попадает в BHacks/bin/Debug/Mods/mod/. Упаковываем BHacks.dll + modinfo.json в zip:

Compress-Archive -Path BHacks/bin/Debug/Mods/mod/BHacks.dll,
    BHacks/bin/Debug/Mods/mod/modinfo.json -DestinationPath BHacks.zip

Для полного проекта с нужными сборками игры и зависимостями команды выше иллюстрируют этап сборки. Эти исходные материалы здесь не приведены, поэтому одни фрагменты не дают воспроизводимую сборку.


Что охватывают фрагменты

Фрагменты показывают три Harmony prefix-патча. Они меняют локальное состояние и отдельные поля пакетов, а не серверные права:

Цель патчаНаправлениеНазначение
UpdateFromPacket(Packet_PlayerMode)ВходящийФорсировать Creative в пакетах смены режима
UpdateFromPacket(Packet_PlayerData)ВходящийФорсировать Creative в пакетах полных данных игрока
SendPacketClientИсходящееСообщать Survival в показанных запросах режима

Область действия и ограничения:

  1. Показанные сеттеры меняют локальное состояние без запроса режима.
  2. Патчи меняют отдельные входящие обновления и исходящие запросы.
  3. Визуальный эффект, принятое сервером действие и сохранённое изменение мира — разные результаты; последние два этими фрагментами не установлены.
  4. Harmony даёт доступ ко всему. Даже private методы в основной сборке игры можно перехватить с помощью prefix-хуков — не нужно модифицировать файлы на диске. Схема пакетов условна: её утверждения о том, что считает каждая сторона, не доказывают незаметность или серверные права Creative.

Сервер валидирует запросы смены режима, а показанные клиентские сеттеры меняют локальное состояние без запроса. Это объясняет локальное поведение, но не доказывает полноценные серверные права Creative или одинаковый результат на разных серверах.

Читайте также

Другую границу между клиентом и сервером разбирает статья о телепортации: она посвящена обновлениям позиции, а не запросам режима Creative.

Обе статьи собраны в разделе темы Vintage Story.

Сообщить об ошибке

Нашли ошибку? Напишите мне в LinkedIn: приложите ссылку на страницу и укажите, что нужно исправить.