Under Construction
Get Started

A polished configuration solution
for game development

Define, Fill in, Export & Generate, Integrate, Leverage, Collaborate: Archmage forges every stage of config work into one integrated workflow, backed by thoughtfully designed specs and tools.

Why Archmage

Diverse data types

50+data types in 7 categories

  • Basic
  • Passive
  • Compact
  • Multi-Column
  • Subtable
  • Behavior
  • Non-Leaf

Archmage’s type system goes beyond numbers and strings: enum, duration, cross-table reference, back-reference, l10n string, min–max range, weighted pool, condition, anchor, and more — many of which general-purpose config tools rarely offer.

Each type is end-to-end: type declaration, value fill-in, default export value, validation rules, data export, code generation, and runtime access. If you build an in-house tool, you'll have to decide, implement, test, and maintain each of these — type by type.

Even the simplest type, the integer, is not that simple: bit width, signedness, number bases such as hex, underscores as digit separators, the range each file format can hold, and constraints across programming languages.

One source, two pipelines

10+filtering mechanisms

  • index table
  • #
  • enabled
  • brace flags
  • xflags
  • tags
  • minver
  • switch + case
  • condition
  • cflags
  • env

Config files can be spreadsheets, tree-structured data files (e.g., YAML, JSON), or a mix of both.

You edit the config files. Archmage runs two pipelines. Data export produces runtime data. Code generation produces typed code. Both pipelines apply the same filters to carve out the same subset from the source. Data export also enforces validation rules and collects l10n strings for translation.

The Archmage SDK then brings the data and code together, parsing the runtime data using the corresponding config types.

Whenever changes happen, you re-export, regenerate, or both — no handoff docs to write. No hand-written code to update. No drift.

Built for humans and AI agents

2flags: --readable and --schema

TypeDefaultReadable
enum1fire
ref3emberstrike
duration[0, 90]1m30s
l10nspell[101].nameFireball
anchorheartseeker

Runtime data is shaped for the game to load, not for reading. Export with--readable, and every value becomes readable on its own: an enum shows its item name, a cross-table reference resolves to its anchor text, a duration renders in a human-friendly form, and an l10n string shows its raw value.

Add --schema, and every exported config gets a .schema file beside it, naming every field once — its path, its type, and the description you gave it. Nothing about the structure is left to guess.

--readable puts the meaning back into values. --schema hands over the blueprint. With both flags, AI agents understand your configs better, straight from the Archmage output, and help you reason through decisions and iterate on your design.

Supported Input File Formats

Spreadsheets: .xlsx .xlsm .csv

Tree-structured data: .yaml .json .json5 .js .toml .xml

Supported Languages
  • C#
  • Go

More planned

Supported Game Engines
  • Unity

More planned

Supported Languages for Enums
  • C#
  • Go
  • Java
  • Python
  • TypeScript
  • JavaScript
  • Lua
  • GDScript
  • C++
  • Rust
  • PHP
  • Protocol Buffers

One workflow. Every stage.

Define

You decide the layout of each set of config data and the data type of each field. You choose whatever file format works best: spreadsheets, tree-structured data files (e.g., YAML, JSON), or a mix of both.

Spreadsheets
spell.xlsx
DescSpell nameDamageBound rune
Namenamedamagerune
Typel10n>>minmax|intref@spell-rune
    
    
quest.yaml
ember-hunt:
    title: The Ember Hunt
    level: 3
    repeatable: false
    objectives:
        - kind: kill
          enemy: cinder-wisp
          count: 8
        - kind: collect
          item: ember-shard
          count: 12

Fill In

A field’s data type is not just a label. It comes with carefully designed fill-in settings that make data input easier.

Data Types
spell.xlsx
DescSpell nameDamageBound rune
Namenamedamagerune
Typel10n>>minmax|intref@spell-rune
101Fireball40 | 55emberstrike
102Frost Nova30 | 38deep-freeze

Collaborate

Google Sheets (Optional)

Edit together live in Google Sheets — no file locks, no binary merge conflicts. Those sheets are then pulled down into your repository as spreadsheet files, fitting into your team’s existing version control workflow.

Team Collaboration
Version-Controlled Repository

Export

Archmage parses and validates config files and outputs the runtime data. It also collects l10n strings for translation, and optionally turns opaque numbers into text that is readable by humans and AI agents.

Data Export
spell.json
{
  "101": {
    "name": "spell[101].name",
    "damage": { "min": 40, "max": 55 },
    "rune": 3
  },
  "102": {
    "name": "spell[102].name",
    "damage": { "min": 30, "max": 38 },
    "rune": 5
  }
}
Runtime Data
Readable Configs
L10n Strings

