Skip to content

Repository files navigation

Archmage

C# SDK Overview

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

Requirements

Unity

  • Unity 6000.3 or later
  • com.unity.nuget.newtonsoft-json 3.2.2

Godot

  • Godot 4.6 or later, .NET edition
  • Newtonsoft.Json 13.0.3

.NET

  • net8.0, netstandard2.1 or later
  • Newtonsoft.Json 13.0.3

Installation

Unity

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

Note

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.Sdk
  • Shadop.Archmage.Sdk.Unity
  • Shadop.Archmage.Sdk.Unity.Addressables (optional, only if using Addressables)
  • Shadop.Archmage.Sdk.Unity.Editor (optional)

Godot (via NuGet)

dotnet add package Shadop.Archmage.Godot

.NET (via NuGet)

dotnet add package Shadop.Archmage

Getting Started

Unity (Addressables)

A 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);

Godot

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.

.NET

using Shadop.Archmage.Sdk;

// ConfigAtlas is generated by Archmage
var atlas = new ConfigAtlas();
Archmage.LoadAtlas("configs/atlas.json", "configs/", atlas);

Concepts

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 Configs

Loading proceeds in the following steps:

  1. Parse atlas.json
  2. Apply AtlasModifier (if set)
  3. For each atlas item: read files → deserialize → apply overrides
  4. BindRefs() — resolve cross-table references
  5. OnLoaded() — 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.

AtlasOptions

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.

Custom File System

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:

  • MainThreadOnly returns true if the methods work only on the main thread, as with Unity's file systems. Otherwise, it returns false.
  • FileExists and DirectoryExists may return true without checking when an exact check is expensive.
  • ReadAllBytes and ReadAllBytesAsync must throw FileNotFoundException when the file does not exist. Missing override files are skipped this way.
  • If MainThreadOnly is true for the main IFS or for any override IFS, loading must start on the main thread. LoadAtlas and LoadAtlasAsync call the methods of every IFS on the calling thread.
  • Otherwise, if MainThreadOnly is false for every IFS, LoadAtlas and LoadAtlasAsync read the files of atlas items with ReadAllBytes and may call the methods on thread pool threads.

Special Types

I18n — Localization

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 — Cross-table Reference

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 object

Duration

A 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();    // TimeSpan

Arithmetic operators (+, -, *, /, %) and comparisons are supported.

Rgba

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

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

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.

Vec

Vec2<T>, Vec3<T>, Vec4<T> are typed vectors. Fields are accessed as .X, .Y, .Z, .W.

Tup

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.

Data Versioning

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"

See Also


Development

Project Structure

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)

Build & Test

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 files

License

Apache 2.0. See LICENSE for details.

About

Runtime library for the Archmage game configuration solution

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages