Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit44fd9e59bd

README.md

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 — structure (field types, constraints, defaults) and behavior live on the bound contract, never on the instance. Think of it as the ScriptableObject asset to dataType's ScriptableObject class.

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 opts or without contract fails on the missing value; the error that names the problem (no dataType contract named "<x>", saying to author the contract first) is the one for a contract name that isn't a registered dataType. Either way nothing is written. 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; a missing opts, a missing contract and an unknown contract name each 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.

Related types

  • .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.