The C# SDK is the runtime library through which C# applications load and access config data exported by Archmage.
Archmage is a configuration solution for game development: specifications for how to structure config data, define fields, and fill in each value; pipelines that export runtime data and generate strongly-typed code; multi-language SDKs for loading and accessing that data at runtime; and a collaborative editing workflow for teams.
The SDK is built around the concept of an Atlas — a registry that maps named keys to configurations. Each key is associated with one or more JSON files. At runtime, the SDK reads these files, deserializes them into instances of generated C# types, resolves cross-table references, and calls post-load hooks.
Key features
- I18n — multi-language text management with automatic fallback
- XRef — cross-table reference resolution via
IAtlas.BindRefs - Duration — nanosecond precision; formats as human-readable strings such as
1s200ms - MinMax — random value selection within a range
- WeightedPool — weighted random selection with probability proportional to item weight
- Variants — switch an atlas item to use an alternative data set at load time via
WithVariant - Whitelist/Blacklist — load only a subset of atlas items
- Layered overrides — merge files with matching relative paths from additional override sources (a directory path or a custom file system) into the base configs, field by field, at load time
- Synchronous and asynchronous loading — progress reporting, cancellation, and parallel deserialization
- Pluggable file system — load from embedded resources, in-memory data, or any other source via
IFS - Versioning — VCS metadata (branch, commit, timestamp, etc.), when present in
atlas.json, is available on the loaded atlas - Unity support — built-in adapters for Addressables, Resources, and
StreamingAssets; Inspector dropdowns for config ID fields, populated from the loaded atlas, for easy selection - Godot support — a file system adapter based on
FileAccess, and handy utilities
- Unity 6000.3 or later
- com.unity.nuget.newtonsoft-json 3.2.2
- Godot 4.6 or later, .NET edition
- Newtonsoft.Json 13.0.3
- net8.0, netstandard2.1 or later
- Newtonsoft.Json 13.0.3
Via GitHub — In the Package Manager window, click + → Add package from git URL, and enter:
https://github.com/shadowopera/sdk-cs.git?path=unity/dev.shadop.archmage
Or via OpenUPM:
openupm add dev.shadop.archmageNote
Unity Signature Warning: Unity may display a "Missing Signature" warning. This is expected for OpenUPM packages. Archmage is safe to use — the warning does not affect functionality. Simply proceed with your development as usual.
If your project uses .asmdef files, add the following assembly references:
Shadop.Archmage.SdkShadop.Archmage.Sdk.UnityShadop.Archmage.Sdk.Unity.Addressables(optional, only if using Addressables)Shadop.Archmage.Sdk.Unity.Editor(optional)
dotnet add package Shadop.Archmage.Godotdotnet add package Shadop.ArchmageA complete working example is in ConfLoader.cs, covering Addressables, Resources, and StreamingAssets — with sync/async variants, concurrent loading, and I18n setup. To show config ID fields as dropdowns in the Inspector, see ArchmageEditorTools.
The recommended starting point:
using Shadop.Archmage.Sdk;
// ConfigAtlas is generated by Archmage
var atlas = new ConfigAtlas();
var options = new AtlasOptions()
.WithLogger(new UnityAtlasLogger())
.WithJsonSettings(UnityJsonSettingsFactory.Create())
.WithFS(new UnityAddressablesFS());
await Archmage.LoadAtlasAsync(
"Assets/Configs/atlas.json", "Assets/Configs", atlas, options);A complete working example is in ConfLoader.cs, covering asynchronous loading on a worker thread and I18n setup. To show config ID properties as dropdowns in the Inspector, see ArchmageEditorPlugin.
The recommended starting point:
using Shadop.Archmage.Sdk;
// ConfigAtlas is generated by Archmage
var atlas = new ConfigAtlas();
var options = new AtlasOptions()
.WithLogger(new GodotAtlasLogger())
.WithJsonSettings(GodotJsonSettingsFactory.Create())
.WithFS(new GodotFileAccessFS());
await Archmage.LoadAtlasAsync(
"res://configs/atlas.json", "res://configs", atlas, options, workerThreadLoading: true);GodotFileAccessFS reads res:// paths, user:// paths and operating system paths. If you mount resource packs with
ProjectSettings.LoadResourcePack, mount them before loading starts.
using Shadop.Archmage.Sdk;
// ConfigAtlas is generated by Archmage
var atlas = new ConfigAtlas();
Archmage.LoadAtlas("configs/atlas.json", "configs/", atlas);atlas.json is generated by Archmage. It declares how each config key maps to its JSON files using one of three strategies:
| Strategy | Shape | Behavior |
|---|---|---|
| unique | key → "file.json" |
Deserializes one file into the config object |
| variant | key → { "/": "file.json", "alt": "file-alt.json" } |
Selects one variant by case; "/" is the default |
| many | key → ["a.json", "b.json"] |
Deserializes and merges multiple files in order |
Example atlas.json:
{
"unique": {
"hero": "hero.json",
"item": "clutter/item.json"
},
"variant": {
"game": { "/": "game.json", "hard": "game_hard.json" }
},
"many": {
"weapon": [ "vtbl/weapon-sword.json", "vtbl/weapon-staff.json" ]
}
}Loading proceeds in the following steps:
- Parse
atlas.json - Apply
AtlasModifier(if set) - For each atlas item: read files → deserialize → apply overrides
BindRefs()— resolve cross-table referencesOnLoaded()— post-load initialization
Do not block on the task returned by LoadAtlasAsync (for example with .Result or .Wait()) on a thread that
has a synchronization context, such as the Unity main thread; it deadlocks. Use LoadAtlas to load synchronously.
Configure loading via the fluent AtlasOptions builder:
var opts = new AtlasOptions()
// custom logger (default: stderr; silent in Unity)
.WithLogger(myLogger)
// replace the default filesystem (System.IO)
.WithFS(myFS)
// load only these keys
.WithWhitelist(new[] { "hero", "item" })
// skip these keys
.WithBlacklist(new[] { "debug" })
// select a variant
.WithVariant("game", "hard")
// add an override directory
.WithOverrideRoot("configs/override/")
// add an override filesystem
.WithOverrideFS(embeddedFS)
// mutate atlas.json after parsing
.WithAtlasModifier(atlasJson => { ... })
// load at most 8 atlas items at the same time (default: 32)
.WithMaxConcurrency(8)
// custom Newtonsoft.Json settings
.WithJsonSettings(customSettings);Whitelist and blacklist — If a non-empty whitelist is set, only listed keys are loaded (blacklist is ignored). All keys must exist in the atlas or an exception is thrown.
Variant selection — A variant-mapped key loads its "/" variant unless WithVariant
selects another one. The variant in use is recorded in AtlasItem.Variant.
Override layers — Each WithOverrideRoot / WithOverrideFS call adds another
override source. When loading an atlas item, each override source is checked in the order they
were added; any matching file is deserialized and its fields applied on top of the base
data. This is useful for environment-specific patches.
Field-level merge rules during override processing:
| Value in override | Behavior |
|---|---|
null |
Resets the target field to its default value or raises an exception |
| JSON object | Recursively merges — only fields present in the override are updated, others remain unchanged |
| Any other value | Overwrites the field |
Concurrency limit — WithMaxConcurrency limits how many atlas items are loaded at the same time.
Both WithFS and WithOverrideFS accept an IFS implementation. The default
filesystem reads from System.IO. You can supply a custom IFS to replace it or
to use as an override source — for example to load from embedded resources or
an in-memory dictionary:
class EmbeddedFS : IFS
{
public bool MainThreadOnly => false;
public bool DirectoryExists(string path) => true;
public bool FileExists(string path) => /* check assembly resources */;
public byte[] ReadAllBytes(string path) => /* load from resources */;
public Task<byte[]> ReadAllBytesAsync(string path, CancellationToken ct) => /* async load */;
}
var opts = new AtlasOptions().WithFS(new EmbeddedFS());Notes for IFS implementations:
MainThreadOnlyreturnstrueif the methods work only on the main thread, as with Unity's file systems. Otherwise, it returnsfalse.FileExistsandDirectoryExistsmay return true without checking when an exact check is expensive.ReadAllBytesandReadAllBytesAsyncmust throwFileNotFoundExceptionwhen the file does not exist. Missing override files are skipped this way.- If
MainThreadOnlyistruefor the main IFS or for any override IFS, loading must start on the main thread.LoadAtlasandLoadAtlasAsynccall the methods of every IFS on the calling thread. - Otherwise, if
MainThreadOnlyisfalsefor every IFS,LoadAtlasandLoadAtlasAsyncread the files of atlas items withReadAllBytesand may call the methods on thread pool threads.
I18n holds per-language translations and falls back to a default language when a key is missing.
var i18n = new I18n(fallbackLanguage: "en");
i18n.MergeL10nFile("l10n/en.json", "en");
i18n.MergeL10nFile("l10n/zh-CN.json", "zh-CN");
i18n.Text("ui.ok", "zh-CN"); // → "确认"
i18n.Text("ui.ok", "ja"); // → falls back to "OK" in "en"
i18n.Text("ui.xx", "ja"); // → no translation found, so returns "ui.xx"In generated config classes, localized fields are typed as L10n. In JSON they are
represented as strings (e.g., "ui.ok"); accessing .Text on an L10n field looks up
that key in a shared I18n instance. Set L10n.GetI18n and L10n.GetPreferredLanguage
to configure the lookup before use.
L10n.GetI18n = () => i18n;
L10n.GetPreferredLanguage = () => "zh-CN";
// Then in your code:
string label = hero.Name.Text;To detect a missing translation, use GetText, which returns a bool.
XRef<V, T> pairs a config ID (CfgId) with a resolved reference (Ref) set during BindRefs.
// In generated config class:
public XRef<HeroCfgId, HeroCfg> Boss { get; set; }
// After loading:
var boss = atlas.HeroTable[1].Boss.Ref; // resolved objectA nanosecond-precision duration type. It serializes as a compact integer array in JSON (e.g., [0, 5] = 5 seconds).
Duration d = Duration.Second * 90 + Duration.Millisecond * 500;
d.ToString(); // "1m30s500ms"
d.Seconds(); // 90.5
d.Milliseconds(); // 90500
d.ToTimeSpan(); // TimeSpanArithmetic operators (+, -, *, /, %) and comparisons are supported.
A color type with R, G, B, A byte channels. .ToColor() converts it to
UnityEngine.Color in Unity and to Godot.Color in Godot.
var color = Rgba.Parse("#FF8000"); // R=255, G=128, B=0, A=255
color.ToString(); // "#FF8000"MinMax<T> is a range bounded by Min and Max. The Sample extension methods draw a random value from the range. T may be any integer type, float, double, or Duration.
WeightedPool<T> holds parallel Items and Weights arrays. The Sample / SampleIndex
extension methods draw an item (or its index) at random with probability proportional to its
weight.
To change weights at runtime, call Clone and change the copy.
Vec2<T>, Vec3<T>, Vec4<T> are typed vectors. Fields are accessed as .X, .Y, .Z, .W.
Tup1–Tup7 are heterogeneous tuples. They serialize as JSON objects with keys
item0, item1, etc. (0-based). Fields are accessed as .Item0, .Item1, etc.,
and deconstruction is supported.
atlas.json can carry a version block with VCS metadata (branch, commit ID, timestamp, author). After loading, it is available on the atlas:
{
"version": {
"branch": "main",
"id": "a1b2c3d4e5f6...",
"shortId": "a1b2c3d",
"timestamp": "2025-01-01T00:00:00Z"
},
...
}var ver = atlas.DataVersion; // VersionInfo?, null if not present
ver?.Branch // "main"
ver?.ShortID // "a1b2c3d"- Documentation: https://docs.shadop.dev/archmage/overview-cs/sdk-cs/
- Source Code: https://github.com/shadowopera/sdk-cs
sdk-cs/
├── src/Archmage/
│ ├── Sdk/ # C# runtime source (canonical)
│ │ ├── Godot/ # Godot adapters and the Shadop.Archmage.Godot project
│ │ └── Unity/ # Unity adapters (FS, logger, JSON settings, type extensions)
│ │ └── Addressables/ # Addressables FS adapter
│ └── Editor/Unity/ # Inspector dropdowns for config ID fields
├── unity/
│ ├── ArchmageDev/ # Unity demo & development project
│ └── dev.shadop.archmage/ # Unity package (OpenUPM)
├── godot/
│ └── ArchmageDev/ # Godot demo & integration test project
├── tests/ # xunit.v3 tests
│ ├── Conf/ # Generated config code
│ ├── testdata/ # atlas.json and config JSON
│ ├── override/ # Override-layer JSON
│ └── golden/ # Expected DumpAtlas output
├── scripts/ # Engine sync, tests, version bump, release
│ └── rsync-engines.sh # src/ → Unity package; test data → Unity and Godot projects
└── docs/ # Documentation site (Starlight)
dotnet build src/Archmage/Archmage.csproj
dotnet build src/Archmage/Sdk/Godot/Archmage.Godot.csproj
dotnet test tests/Archmage.Tests.csproj
dotnet test tests/Archmage.Tests.csproj --filter "FullyQualifiedName~TestName"
UPDATE_GOLDEN=1 dotnet test tests/Archmage.Tests.csproj # regenerate golden filesApache 2.0. See LICENSE for details.
