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 item to an alternative data set at load time via WithVariant
  • Whitelist/Blacklist — load only a subset of 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 pluggable strategies for parallel loading
  • 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

Requirements

Unity

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

.NET

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

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)

.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. For Inspector integration, see ArchmageEditorTools.cs, which demonstrates config ID dropdown wiring.

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

.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 item: read files → deserialize → apply overrides
  4. BindRefs() — resolve cross-table references
  5. OnLoaded() — post-load initialization

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 => { ... })
    // custom Newtonsoft.Json settings
    .WithJsonSettings(customSettings);

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

Custom load strategy — By default, items are loaded one by one in alphabetical order. WithLoadStrategy and WithAsyncLoadStrategy let you take control of that loop — for example to load items in parallel:

// Parallel sync
var opts = new AtlasOptions().WithLoadStrategy((items, load) =>
    Parallel.ForEach(items, kvp => load(kvp.Key, kvp.Value)));

// Parallel async
var opts = new AtlasOptions().WithAsyncLoadStrategy(async (items, loadAsync, ct) =>
    await Task.WhenAll(items.Select(kvp => loadAsync(kvp.Key, kvp.Value, ct))));

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

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

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. In Unity, .ToColor() converts it to UnityEngine.Color.

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.

Vec

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

Tup

Tup1Tup7 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)
│   │   └── 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)
├── tests/                          # xunit.v3 tests
│   ├── Conf/                       # Generated config code
│   ├── testdata/                   # atlas.json and config JSON
│   ├── override/                   # Override-layer JSON
│   └── golden/                     # Expected DumpAtlas output
├── scripts/                        # Unity sync, version bump, release
│   └── rsync-unity.sh              # src/ → unity/dev.shadop.archmage/ sync
└── docs/                           # Documentation site (Starlight)

Build & Test

dotnet build src/Archmage/Archmage.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

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages