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
dataTypecontract (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
.datainstance 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.datasuffix strips). - Folder shape:
values.yaml—contract: <dataTypeName>plus avalues: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 boundAssetRef<dataType>. -
ref:satisfies(wanted)—(ok, reason?): whether this instance's contract chain includeswanted(the bound contract itself, or any contract it extends).reasonis a string explaining afalseresult (unrelated contract, unresolvable binding, invalid values). -
ref:validate()—(ok, violations)against the bound contract's merged schema.violationsis an array of{ path, message }. -
ref:data(wanted?)— the resolved, read-only value object: this instance'svalues.yamlwith the bound contract's schema defaults filled in,assetRef/dataReffields materialized (assetRef→AssetRef,dataRef→ a nested value object resolved the same way), and the bound contract chain'sbehavior.luaumethods reachable by field-style call —v:someMethod()alongsidev.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 nobehavior.luausimply contributes no methods — it still inherits its ancestors' via the chain.Pass
wantedto 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 towanted.Raises when the instance's values violate its contract (same violations
:validate()reports), whenwanteddoesn't appear in the bound contract's chain, and whendataReffields 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.yamlinstead. 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, asAssetRefs.<contractRef>:instances()— every.datainstance bound to that contract or to a contract that extends it (seedataType'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'svalues.yaml::contract). - Keep
values.yamllimited 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.contractmust name an existingdataType; both a missingoptsand an unknown contract name error before any file is written. - Values drifting from the contract. Editing
values.yamlre-runs validation immediately; a violation warns rather than silently passing through. - Confusing configuration with runtime state. A
.datainstance never mutates during play — components own live state.
Related types
.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.