Vintage Story creative mode cheat via reverse engineering

Game version

Tested on Vintage Story v1.21.6 (Stable), .NET 8.0. The mod uses the Harmony library bundled with the game.

Disclaimer

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.

What we’re building

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:

TL;DR

We decompile 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

Vintage Story uses a classic authoritative server model for game modes. Here’s the normal flow when you try to change mode:

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

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 key class

Decompiling 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;
        // ...
    }
}
How the setters work

The property setters (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.

This means we can change CurrentGameMode locally without the setter sending a mode request. It does not mean the server cannot observe the resulting actions.

Three data paths

From the decompiled code, there are exactly three data paths we need to control:

#DirectionWhat happensOur action
1LocalWe set CurrentGameMode directlyToggle on Ctrl+B
2IncomingServer sends Packet_PlayerMode or Packet_PlayerData that overwrites gameModeIntercept and force Creative
3OutgoingClient sends RequestModeChange (Id=20) to serverRewrite to Survival before sending

Those are the three paths covered here. Let’s look at the mod fragments.


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": ""
  }
}
Why side: client matters

"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

When the user presses Ctrl+B, we directly modify the player’s world data — no packet, no request, just a field assignment:

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 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.


The three Harmony patches

Patch 1: incoming Packet_PlayerMode

When the server broadcasts a mode change (packet Id 46), HandleModeChange calls UpdateFromPacket(ClientMain, Packet_PlayerMode). This is the primary revert path — the server says “you’re in Survival” and the client obeys.

We intercept the packet before it reaches the method and overwrite the fields:

[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_PlayerData

A second overload of UpdateFromPacket accepts Packet_PlayerData, a larger player-state packet. It is another incoming path that can overwrite local mode state.

Without this patch, an incoming player-state update can restore the local mode through this separate code path:

[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;
    }
}
Why both overloads?

Both overloads can update the local mode. Patching only Packet_PlayerMode leaves the Packet_PlayerData path unchanged. Neither patch changes the server’s own mode state.

Patch 3: outgoing RequestModeChange

The client periodically sends mode sync packets — when view distance changes (RequestNewViewDistance 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.

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.

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
    }
}
Why rewrite instead of block?

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.


The complete packet flow

Here’s how all three patches work together:

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

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.


Decompilation setup

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:

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

2. grep for finding patterns — faster than clicking around in a GUI:

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

3. Harmony docs for understanding prefix/postfix semantics: harmony.pardeike.net

Decompile as a project

Use the -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 Debug

The output lands in BHacks/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.zip

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.


What the fragments cover

The fragments show three Harmony prefix patches. They change local state and selected packet fields, not server-side privileges:

Patch targetDirectionPurpose
UpdateFromPacket(Packet_PlayerMode)IncomingForce Creative in mode change packets
UpdateFromPacket(Packet_PlayerData)IncomingForce Creative in full player data packets
SendPacketClientOutgoingReport Survival in the shown mode requests

Scope and limitations:

  1. The shown setters change local state without sending a mode request.
  2. The patches alter selected incoming updates and outgoing requests.
  3. Visual effects, accepted server actions, and persisted world changes are different outcomes; these excerpts do not establish the latter two.
  4. Harmony gives us access to everything. Even 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.

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.

Report an error

Found an error? Contact me on LinkedIn with the page link and what needs correcting.