Чит на креативный режим в Vintage Story через реверс-инжиниринг
Содержание
Реверсим клиент Vintage Story: локальное состояние Creative, Harmony-патчи и граница между поведением клиента и серверным Survival. Протестировано на Vintage Story v1.21.6 (Stable), .NET 8.0. Мод использует библиотеку Harmony, которая идёт в комплекте с игрой. Это техническая статья для изучения устройства игры. Проверяйте описанные приёмы только в одиночной игре, на своём сервере или с разрешения владельца сервера. Соблюдайте правила сервера. Клиентский мод для Vintage Story. Нажимаем Ctrl+B — клиент переключается в состояние Creative: меняются интерфейс, управление движением и поведение при ломании блоков. Нажимаем снова — обратно в локальный Survival. Серверных прав Creative это не даёт: для сервера игрок может оставаться в Survival. Здесь используется клиентский мод, а не патчинг бинарников. Ниже — фрагменты, а не полный воспроизводимый проект: файл проекта, ссылки на зависимости и окружающий код приведены не целиком. Вот как это выглядит на реальном сервере: Декомпилируем Vintage Story использует модель, в которой сервер определяет права для игровых режимов. Вот обычный поток при попытке сменить режим: Сервер проверяет привилегии, отклоняет запрос и отправляет обратно исходный режим. Так работает обычная смена режима. Но вот ключевое наблюдение: игровой режим управляет и клиентским поведением — интерфейсом инвентаря, ломанием блоков, управлением полётом и коллизиями. Эти локальные проверки читают поле режима клиента. Само по себе это ничего не говорит о том, какие действия сервер принимает и сохраняет. Декомпиляция Сеттеры свойств ( Это значит, что можно изменить Из декомпилированного кода видно ровно три пути данных, которые нужно контролировать: Здесь разобраны эти три пути. Посмотрим на фрагменты мода. Когда пользователь нажимает Ctrl+B, мы напрямую изменяем данные мира игрока — без пакета, без запроса, просто присвоение поля: Клиент теперь использует состояние Creative. Полёт, коллизии и ломание блоков могут измениться локально; визуальное исчезновение блока не доказывает, что сервер принял и сохранил его удаление. Когда сервер рассылает смену режима (пакет Id 46), Мы перехватываем пакет до того, как он попадёт в метод, и перезаписываем поля: Вторая перегрузка Без этого патча входящее обновление состояния игрока может восстановить локальный режим через отдельный путь кода: Обе перегрузки могут обновить локальный режим. Патч только для Клиент периодически отправляет пакеты синхронизации режима — при изменении дальности прорисовки ( Запросы режима по этому пути сообщают Survival, хотя локально клиент использует Creative. Это расхождение полей пакета и локального состояния, а не гарантия от обнаружения или логирования. Блокировка всего пакета отбросила бы и другие поля запроса режима. Показанный патч меняет выбранные поля; Survival в запросе не доказывает, что действия приняты или незаметны. Вот как все три патча работают вместе: Исходящий запрос режима сообщает Survival, а клиент использует Creative. Сервер остаётся авторитетным; эти фрагменты не устанавливают, какие эффекты сохраняются и какие отклоняются другими проверками. Всё это потребовало чтения внутреннего кода игры. Вот подход: 1. Массовая декомпиляция с ILSpy CLI — даёт полный C#-проект с IntelliSense: 2. grep для поиска паттернов — быстрее, чем кликать по GUI: 3. Документация Harmony для понимания семантики prefix/postfix: harmony.pardeike.net Используйте флаг Результат попадает в Для полного проекта с нужными сборками игры и зависимостями команды выше иллюстрируют этап сборки. Эти исходные материалы здесь не приведены, поэтому одни фрагменты не дают воспроизводимую сборку. Фрагменты показывают три Harmony prefix-патча. Они меняют локальное состояние и отдельные поля пакетов, а не серверные права: Область действия и ограничения: Сервер валидирует запросы смены режима, а показанные клиентские сеттеры меняют локальное состояние без запроса. Это объясняет локальное поведение, но не доказывает полноценные серверные права Creative или одинаковый результат на разных серверах. Другую границу между клиентом и сервером разбирает статья о телепортации: она посвящена обновлениям позиции, а не запросам режима Creative. Обе статьи собраны в разделе темы Vintage Story. Нашли ошибку? Напишите мне в LinkedIn: приложите ссылку на страницу и укажите, что нужно исправить.Что мы делаем
VintagestoryLib.dll и разбираем три Harmony-патча: локальное состояние Creative, входящие обновления режима и исходящие запросы, переписанные в Survival. Локальное состояние не равно серверным правам, а видимый на клиенте эффект не доказывает сохранённое изменение на сервере.Разбираем систему игровых режимов
Клиент-серверная архитектура
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" обозначает клиентский мод. Это относится к загрузке и распространению, а не к правам менять серверное состояние или гарантии работы на каждом сервере.Точка входа
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;
}
}Обработчик переключения
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;
}
}Три Harmony-патча
Патч 1: входящий
Packet_PlayerModeHandleModeChange вызывает 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_PlayerDataUpdateFromPacket принимает 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: исходящий
RequestModeChangeRequestNewViewDistance вызывает RequestMode внутренне), при изменении настроек рендера или когда внутренние горячие клавиши запускают повторную отправку режима. Каждый раз текущие значения gameMode и freeMove сериализуются в Packet_Client с Id = 20.Обработка серверных обновлений и исходящих запросов
[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
}
}Полный поток пакетов
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 |
| |Настройка декомпиляции
dotnet tool install -g ilspycmd
ilspycmd -p -o ./decompiled "%APPDATA%/Vintagestory/VintagestoryLib.dll" \
-r "%APPDATA%/Vintagestory" \
-r "%APPDATA%/Vintagestory/Lib"grep -rn "RequestModeChange" ./decompiled/ --include="*.cs"
grep -rn "gameMode\s*=" ./decompiled/ --include="*.cs"-p в ILSpy CLI. Занимает ~10 секунд и даёт полный .csproj с IntelliSense в вашей IDE. Гораздо быстрее, чем прыгать по GUI-декомпилятору пытаясь проследить цепочки вызовов.4. Сборка существующего проекта
dotnet build BHacks.sln -c DebugBHacks/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Что охватывают фрагменты
Цель патча Направление Назначение UpdateFromPacket(Packet_PlayerMode)Входящий Форсировать Creative в пакетах смены режима UpdateFromPacket(Packet_PlayerData)Входящий Форсировать Creative в пакетах полных данных игрока SendPacketClientИсходящее Сообщать Survival в показанных запросах режима private методы в основной сборке игры можно перехватить с помощью prefix-хуков — не нужно модифицировать файлы на диске. Схема пакетов условна: её утверждения о том, что считает каждая сторона, не доказывают незаметность или серверные права Creative.Читайте также
Сообщить об ошибке