Vintage Story creative mode cheat via reverse engineering
Table of Contents
Reversing the Vintage Story client to inspect local Creative state, Harmony patches, and the boundary between client behavior and server-authoritative Survival. Tested on Vintage Story v1.21.6 (Stable), .NET 8.0. The mod uses the Harmony library bundled with the game. This is a technical write-up for educational purposes. Test only in singleplayer, on your own server, or with the server owner’s permission. Follow the server’s rules. A client-side Vintage Story mod. Press Ctrl+B — the client switches to Creative state, changing its UI, movement controls, and block-breaking behavior. Press again — back to local Survival. This does not grant server-side Creative privileges: the server can still treat the player as Survival. The approach uses a client mod rather than binary patching. The snippets below are fragments, not a complete reproducible project: the project file, dependency references, and surrounding code are not all included. Here’s what it looks like on a real server: We decompile Vintage Story uses a classic authoritative server model for game modes. Here’s the normal flow when you try to change mode: The server checks privileges, rejects the request, and sends back your original mode. This is the normal behavior. The relevant distinction is that game mode controls client-side behavior too — the inventory UI, block-breaking behavior, fly controls, and collision detection. These local checks read the client’s mode field. That alone says nothing about which actions the server accepts or persists. Decompiling The property setters ( This means we can change From the decompiled code, there are exactly three data paths we need to control: Those are the three paths covered here. Let’s look at the mod fragments. When the user presses Ctrl+B, we directly modify the player’s world data — no packet, no request, just a field assignment: The client now uses Creative state. Flight, collision, and block-breaking behavior may change locally; visually removing a block does not establish that the server accepted or saved its removal. When the server broadcasts a mode change (packet Id 46), We intercept the packet before it reaches the method and overwrite the fields: A second overload of Without this patch, an incoming player-state update can restore the local mode through this separate code path: Both overloads can update the local mode. Patching only The client periodically sends mode sync packets — when view distance changes ( Mode requests sent through this path report Survival even while the local client uses Creative. That is a packet-field mismatch, not a guarantee against detection or logging. Blocking the whole packet would also drop unrelated mode-request fields. The shown patch changes selected fields; reporting Survival is not proof that actions are accepted or unobserved. Here’s how all three patches work together: The outgoing mode request reports Survival while the client uses Creative. The server remains authoritative; these fragments do not establish which gameplay effects persist or whether other checks reject them. All of this required reading the game’s internal code. Here’s the approach: 1. Bulk decompilation with ILSpy CLI — produces a full C# project with IntelliSense: 2. grep for finding patterns — faster than clicking around in a GUI: 3. Harmony docs for understanding prefix/postfix semantics: harmony.pardeike.net Use the The output lands in For a complete project with the required game assemblies and dependencies, the commands above illustrate the build step. Those project inputs are not included here, so the fragments alone are not a reproducible build. The fragments show three Harmony prefix patches. They change local state and selected packet fields, not server-side privileges: Scope and limitations: The server validates mode change requests, while the shown client setters can change local state without making a request. That split explains the local behavior. It does not establish full server-side Creative privileges or consistent results across servers. For another client/server boundary, read the teleport analysis: it follows position updates rather than Creative mode requests. Browse both articles under Vintage Story topics. Found an error? Contact me on LinkedIn with the page link and what needs correcting.What we’re building
VintagestoryLib.dll and inspect three Harmony patches: local Creative state, incoming mode updates, and outgoing mode requests rewritten as Survival. Local state is not server authorization, and a client-visible effect does not prove a persisted server change.Reversing the game mode system
Client-server architecture
Client Server
| |
| Packet_Client (Id=20) |
| RequestModeChange { |
| GameMode=2 (Creative) |
| FreeMove=1, NoClip=1 |
| } |
| ---------------------------------------> |
| | checks privileges:
| | PlayerHasPrivilege("gamemode")
| | PlayerHasPrivilege("freemove")
| |
| Packet_Server (Id=46) |
| ModeChange { |
| GameMode=1 (Survival) <- rejected |
| FreeMove=0, NoClip=0 |
| } |
| <--------------------------------------- |
| |
| "Not allowed to change game mode" |Decompiling the key class
VintagestoryLib.dll with ILSpy reveals the class that stores the local mode state — ClientWorldPlayerData:public class ClientWorldPlayerData : IWorldPlayerData
{
private EnumGameMode gameMode;
private bool freeMove;
private bool noClip;
public EnumGameMode CurrentGameMode
{
get => gameMode;
set => gameMode = value; // ← plain field setter, no side effects
}
public bool FreeMove
{
get => freeMove;
set => freeMove = value; // ← same, no packet sent
}
public bool NoClip
{
get => noClip;
set => noClip = value; // ← same
}
// THIS method sends a packet to the server:
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);
}
// Called when server sends mode data:
public void UpdateFromPacket(ClientMain game, Packet_PlayerMode mode)
{
gameMode = (EnumGameMode)mode.GameMode;
freeMove = mode.FreeMove > 0;
noClip = mode.NoClip > 0;
// ...
}
// Another overload — also overwrites gameMode:
public void UpdateFromPacket(ClientMain game, Packet_PlayerData packet)
{
gameMode = (EnumGameMode)packet.GameMode;
freeMove = packet.FreeMove > 0;
noClip = packet.NoClip > 0;
// ...
}
}CurrentGameMode, FreeMove, NoClip) are plain field assignments. Setting CurrentGameMode = Creative does NOT trigger a server packet. Only the explicit RequestMode() method does. The setter and the network call are completely decoupled.CurrentGameMode locally without the setter sending a mode request. It does not mean the server cannot observe the resulting actions.Three data paths
# Direction What happens Our action 1 Local We set CurrentGameMode directlyToggle on Ctrl+B 2 Incoming Server sends Packet_PlayerMode or Packet_PlayerData that overwrites gameModeIntercept and force Creative 3 Outgoing Client sends RequestModeChange (Id=20) to serverRewrite to Survival before sending Mod fragments
Project structure
BHacks/
├── BHacks.csproj # .NET 8.0, references VintagestoryLib + 0Harmony
├── modinfo.json # side: "client" — critical for multiplayer
├── BHacksModSystem.cs # Entry point, Ctrl+B handler
└── Patches/
└── ModeChangePatches.cs # Three Harmony prefix patches{
"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" declares a client-only mod. That concerns loading and distribution, not permission to change server state or a guarantee that it works on every server.Entry point
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;
// Apply all Harmony patches in the "bhacks" category
if (!Harmony.HasAnyPatches(Mod.Info.ModID))
{
patcher = new Harmony(Mod.Info.ModID);
patcher.PatchCategory(Mod.Info.ModID);
}
// Raw KeyDown event — more reliable than the hotkey system
api.Event.KeyDown += OnKeyDown;
}
}Toggle handler
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;
}
}The three Harmony patches
Patch 1: incoming
Packet_PlayerModeHandleModeChange calls UpdateFromPacket(ClientMain, Packet_PlayerMode). This is the primary revert path — the server says “you’re in Survival” and the client obeys.[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;
}
}Patch 2: incoming
Packet_PlayerDataUpdateFromPacket accepts Packet_PlayerData, a larger player-state packet. It is another incoming path that can overwrite local mode state.[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 leaves the Packet_PlayerData path unchanged. Neither patch changes the server’s own mode state.Patch 3: outgoing
RequestModeChangeRequestNewViewDistance calls RequestMode internally), when render settings change, or when internal hotkeys trigger a mode resend. Each time, the current gameMode and freeMove values are serialized into Packet_Client with Id = 20.Handling server updates and outgoing requests
[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); // default survival range
}
}The complete packet flow
Client Server
| |
| [Ctrl+B pressed] |
| CurrentGameMode = Creative |
| FreeMove = true, NoClip = true |
| (no packet sent) |
| |
| -- some time later -- |
| |
| Client tries to send: |
| RequestModeChange { |
| GameMode=2, FreeMove=1 |
| } |
| | |
| [Patch 3] rewrites: |
| GameMode=1, FreeMove=0 |
| | |
| Packet_Client (Id=20) |
| RequestModeChange { |
| GameMode=1 (Survival) |
| FreeMove=0, NoClip=0 |
| } |
| ---------------------------------------> |
| | looks normal, no errors
| Packet_Server (Id=46) |
| ModeChange { |
| GameMode=1 (Survival) |
| FreeMove=0, NoClip=0 |
| } |
| <--------------------------------------- |
| | |
| [Patch 1] rewrites: |
| GameMode=2, FreeMove=1, NoClip=1 |
| | |
| Client processes: |
| CurrentGameMode = Creative |
| |Decompilation setup
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 flag with ILSpy CLI. It takes ~10 seconds and gives you a full .csproj with IntelliSense in your IDE. Much faster than jumping around in a GUI decompiler trying to follow call chains.4. Compiling an existing project
dotnet build BHacks.sln -c DebugBHacks/bin/Debug/Mods/mod/. Package BHacks.dll + modinfo.json into a zip:Compress-Archive -Path BHacks/bin/Debug/Mods/mod/BHacks.dll,
BHacks/bin/Debug/Mods/mod/modinfo.json -DestinationPath BHacks.zipWhat the fragments cover
Patch target Direction Purpose UpdateFromPacket(Packet_PlayerMode)Incoming Force Creative in mode change packets UpdateFromPacket(Packet_PlayerData)Incoming Force Creative in full player data packets SendPacketClientOutgoing Report Survival in the shown mode requests private methods in the game’s core assembly can be intercepted with prefix hooks — no need to modify any files on disk. The packet diagram is schematic: its claims about what either side believes are not evidence of undetectability or server-side Creative privileges.Related reading
Report an error