Чит на креативний режим у 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.Читайте також
Повідомити про помилку