DEMIENGINEDOCUMENTATION Everything is free
Game systems

In this guide

Schema-backed data assets for items, quests, dialogue, and balance.

Game data

DataAsset holds versioned JSON game content — items, quests, dialogue, balance tables. DataSchema constrains that content so mistakes fail validation with an exact JSON pointer instead of breaking at runtime.

Author a schema

A schema is itself an asset. Example assets/schemas/item.data.json declared as a DataSchema manifest:

{
  "format_version": 1,
  "type": "object",
  "required": ["id", "name", "damage"],
  "properties": {
    "id": { "type": "string" },
    "name": { "type": "string" },
    "damage": { "type": "number", "minimum": 0, "maximum": 999 },
    "rarity": { "type": "string", "enum": ["common", "rare", "epic"] },
    "icon": { "type": "string", "reference": "asset" },
    "prefab": { "type": "string", "reference": "prefab" }
  }
}

Supported constraints:

  • type — object, array, string, number, integer, boolean.
  • required — field names that must be present.
  • properties / items — describe object fields and array elements.
  • enum — allowed values.
  • minimum / maximum — numeric bounds.
  • reference — asset, prefab, or scene. Validates that the URI resolves, so typos fail at authoring time.

Author data assets

General game-data JSON uses format_version: 1; native content types own their versions (terrain palettes use version 2). The manifest links the source to its schema:

{
  "format_version": 1,
  "id": "asset://items/iron_sword",
  "type": "DataAsset",
  "source": "iron_sword.json",
  "importer": "json_data",
  "importer_version": 1,
  "source_hash": "fnv1a64:...",
  "dependencies": ["asset://schemas/item"],
  "settings": {
    "schema": "asset://schemas/item",
    "content_type": "item",
    "tags": ["shop:forest", "tier:1"]
  }
}

content_type and tags are query keys — group items, quests, or dialogue lines by type and tag without scanning every asset.

To start with a generic data asset, run demi asset import content.json --project demi.project.json --id asset://data/content, then add a domain-specific content_type, tags, and schema to its manifest. Reimport after editing the source so its hash and derived content stay current: demi asset reimport path/to/content.asset.json. Validation checks the source, schema, and referenced assets before cooking.

Engine and editor import requests may set dataContentType when creating a data asset; an omitted or empty value uses data. The built-in terrain_material_set and terrain_palette types use their native content parsers during import and reimport. Invalid typed content is rejected before the manifest changes, and its asset:// references are added to manifest dependencies. Prefab references remain source references handled by prefab discovery and cooking; they are not asset manifest dependencies. Reimport retains other declared dependencies. Other content types remain generic: a string containing asset:// is not automatically a dependency. A schema with reference fields still validates its declared references and requires them in the manifest dependencies.

Read data from Lua

local Data = require("demi.data")

local sword, err = Data.load("asset://items/iron_sword")
if not sword then
    -- err is a table: err.code, err.message, err.path
    print("load failed:", err.code, err.message, err.path)
    return
end

-- sword is a plain Lua table matching the JSON document:
print(sword.name, sword.damage)   -- "Iron Sword"  12
print(sword.rarity or "common")   -- optional field → nil when absent

Data.load returns the parsed table on success (value, nil) or nil, error. The error table always has code, message, and path (the JSON pointer to the offending field when validation failed). Values are detached snapshots — copy them if you plan to mutate.

local results = Data.query({ content_type = "item", tags = { "shop:forest" } })
print(#results)                   -- 0 when nothing matches
for _, item in ipairs(results) do
    print(item.name, item.damage)
end

Data.query returns detached snapshots matching the content type and tags (an item must include every requested tag), ordered by stable asset ID. Empty array means “no matches”, not an error. Mutating a snapshot does not change the engine's source document or other snapshots.

CallReturns
Data.revision(uri)Integer revision counter — changes on every reload. Compare it to detect updates.
Data.kind(value)String type name of a loaded value (“object”, “array”, “string”, …).
Data.is_null(value)true when the value is the JSON-null sentinel.
Data.nullThe sentinel itself. Use it when you need to round-trip a JSON null: optional = value or Data.null.
Data.parse_yaml(text, source)value, nil or nil, error — converts a YAML string into the same table shape.

Hot reload

Watched data changes reload at frame boundaries and emit data_asset_reloaded:

Events.subscribe("data_asset_reloaded", function(e)
    -- e.id, e.old_revision, e.new_revision, e.affected_dependents
    print(e.id, "now at revision", e.new_revision)
end)

An invalid document keeps the previous revision and prints diagnostics, so a bad edit cannot break a running game.

YAML as input

Data.parse_yaml(text, source?) converts YAML mappings, sequences, and scalars to the same detached Lua value shapes as JSON data. It returns value, nil or nil, DataError; source is a diagnostic label. The parser observes the data-document size, depth, element, and string limits. Locale selection and other game rules belong in gameplay or packages.

Gameplay data packages

Optional pure-Lua packages provide flags, conditions, inventories, quests, and dialogue helpers on top of DataAsset. Install them like any other package and require their public modules.

Next