Modding Support

The free version of Charon can be distributed together with your shipped game, letting players edit game data with the same editor your team used to build it. Combined with patch files, this enables a full modding pipeline: players edit a copy of the game data, changes are captured as a small patch, and the game loads the base data with any number of patches applied on top.


Overview

A typical modding setup consists of three parts:

  1. The base game data file (e.g. gamedata.json), shipped with the game or provided in a separate mod kit.

  2. The Charon editor, launched locally by the player (or by your game) against a copy of that file, with schema editing disabled.

  3. Patch files describing the mod’s changes relative to the base data, loaded by the game at startup through the generated Load() code.


Shipping the Editor with Your Game

Charon is a .NET tool and requires the .NET 8 Runtime or later on the player’s machine. You can verify what is installed with:

dotnet --list-runtimes

If the runtime is missing, direct the player to the .NET download page. Some distribution platforms (e.g. Steam) can install the .NET Runtime automatically as a game dependency.

There are two ways to run the editor:

On-demand via the .NET SDK (simplest, requires .NET 10 SDK):

dnx dotnet-charon -- SERVER START --dataBase "./gamedata.json" --denySchemaEditing --launchDefaultBrowser

A bundled copy of a specific version (full control, no SDK required):

Download the dotnet-charon package from nuget.org (a .nupkg file is a ZIP archive) and ship the contents of tools/net8.0/any/ with your game, then launch it via the runtime:

dotnet "<game folder>/Charon/Charon.dll" SERVER START \
  --dataBase "./mods/gamedata_copy.json" \
  --port 8080 \
  --denySchemaEditing \
  --launchDefaultBrowser

The --denySchemaEditing flag is important for modding scenarios: it locks the data structure (schemas), so mods can change values and documents but cannot break the shape of the data your compiled game expects. See SERVER START for all flags.


Capturing Mod Changes as a Patch

Instead of shipping a whole modified game data file, a mod can be a small patch file - the difference between the base data and the modified copy. Creating patches is currently a CLI-only operation - there is no UI action for it:

charon DATA CREATEPATCH \
  --dataBase1 "./gamedata.json" \
  --dataBase2 "./mods/gamedata_copy.json" \
  --output "./mods/my_mod.gdpatch" \
  --outputFormat json

Patch files are small, composable, and independent of each other as long as they touch different documents. See Patch and Diff Workflow for the format details and conflict behavior.


Loading Mods at Runtime

The generated source code loads patches alongside the base game data - pass patch streams in the load options. Patches are applied in the order given, and each patch is merged into the data following the default merge rules. Patching during load is supported by all Tier 1 code generators: C# 4.0, C# 7.3, TypeScript, and Unreal Engine C++ (see Generating Source Code).

C# example:

var gameData = new GameData(gameDataStream, new Formatters.GameDataLoadOptions {
    Format = Formatters.Format.Json,
    Patches = new[] { modPatchStream1, modPatchStream2 }
});

A simple mod-loading convention: scan a mods/ folder for patch files at startup and pass them all to Load() in a deterministic order (e.g. alphabetical, or driven by a mod manifest).


How Patches Are Merged at Load Time

Runtime patching follows the same deterministic principles as the editor-side document merge logic. The core rules, in short:

  • If the patch provides a value for a property → the result uses the patch value.

  • If the patch does not provide a value (the property key is absent) → the original value is kept.

  • A property present with an explicit null is an intentional deletion, not a missing value.

  • The rules apply recursively: embedded documents are merged property-by-property, and collections are merged item-by-item using the document’s Id as the key.

On top of that, for whole documents inside a patch, the presence of the Id field decides how the entry is applied - the Id value is irrelevant:

No Id field - a partial update. The entry’s fields are merged into the existing document. A partial update cannot resurrect a deleted document - if the document was removed by an earlier patch, it stays deleted:

original     patch 1    patch 2     patch 3     result
{ Id: 1 }  →  null   →  { ... }  →  { ... }  =  null

Has an Id field - a full replacement. The document is replaced entirely. A replacement can also resurrect a previously deleted document:

original     patch 1    patch 2       patch 3       result
{ Id: 1 }  →  null   →  { Id: 2 }  →  { Id: 3 }  =  { Id: 3 }

See Document Merge Logic for the full property-level rules (LocalizedText per-language merging, collection modes, and validation caveats when a patch recreates a document with missing required fields).


See also