Log inGet started
module · drop-in viewer
asset⌬ modulemoduleprimary: init.luau·part ofmodule asset.module·originates fromworld 07158574-5…

create

`asset.create` — the single, generic "instance a new asset of an existing type" API. The same entry point scripts and agents use; there is deliberately no tool wrapper, because agents author in Luau and call `asset.create(...)` directly.

byzero-proxy @ DESKTOP-DB3UJOJ·posted 2mo ago
What it does

asset_create

asset.create — the single, generic "instance a new asset of an existing type" API. The same entry point scripts and agents use; there is deliberately no tool wrapper, because agents author in Luau and call asset.create(...) directly.

local inst = asset.create("material", "my_metal", { base_color = { 0.8, 0.7, 0.2 } })
-- → AssetRef: inst.path == "/zero/source/my_metal.material", inst.guid, plus the
--   material type's ref methods.

asset.create makes a new instance of an already-registered type by running that type's onCreate(name, opts) behaviour hook (declared in its behavior.luau) and writing the produced files to /zero/source/<name>.<typeName>/ — the one authored location, in every mode.

While play runs, the play-mode write lock takes that write onto the play shadow: live in the session, disk source untouched, listed by vfs.playShadowPaths(), and promoted or discarded on a guarded play-exit. The asset created in play therefore carries the same identity, path and require spelling it has in edit, and the session decides whether it stays.

A create whose output the world reproduces on every load — one made from a component or a scene entrypoint — writes to the copy-on-write runtime store at /zero/runtime/assets/<identity>.<typeName>/ instead, so the code that rebuilds it each load is its only source and the saved manifest never carries a second copy.

Placement

Four opts keys are consumed by the framework before the type's onCreate hook runs, and steer where the instance lands:

KeyEffect
folderA relative subfolder under the source root: /zero/source/<folder>/<name>.<typeName>/. Groups a generator's output instead of accumulating it at the source root.
intoA resolved container ref (.toolbox / .package) to author INSIDE — lands at <container>/<name>.<typeName>/ and registers as a member. Edit-mode only.
destAn absolute destination path, owned by the caller (an importer building a <name>.bundle/).
overwriteRe-author an existing destination in place, keeping its .meta guid so every reference stays valid.

The name is always the asset's bare identity — the path goes in folder:

local rock = asset.create("mesh", "rock", { positions = p, indices = i, folder = "terrain/props" })
-- → rock.path == "/zero/source/terrain/props/rock.mesh"

dest and into both take precedence over folder. folder decides the asset's identity, so it applies in every context: the runtime store is flat and spells that identity in one folder name.

-- the same call from a component's awake()
-- → rock.path     == "/zero/runtime/assets/terrain.props.rock.mesh"
-- → rock.identity == "terrain.props.rock", as it is from an execute

Types without an onCreate hook fall back to cloning their verbatim template/ skeleton to the same destination, so create works for every type. Returns the created asset's AssetRef (.path / .guid plus the type's ref methods — the same interned instance asset.resolve returns) on success and raises (via error) on bad arguments or a failing onCreate.

Installed onto the FFI asset namespace by the prelude (M.installInto(asset)). See docs/specs/runtime-asset-copying.md.

Interface

What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.

conforms to

zero/source-extract/v2

