Log inGet started

data (asset type)

A .data instance is a configured value binding to a dataType contract: values.yaml names the contract and supplies the field values the contract's schema declares. Instances carry values only —…

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.yamlcontract: <dataTypeName> plus a values: map of the contract's fields. Required.
    • README.md — instance documentation: what this configuration is for and where it's used. 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)
--   README.md     (instance README template)
--   .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 (assetRefAssetRef, 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.
  • cat /zero/source/<Name>.data — same summary.

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.
  • Document what this specific configuration is tuned for in README.md — 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 snapshot of one component's properties, not a typed multi-field contract.
  • asset-type
  • reference