Translate

Once exported, the l10n strings are ready for you to translate with your preferred translators, tools, and workflow.

Localized Text

Generate

Data export and code generation run as independent pipelines over the same config source. Code generation renders config structures into strongly-typed code and emits a ConfigAtlas type.

Code Generation
// Code generated by (shadop.dev) archmage. DO NOT EDIT.

public partial struct SpellCfgId
{
    public long Value;
    public readonly SpellCfg Cfg => ConfigAtlas.Instance.SpellTable[Value]!;
}

public partial class SpellTable : Dictionary<SpellCfgId, SpellCfg> { }

public partial class SpellCfg
{
    [JsonProperty("id")] public SpellCfgId Id { get; set; }
    /// <summary>Spell name</summary>
    [JsonProperty("name")] public L10n Name { get; set; }
    /// <summary>Damage</summary>
    [JsonProperty("damage")] public MinMax<long> Damage { get; set; }
    /// <summary>Bound rune</summary>
    [JsonProperty("rune")] public XRef<SpellRuneCfgId, SpellRuneCfg> Rune { get; set; }
}
// Code generated by (shadop.dev) archmage. DO NOT EDIT.

type SpellCfgID int64

type SpellTable map[SpellCfgID]*SpellCfg

type SpellCfg struct {
    ID SpellCfgID `json:"id"`
    // Spell name
    Name L10n `json:"name"`
    // Damage
    Damage MinMax[int] `json:"damage"`
    // Bound rune
    Rune XRef[SpellRuneCfgID, SpellRuneCfg] `json:"rune"`
}

func (x SpellCfgID) Cfg() *SpellCfg {
    return GetConfigAtlas().SpellTable[x]
}
// Code generated by (shadop.dev) archmage. DO NOT EDIT.

public partial class ConfigAtlas : IAtlas
{
    /// <summary>
    /// The active ConfigAtlas. It must be set before calling any XxxCfgId.Cfg.
    /// </summary>
    public static ConfigAtlas Instance = null!;

    /// <summary>The user-defined configuration extensions.</summary>
    public AtlasExtension Extension { get; private set; }
    /// <summary>The version info of the config repo at export time.</summary>
    public VersionInfo DataVersion { get; private set; }

    public QuestCfg QuestCfg { get; set; }
    public SpellTable SpellTable { get; set; }
    public SpellRuneTable SpellRuneTable { get; set; }

    // ...
}
// Code generated by (shadop.dev) archmage. DO NOT EDIT.

// GetConfigAtlas returns the active ConfigAtlas. It must be set before
// calling any XxxCfgId.Cfg.
var GetConfigAtlas func() *ConfigAtlas

// ConfigAtlas holds all game configuration tables and provides cross-table
// reference binding.
type ConfigAtlas struct {
    AtlasExtension

    // DataVersion is the version info of the config repo at export time.
    DataVersion *archmage.VersionInfo

    QuestCfg       QuestCfg
    SpellTable     SpellTable
    SpellRuneTable SpellRuneTable
}

// ...
Typed Code

Integrate

Once the data and the types are in place, you bring them together with an Archmage SDK.

SDKs
using Shadop.Archmage.Sdk;

var i18n = new I18n("en");
i18n.MergeL10nFile("configs/l10n.json", "en");
i18n.MergeL10nFile("l10n/fr.json", "fr");
L10n.GetI18n = () => i18n;
L10n.GetPreferredLanguage = () => settings.Language;

// `atlas.json` is the manifest file generated by `archmage export`
var atlas = new ConfigAtlas();
Archmage.LoadAtlas("configs/atlas.json", "configs/", atlas);
ConfigAtlas.Instance = atlas;
import "shadop.dev/pkg/sdk-go/archmage"

i18n := archmage.NewI18n(language.English)
i18n.MergeL10nFile("configs/l10n.json", language.English)
i18n.MergeL10nFile("l10n/fr.json", language.French)
conf.GetI18n = func() *archmage.I18n { return i18n }
conf.GetPreferredLanguage = func() language.Tag { return settings.Language }

// `atlas.json` is the manifest file generated by `archmage export`
atlas := &conf.ConfigAtlas{}
archmage.LoadAtlas("configs/atlas.json", "configs/", atlas)
conf.GetConfigAtlas = func() *conf.ConfigAtlas { return atlas }
ConfigAtlas

Leverage

Your game logic works with the ConfigAtlas. Every field is typed, and you enjoy the capabilities built into the types, especially those the Archmage SDK provides.

var spell = atlas.SpellTable[spellId];