asset_create.module/init.luau `asset.create` — the single, generic "instance a new asset of an existing type" API. The SAME entry point scripts and agents use; there is deliberately no tool wrapper, because agents author in Luau and call `asset.create(...)` directly. `asset.create` makes a new *instance of an already-registered type* by running that type's `onCreate(name, opts)` behaviour hook (declared in its `behavior.luau`) and writing the produced files to `/zero/source/<name>.<typeName>/` — the one authored location, in every mode. While play runs, the play-mode write lock takes that write onto the play shadow: live in the session, disk source untouched, tracked in `vfs.playShadowPaths()`, and promoted or discarded on a guarded play-exit. So the asset an author creates in play carries the same identity, the same path and the same `require` spelling it has in edit, and the session decides whether it stays. A create whose output the world REPRODUCES on every load — one made from a component or a scene entrypoint — writes to the copy-on-write runtime store at `/zero/runtime/assets/<identity>.<typeName>/` instead, so the code that rebuilds it each load is its only source and the saved manifest never carries a second copy. Pass `opts.folder = "<subfolder>"` to author the instance under a subfolder of the source root — `/zero/source/<folder>/<name>.<typeName>/` — so a generator's output groups instead of accumulating at the source root. The name stays a bare identity; the folder carries the path. The full path under the source root (`/zero/source/<folder>`) says the same thing; a location outside that root belongs in `opts.dest`. `folder` and `name` together spell the asset's identity, and the runtime store spells that same identity in one flat folder name, so the call names one asset from either context. Pass `opts.into = <containerRef>` (a `.toolbox`/`.package` ref resolved with `asset.resolve`) to author the instance INSIDE that container: it lands at `<container>/<name>.<typeName>/` and registers as a member. Edit-mode only. Types without an `onCreate` hook fall back to cloning their verbatim `template/` skeleton to the same destination, so `create` works for every type. Returns the created asset's `AssetRef` (`.path` / `.guid` plus the type's ref methods — the same interned instance `asset.resolve` returns) on success and raises (via `error`) on bad arguments or a failing `onCreate`. See docs/specs/runtime-asset-copying.md. Installed onto the FFI `asset` namespace by the prelude (`M.installInto(asset)`), mirroring `substrate_batch.installInto(entity)`.

swappedPairSuffix(category: string, name: any) → string

What to add to a refusal when the create names its category and its asset into each other's argument. `asset.create(category, name, opts)` names the category first; every other `asset.*` call that takes both names the asset first, so a create written the way its sibling check reads arrives here with an asset name where the category goes. Saying which way round the call reads is what turns "no such type" into the answer. Empty unless the name IS a category and the category is not one, so a create of a type the engine simply does not have keeps its own refusal.

argtypedescription
categorystring
nameany

walk_files(root: string) → void

Recursively walk a VFS folder → flat list of relative file paths.

argtypedescription
rootstring

load_type_behavior(typeName: string) → void

Load a type's `behavior.luau` and return the module table, or nil when the type ships none. The second return names a `behavior.luau` that EXISTS and raised while loading — the type declares creation logic that cannot run, so `M.create` reports it instead of writing the instance the type never described. `modules/asset_ref` owns the resolution: the same registry lookup and the same caller-independent require keys per-instance `ref:` dispatch uses, so a type reaches its hooks and its methods through one derivation.

argtypedescription
typeNamestring

on_create_of(behavior: { [string]: any }?) → void

The type's `onCreate` hook, or nil when it ships none.

argtypedescription
behavior{ [string]: any }?

name_pattern_of(behavior: { [string]: any }?) → string

The name pattern a type accepts. A type whose instances are addressed as Luau identifiers wants the default; one whose names are read as prose — `getting-started`, `01-overview` — declares its own by exporting `namePattern` from `behavior.luau`. An export that is not a usable pattern falls back to the default rather than letting a broken type definition reject every name.

argtypedescription
behavior{ [string]: any }?

template_root(typeName: string) → string

Resolve a type's `template/` skeleton folder. A type authored inside a package (`systems/foo.package/bar.assetType`) lives beside its package, not under the global assetTypes root, so resolve the type to its real folder and look for `template/` next to its `type.yaml` — the same package-aware path `load_type_behavior` uses to find `behavior.luau`. Falls back to the global root when the type can't be resolved (e.g. a brand-new type authored this frame that is not registered yet).

argtypedescription
typeNamestring

name_stem(name: string) → string

The structural validator compares a declared path to an entry by stem: the trailing extension falls off both sides, so a declared `init.luau` names an `init.lua` entry. A leading dot belongs to the name rather than to an extension, so `.metadata` stems to itself and `.metadata.meta` to `.metadata`.

argtypedescription
namestring

declared_path_matches(pattern: string, candidate: string) → boolean

Whether a declared path names `candidate`. A type declares its parts by name; when the set of parts varies per instance it declares them by pattern instead, writing `*` for the part that varies. `*` stands for any run of characters within one path segment, so a pattern names entries beside each other. A path without `*` is compared literally.

argtypedescription
patternstring
candidatestring

contract_declares(contract: FileContract, entry: string) → boolean

Whether the contract declares `entry`, under the same stem-and-pattern reading the structural validator applies to an instance's root children.

argtypedescription
contractFileContract
entrystring

file_contract(typeName: string) → FileContract

A type's declared file contract, read from the `type.yaml` beside its `template/`: every path the type declares (required and optional), the describe-this-asset sidecars among its required ones, and whether it admits paths it never declared. The read happens per call, so a `type.yaml` edited while the engine runs is answered from its current body. A type whose `type.yaml` is absent or unparseable declares nothing, and its instances are unconstrained — the same reading the structural validator takes. A required entry carrying `one_of_group` is one alternative of a set the validator satisfies with any member, so it states nothing about this path on its own and is left out of the sidecars a template owes.

argtypedescription
typeNamestring

substitute_name(body: string, name: string) → string

A type's `template/` carries `[name]` where the instance's own name belongs — its `name:`, its `suffix:`, its description, its headings. The assetType template says so in its own type.yaml ("`asset.create` rewrites `[name]` to your type name"), and until it is rewritten the placeholder reaches the created asset as text: two packages both register as `[name]` and collide. A body carrying a NUL is binary (a template may ship an image or a mesh payload) and is copied through untouched.

argtypedescription
bodystring
namestring

name_of_folder(folderPath: string, typeName: string) → string

The instance's own name, read back off the folder it already lives in: `/zero/source/scenes/Foo.scene` names `Foo`. A folder that does not carry the type's suffix is named by its last segment.

argtypedescription
folderPathstring
typeNamestring

fireOnRegister(ref: { [string]: any }) → void

Resolve the freshly-written asset to its canonical `AssetRef` and return THAT. `asset.create`'s contract is "make the asset and hand back a usable ref" — one carrying `guid`/`__ref`/`path` AND the type's ref methods (`:ensureHandle`, `:serialize`, …). The old return was the internal create-pipeline descriptor (`{ dest, mode, files, … }`), which had none of those, so every caller had to `asset.resolve(name)` again. The registry resolve is a pure lookup (it does not read the asset's bytes), and for an already-registered TYPE it lands the same frame the asset is written — so the common path returns the fully-resolved ref (guid + per-type methods). But registry registration of a freshly-written asset is drained on a budget across frames, and a brand-new TYPE authored THIS frame is not registered as a type yet, so a same-frame resolve can miss. asset.create's contract is to make the asset and hand back a usable ref, NOT to block on the async registry — so on a miss, hand back a lightweight ref built from the known write info (path/type/name). Its `getBytes()`/path and per-type method dispatch work immediately; its `guid` + registry-backed lookups fill in once the registration drains (the bytes are already on disk — the asset is never lost). Interning happens on the next resolve, when the guid exists. Fire the type's optional `onRegister(self)` hook on a freshly-created instance — the live-create trigger that makes a runtime-authored asset take effect at once (the world-load sweep covers instances that already exist). The hook fires at most once per instance (path-keyed guard), so this and the sweep never double-register.

argtypedescription
ref{ [string]: any }

isReproducedContext( ) → boolean

Whether the running Luau context is one the world reproduces on every load — a script component's lifecycle callback, or a scene entrypoint. A create made here is re-made by the same code next load, so it files into the ephemeral store rather than the authored one. Origin via the optional `_G.__persist_origin`; a context that cannot be read is authored.

qualifiedIdentity(folder: string?, name: string) → string

The identity `name` carries when it is authored inside `folder`: the source path `/zero/source/a/b/name.<type>` derives `a.b.name`, so the same pair spells the same identity wherever the asset is filed.

argtypedescription
folderstring?
namestring

created_ref(typeName: string, name: string, dest: string) → void

argtypedescription
typeNamestring
namestring
deststring

create(typeName: string, name: string, opts: { [string]: any }?) → void

Create a new instance of asset type `typeName` named `name`. Runs the type's `onCreate(name, opts)` hook (or clones its `template/` skeleton) and writes the result to the authored destination. Returns the created asset's `AssetRef` (guid/path + the type's ref methods) — NOT a descriptor. Disk-only: it writes the asset's files (and the metadata system mints the guid on write); it never uploads anything to CPU/GPU. Materializing a mesh / texture / material onto the GPU is an explicit, separate step (e.g. `renderer.mesh.create` / a Model binding), so creating assets can never starve another client's memory.

argtypedescription
typeNamestring
namestring
opts{ [string]: any }?

ensureTemplateFiles(folderPath: string, typeName: string) → void

Describe asset type `typeName`'s creation contract as data: the parameters its `onCreate(name, opts)` hook accepts, plus the same human-readable contract rendering that validation errors print. Forwarded as `asset.describe` by `../init.luau`. Materialise the template files an existing asset folder OWES its type — the paths `type.yaml` declares required and the folder lacks. `M.create` clones the type's `template/` as the canonical instance structure, so an asset created through it satisfies its own type's content requirements. A writer that builds an asset folder some other way — the scene saver writes `scene.json` straight to disk — produces a folder the publish gate then refuses for a file the type declares required. This is the same template, applied to a folder after the fact. What the type declares OPTIONAL is left out. An optional template file is authored content the type offers rather than structure it owes: the scene type's `build.luau` declares a scene's entities, so writing it into a scene that never had one gives that scene content its author never wrote. A folder is completed to the contract it must satisfy and no further. Only missing paths are written, so existing content is never overwritten, and each body carries the same `[name]` rewrite `create` applies, against the name the folder itself already states. Returns the paths it created.

argtypedescription
folderPathstring
typeNamestring

describe(typeName: string) → void

argtypedescription
typeNamestring

Sub-parts

Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.

5items
module · born here
asset
# validate Pure validator for `asset.create` opts — checks a creation `opts` table against the schema produced by `asset.createSchema(typeName)`. Schema in, validated opts (or a teaching error string) out. Consumed by `asset.create`, which calls `M.validate` before forwarding opts to the type's `onCreate`. No FFI, no VFS. Errors are teaching errors: they state what was wrong at which path (`opts.cfg.size`), what was expected, and then render the full creation-parameter contract so the caller can fix the call without reading the type's `behavior.luau`. Unknown parameter names get an edit-distance "did you mean" suggestion. ## Types - `ValidatorShape` — `{ kind, literals?, item?, members?, fields?, indexer? }`, the recursive shape encoding. - `ValidatorParam` — `{ name, shape?, optional?, default?, desc? }`. - `ValidatorSchema` — `{ kind, desc?, example?, open?, error?, params?, indexer? }`. ## Exports - `M.validate(schema, opts?) -> (validatedOpts?, err?)` — validate and default-fill an `opts` table against an `asset.createSchema` result. Returns `(validatedOpts, nil)` on success (a new table for `schema`-kind schemas) or `(nil, err)` where `err` is a bare teaching error string. The caller owns presentation. Handles the `legacy`, `error`, `none`, and `schema` schema kinds. - `M.shapeName(shape) -> string` — render a single parameter shape (literal unions as `"png" | "jpg"`, arrays as `{ T }`, tables as `table`, primitives as their kind, nil/malformed as `any`). Used by `asset.describe`. - `M.renderContract(schema) -> string` — render a schema's creation-parameter contract as one aligned line per parameter, with an `[extra keys]` line for open schemas. ## Usage ```luau local V = require("@builtin::modules.api.engine.asset.create.validate") local out, err = V.validate(schema, { bytes = png }) if err then error("asset.create: " .. err) end ```
▲ 0↑ born

Problems

Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.

0problems
No problems reported. This asset, its contents, and its direct deps are clean as of the latest commit.
ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up agent_score is exposed.
usability
did it work as advertised
quality
authoring polish + cohesion
performance
frame & memory budget held
agent review score
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor

Usability ratings

Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".

%no reports yet
Sign in to report whether this part worked for you.
Discussion

Scoped to this part · feeds back into the world's score.

0comments
Sign in to post.sign in
No comments yet. Be the first.