Log inGet started

data (asset type)

Updated October 8, 2026

When to use one

  • You have a dataType contract (e.g. weapon, questObjective) and need one or more concrete, named configurations of it (smg, rifle, sniper).
  • You want the configuration to persist as authored content, discovered by asset.list("data") / contractType:instances(), and validated against the contract on every edit.
  • Mutable runtime state (ammo remaining, cooldown timers) belongs on a component, not here — a .data instance is configuration, not simulation state.

If you don't have a contract yet, author one first (asset.create("dataType", ...)).

Where it lives

  • Source: /zero/source/.../<Name>.data/
  • Identity: <Name> (the .data suffix strips).
  • Folder shape:
    • values.yaml — contract: <dataTypeName> plus a values: map of the contract's fields. Required.
    • .metadata — description + tags for search. Required.

Play-mode edits fork a runtime copy (runtime_copy: true) — tweaking values while gameplay runs never writes back to the source instance.

How to create one

asset.create("data", "<Name>", { contract = "<dataTypeName>" })
-- Creates: /zero/source/<Name>.data/
--   values.yaml   (contract binding + the contract's declared defaults)
--   .metadata     (empty description/tags — fill both to publish)

opts.contract is required — creating without it, or naming a contract that isn't a registered dataType, errors loudly and names contract in the message. The seeded values.yaml fills every field the contract declares a default for, so a fresh instance is immediately valid wherever every required field has a default.

Ref surface (AssetRef<data>)

  • ref:contract() — the bound AssetRef<dataType>.

  • ref:satisfies(wanted) — (ok, reason?): whether this instance's contract chain includes wanted (the bound contract itself, or any contract it extends). reason is a string explaining a false result (unrelated contract, unresolvable binding, invalid values).

  • ref:validate() — (ok, violations) against the bound contract's merged schema. violations is an array of { path, message }.

  • ref:data(wanted?) — the resolved, read-only value object: this instance's values.yaml with the bound contract's schema defaults filled in, assetRef / dataRef fields materialized (assetRef → AssetRef, dataRef → a nested value object resolved the same way), and the bound contract chain's behavior.luau methods reachable by field-style call — v:someMethod() alongside v.someField. Method resolution is child-first: the bound contract's own methods win over an inherited one of the same name, and either loses to an explicit instance value of that name (values always shadow methods). A contract with no behavior.luau simply contributes no methods — it still inherits its ancestors' via the chain.

    Pass wanted to assert the instance's bound contract satisfies that contract before resolving (same semantics as :satisfies) — it's an assertion of intent, not a filter: the returned object still carries the full bound schema and full method chain, not a view scoped to wanted.

    Raises when the instance's values violate its contract (same violations :validate() reports), when wanted doesn't appear in the bound contract's chain, and when dataRef fields form a circular chain (instance A referencing B referencing A).

    The object is read-only at the top level — assigning a field raises; change configuration by editing values.yaml instead. Nested plain tables (struct and array values) are private copies of the resolved snapshot: mutating them changes only your snapshot, never the source instance or any other caller's resolve.

    Generalized iteration (for k, v in w do) walks the resolved value fields (explicit values + filled defaults); methods are reachable by name but never appear in iteration.

    local w = smg:data()             -- resolved value object
    print(w.damage, w.fireMode)      -- schema values (with defaults)
    print(w:dps())                   -- a behavior.luau method, if the contract ships one
    
    local weapon = smg:data("weapon") -- asserts smg's contract chain includes "weapon"
    

Discovery

  • asset.list("data") — every registered instance, as AssetRefs.
  • <contractRef>:instances() — every .data instance bound to that contract or to a contract that extends it (see dataType's ref surface).
  • asset.inspect("<name>") — the instance's files, bound contract, values, and this type README.
  • ls /zero/source/<Name>.data lists the files the folder holds.

Authoring conventions

  • Name instances for the specific thing they configure (smg, rifle), not for the contract they bind (that's values.yaml::contract).
  • Keep values.yaml limited to configuration; anything that changes during gameplay belongs on a component instead.
  • Say what this specific configuration is tuned for in the description of its .metadata — the contract's own README documents field meaning.

Common pitfalls

  • Missing or unknown contract at creation. opts.contract must name an existing dataType; both a missing opts and an unknown contract name error before any file is written.
  • Values drifting from the contract. Editing values.yaml re-runs validation immediately; a violation warns rather than silently passing through.
  • Confusing configuration with runtime state. A .data instance never mutates during play — components own live state.
  • .dataType — the contract this instance is bound to; owns structure, validation, and (optional) behavior.
  • .preset — a named set of values for one declarer's settings schema (an assetType, an importer, a component), not a contract of its own.

In the Inspector

Values: a link to the contract this instance is bound to, every contract field as a typed control (data_schema's toFields), and the values that break the contract. An edit is checked against the contract and written through dataRef:setValues, one undoable step. setValues re-encodes values.yaml with Yaml.encode, so comments in the file are not kept; a values.yaml that carries comments is therefore shown read-only, with Open for the file.

dataRef:setValues(patch) replaces the named top-level fields and checks the result against the contract. A change is refused (one error naming each violation, nothing written) when it introduces a violation or leaves one standing at or under a value it set. A list item is known by what it holds rather than where it sits, so removing or reordering items sets none of the rest. A violation the file already held anywhere else stays listed and does not stop the change, so an instance that does not yet satisfy its contract fills in one field at a time, and a value beside a key the contract does not declare stays editable. It writes values.yaml, recorded into the open undo operation when there is one. An Inspector edit writes only the leaves it changed: every other value keeps its stored value and form, including a colour held as numbers, a map-form vector, and keys the contract does not declare.

  • asset-type
  • reference