Packages

A package is a distributable unit of functionality — any mix of assets, Zig source, native libraries, and runtime plugins — that you can install into a project without manually copying files. It is Turian's answer to Unity's Package Manager and Asset Store, built on top of Zig's own package manager.

Status: asset, source, native, and plugin packages are all implemented end-to-end (install → build → run). The registry (remote search/publish) is on the roadmap.


Concepts

Term Meaning
Package The authoring/distribution unit: a turian-package.json manifest plus assets, source, native libs, and/or a plugin entry point.
OAP (.oap) The cooked, runtime asset container your game ships. A package's assets are cooked into the project's game.oap at build time.
Module A Zig compilation unit (b.addModule) — the Unity asmdef equivalent.
Plugin A package's runtime/editor registration entry point.

The full rationale and schema live in ADR 0001.


project.json is the source of truth

Every Turian project has a project.json that records the project's identity and its declared dependencies. The project's build.zig.zon is generated from project.json — like Unity and Flax regenerate their .csproj/.sln. You never hand-edit build.zig.zon; the CLI and Studio mutate project.json and regenerate the ZON on every build. This keeps dependency management in safe, familiar JSON and avoids ZON syntax mistakes.

{
  "turian_version": "0.16",
  "name": "My Game",
  "version": "0.0.0",
  "dependencies": {
    "com.acme.physics": { "url": "git+https://example.com/physics#v1.2.0", "hash": "..." }
  }
}

Dependency resolution (versions, conflicts, hashing, the lockfile) is delegated to Zig's package manager. Turian's layer adds the engine-integration manifest, discovery, asset-pipeline injection, and engine-compatibility checks.


The manifest: turian-package.json

A package declares itself with a turian-package.json at its root:

{
  "name": "com.example.color-palette",
  "version": "1.0.0",
  "author": "Example",
  "description": "A sample asset-only package",
  "license": "MIT",
  "engine_compat": ">=1.0.0",
  "types": ["asset"],
  "assets": ["assets"]
}
Field Required Notes
name Reverse-DNS, globally unique.
version Semver.
types One or more of asset, source, native, plugin.
assets Asset directories relative to the package root (default ["assets"]).
modules Exported Zig modules: { "name", "root" } (source packages).
native Native libraries: { "name", "kind", "lib", "include" }.
plugin Runtime entry point: { "register", "entry" }.
engine_compat Semver range checked against the running engine version.
dependencies Map of package name → semver range.

Installing a package

# From a local directory
turian-cli package install ./path/to/package --project my-game

# From a git repository
turian-cli package install git+https://example.com/my-package --project my-game

By default, packages install into a central store shared across all your projects — $TURIAN_PACKAGE_HOME, else ~/.cache/turian/packages (%APPDATA%\turian\packages on Windows) — laid out <name>/<version>/ so multiple versions coexist. The project records which packages+versions it uses in project.json; nothing is duplicated per project. This is also how a company can stand up an internal asset-store: one shared store, many projects.

Pass --vendored to copy a package into <project>/packages/ instead (for committing it alongside the project, or working offline). Vendored copies take precedence over the store.

turian-cli package install ./pkg            # → central store (default)
turian-cli package install ./pkg --vendored # → <project>/packages/
turian-cli package list   --project my-game
turian-cli package info   com.example.color-palette --project my-game
turian-cli package remove com.example.color-palette --project my-game   # keeps the shared store copy

The next turian-cli build discovers every installed package (store + vendored) and cooks its assets into game.oap automatically — no manual copying.

Installed package assets appear in Studio → Asset Browser under a read-only Packages section: you edit them in the package, not in the consuming project.


Source packages

A source package exports one or more Zig modules that user scripts (and other packages) can @import. The build system emits a b.addModule(...) for each module and wires it into the game and play-mode builds automatically.

Example turian-package.json:

{
  "name": "com.example.spawner",
  "version": "1.0.0",
  "types": ["source"],
  "modules": [
    { "name": "spawner", "root": "src/root.zig" }
  ]
}

Usage from a user script:

const spawner = @import("spawner");
pub fn update(self: *@This(), frame: engine.Frame) void {
    const cfg = frame.services.get(spawner.SpawnerConfig) orelse return;
    _ = cfg;
}

A working example lives at examples/packages/com.example.spawner/.


Plugin packages

A plugin package adds a runtime entry point called before the boot scene loads. The entry function receives *engine.Services so the package can register services that scripts retrieve via frame.services.get(T).

Manifest addition:

{
  "name": "com.example.spawner",
  "version": "1.0.0",
  "types": ["source", "plugin"],
  "modules": [{ "name": "spawner", "root": "src/root.zig" }],
  "plugin": {
    "register": "spawner",
    "entry": "register"
  }
}

register is the import name of the module that contains the entry function. entry is the function name. The generated main.zig calls:

@import("spawner").register(&g_services);

The entry function signature must be pub fn register(services: *engine.Services) void.


Collisions & compatibility

  • Duplicate package names and GUIDs shared across two packages are hard errors that fail the build with a clear diagnostic — package authors must keep asset GUIDs globally unique.
  • A package whose engine_compat range excludes the running engine version produces a warning (the build still proceeds).

← All docs Edit this page