label.Text = spell.Name.Text;           // l10n   → localized text
int dmg    = spell.Damage.Sample(rng);  // minmax → a random value in range
var elem   = spell.Rune.Ref.Element;    // ref    → the rune's element, no lookup
spell := atlas.SpellTable[spellID]

label.SetText(spell.Name.Text())  // l10n   → localized text
dmg  := spell.Damage.Sample(rng)  // minmax → a random value in range
elem := spell.Rune.Ref.Element    // ref    → the rune's element, no lookup

Readable Configs, AI Agents First

--readable puts the meaning back into values. --schema hands over the blueprint.

This matters most when an AI agent consumes the configs. Reviewing game balance, catching a wrong value, or iterating on core mechanics — an AI agent works best when the values mean something on their own. The clearer the values are, the less it guesses and the more it knows.

Readability & AI
spell.json (readable)
{
  "101": {
    "id": 101,
    "name": "Fireball",
    "damage": { "min": 40, "max": 55 },
    "rune": "emberstrike"
  },
  "102": {
    "id": 102,
    "name": "Frost Nova",
    "damage": { "min": 30, "max": 38 },
    "rune": "deep-freeze"
  }
}
spell.json (runtime)
{
  "101": {

    "name": "spell[101].name",
    "damage": { "min": 40, "max": 55 },
    "rune": 3
  },
  "102": {

    "name": "spell[102].name",
    "damage": { "min": 30, "max": 38 },
    "rune": 5
  }
}
spell.schema
---name: spellsource: spell.xlsx---$               map[int64]$.*             {}$.*.id          int64$.*.name        l10n              # Spell name$.*.damage      minmax            # Damage$.*.damage.min  int$.*.damage.max  int$.*.rune        ref@spell-rune    # Bound rune

Fast enough to run on every change

100 config files, 2,000 fields, 300,000 config entries, 6,290,153 values

export time
4.75 s
entries/s
63,127
values/s
1,323,612

Apple M4 Pro, 14 cores

Enum code generation, Free and Unlimited

One enum definition, one set of enum capabilities — Archmage carries them into 12+ programming languages, each rendered in that language’s own idioms and type system.

Definition to Code
Flexible Underlying Types
int, int8/16/32/64, uint, uint8/16/32/64.
Conditional Generation
Scenario flags for filtering enum types and enum items.
Bitflags
Bitwise-combinable enum types.
Auto-Numbering
iota auto-assigns values: 0, 1, 2, 3…; for bitflags: 0, 1, 2, 4, 8…
External Types
Same enum type: native in one language, external in another.
__options__:
    underlyingType: int32
    xflags: "client,server"

__namespace__:
    go: "game/enums"
    cs: "Game.Enums"

Element:
    None: 0
    Fire: 1
    Frost: 2
    Nature: 3
Named Types
Each enum type is defined as a named type.
Typed Constants
Each enum item is defined as a typed constant.
Underlying Types
From int8 to uint64; parsing enforces the value range.
Enum ↔ String Conversion
In both directions — by default the item name, overridable in the definition.
Multiple Parsing Formats
Item name in any case, TypeName(n), decimal, and hex are all supported.
Safe Parsing
Failures trigger explicit errors or exceptions instead of silent zero values.
JSON Integer Serialization
Enum values serialize as integers in JSON.
Sentinel
Boundary markers such as Count, excluded from the valid values.
Duplicate Values
Multiple items can share the same integer value; all names remain valid for parsing.
Bitflag Enums
Parses and formats flag combinations, e.g., "frost, fire".
AllValues
Returns the full set of valid enum values as an ordered list.
IsValid
Checks if a given value corresponds to a defined enum item.
// Code generated by (shadop.dev) archmage. DO NOT EDIT.

namespace Game.Enums
{
    public enum Element : int
    {
        None = 0,
        Fire = 1,
        Frost = 2,
        Nature = 3,
    }

    public static partial class ElementExtensions
    {
        // ...

        public static IReadOnlyList<Element> AllValues() { /* ... */ }
        public static Element Parse(string str) { /* ... */ }
        public static bool IsValid(this Element x) { /* ... */ }
        public static string GetEnumString(this Element x) { /* ... */ }
    }
}
// Code generated by (shadop.dev) archmage. DO NOT EDIT.

package enums

type Element int32

const (
    ElementNone   Element = 0
    ElementFire   Element = 1
    ElementFrost  Element = 2
    ElementNature Element = 3
)

// ...

func ElementValues() []Element { /* ... */ }
func ParseElement(str string) (Element, error) { /* ... */ }
func (x Element) IsValid() bool { /* ... */ }
func (x Element) String() string { /* ... */ }

No cost. No restrictions. No strings attached.
Just enum code generation.

In a sea of scattered configs

Venture forth with data and code,
always in sync