---
name: use-asset-library
description: >
  Add or locate LunCoSim assets under `assets/`: USD components, WGSL shaders,
  Modelica models, SysML requirements, or event-driven Rhai policies. Use for discovery, spawn
  palette entries, `lunco://` references, Twin-mounted assets, source programs,
  or web manifests. This skill owns placement and resolution; use
  author-usd-component for USD authoring and validate-assets for pre-flight.
---

# Use the asset library

Almost everything in LunCoSim is an **asset file**, not a Rust type. A part, a
vehicle, a material, a subsystem's physics, a mission script — all of them are
files under `assets/` that the engine discovers at runtime.

> **Rust ships parameterized behaviours; it never hardcodes a thing.** If you
> are about to add a Rust struct for a specific rover, habitat, or shader, stop
> and add a file instead.

Related: [`author-usd-component`](../author-usd-component/SKILL.md) (how to write
the `.usda`), [`build-vehicle`](../build-vehicle/SKILL.md) (assemble parts),
[`build-usd-scene`](../build-usd-scene/SKILL.md) (assemble a scene),
[`author-scenario`](../author-scenario/SKILL.md) (rhai),
[`run-modelica`](../run-modelica/SKILL.md) (`.mo`),
[`validate-assets`](../validate-assets/SKILL.md) (**pre-flight before you run**).
Design: [`56-asset-resolution-and-cache.md`](../../docs/architecture/56-asset-resolution-and-cache.md),
[`50-usd-driven-visuals.md`](../../docs/architecture/50-usd-driven-visuals.md).

## Where things live

The canonical human/AI map is [`assets/README.md`](../../assets/README.md).
Read it before adding a file; the filesystem is the runtime manifest and the
README defines the stable taxonomy.

| Folder | Holds |
|---|---|
| `assets/components/` | reusable part prims referenced into vessels — domain folders include `avionics/`, `cameras/`, `comms/`, `environment/`, `gnc/`, `lights/`, `mobility/`, `mounting/`, `payload/`, `power/`, `terrain/`, `thermal/` |
| `assets/vessels/` | whole vehicles — `rovers/`, `landers/`, `satellites/`, `balloons/`, plus `control_profiles.usda` |
| `assets/structures/` | surface installations — habitat, mast, ISRU plant, landing pad |
| `assets/props/` | simple scene objects — ball, ramp, wall |
| `assets/scenes/` | loadable stages — `base/`, `luncosim/`, `tests/`, `celestial/` |
| `assets/models/` | behaviour sources: `.mo` (Modelica); `.py` only when the optional Python backend is explicitly enabled |
| Twin-owned requirement roots | `.sysml` / `.kerml` source files declared and indexed by `twin.toml` |
| `assets/scenarios/` | `.rhai` bound as a `LunCoProgramAPI` source |
| `assets/scripting/` | importable rhai modules — `lib/`, `prelude/`, `policy/`, `tools/` |
| `assets/shaders/` | `.wgsl` |
| `assets/celestial/`, `missions/`, `tutorials/`, `lighting/`, `config/` | global/application data |

## Architecture boundary

- USD owns assembly, transforms, variants, references, and wiring.
- Modelica owns continuous/domain math.
- Rhai owns scenario orchestration, rules, scoring, and assertions.
- The task kernel is reusable mechanism; Rhai owns authored behavior policy.
- Rust owns only general heavy runtime capabilities and bridges; expose their
  controls to Rhai instead of baking mission policy into Rust.

For a generic human/AI assembly workflow, use the hot-reloadable
`assets/scripting/tools/model_authoring.rhai` facade. Keep its reusable reads,
plans, and checks generic; put Twin-specific recipes beside the Twin. Pair any
observable asset/linter regression with a USD fixture and Rhai scene observer
under `assets/scenes/tests/` and `assets/scenarios/tests/`.

Environment facts are produced by a distinct
`components/environment/probe.usda` source prim. Never make a Modelica/Python
consumer declare `gravity_accel` or a direction-target triplet as its own
output and connect that output back to itself. EnvironmentProbe outputs use
`<target-id>_mount_x/y/z`; reusable Modelica consumers use the generic
`target_mount_x/y/z` inputs, and their wires choose which object supplies the
direction.

The engine-recognized **source** extensions are walked into the discovery manifest
(`crates/lunco-assets-runtime/src/discovery.rs`): **`.usda`, `.wgsl`, `.rhai`, `.mo`,
`.py`, `.sysml`, `.kerml`**. `.mo` (Modelica), `.py` (optional Python), and
SysML/KerML sources are catalogued both because a `.usda`/Twin names them and
so they can be browsed directly. Python is not the standard scenario backend;
Rhai is. The Scenarios menu groups registered source files by type.
Non-source data (`.json`, `.toml`) is not walked: it is read by a subsystem or
evaluated ad hoc, not browsed as an authored asset.

## The `lunco://` scheme

`lunco://<rel>` = the runtime `assets/<rel>`, with that root's packed cache and
the shared cache after authored assets. The runtime root is selected by
the fallible `lunco_assets_core::assets_dir_abs()` from `LUNCO_ASSET_ROOT` when
set, otherwise executable/package ancestry before current-directory ancestry.
An invalid override or unresolved library stops admission with an error;
callers propagate that error instead of inventing a root. The asset owner
provides typed Bevy load addresses, preserving literal `#` and `%` filenames.
The complete read order comes from `lunco_assets_core::library_roots()` in
`crates/lunco-assets-core/src/lib.rs`.
`twin://<assigned-mount>/<rel>` is the same shape one level down: the Twin's authored
root, its `<twin>/.cache`, then the global cache. This lets Twins reuse a
global downloaded product without putting a machine path into USD.
Authored bytes always win over materialised ones. Schemes are registered in
`crates/lunco-assets-runtime/src/asset_sources.rs`; `twin://` is stateful, it is not a
second texture scheme. Use the existing logical `lunco://` or `twin://` identity
for every delivered artifact. Canonical URI construction, separator normalization,
and traversal validation come from the dependency-free `lunco-assets-path` crate;
source roots and bytes are admitted by `lunco-assets-runtime` using the identity
and cache contracts from `lunco-assets-core`. For reopened Twins, use the
authority returned by the current mount, never reconstruct it from the
manifest name or retain an outgoing address. Follow the canonical
[mount identity contract](../../docs/architecture/55-scene-addressing-and-roots.md#3-mount-addresses-and-stable-source-identity)
for provenance and scenario synchronization. Verify replacement content through
the production app; resolver seam tests alone do not prove Bevy cache behavior.
Native USD payloads, texture inputs, and Rhai asset sources use the
[typed native admission contract](../../docs/architecture/55-scene-addressing-and-roots.md#native-payload-and-source-admission).
Pass the stage's actual `AssetServer` path and current `TwinRoots` to the asset
owner; keep its `Result<AssetPath>` through loading and attach labels separately.
Verify a valid native filename with URI characters and a rejected foreign or
retired mount through the production consumer; do not register an unrestricted
`file:` source or enable unapproved paths to make a fixture load.

Anything the cache fallback can serve is DECLARED in an `Assets.toml` and
downloaded only on request (Settings ▸ Downloadable data, the Twin Browser
Files section's Twin assets rows, or the `lunco-assets` CLI) — the engine never
fetches on its own, so an asset that is merely declared resolves to nothing
until someone asks for it. The browser rows are only a view of
`lunco_assets_datasets::DatasetRegistry`; they must emit its typed
request/cancel commands and never create a second downloader or parse cache
paths. The optional `lunco-assets` package composes native workers from
`lunco-assets-transport`, `lunco-assets-download`, and
`lunco-assets-processing`; consumers that only inspect declarations or state should depend on
`lunco-assets-datasets`.

All requesters use the `download` section of the one settings file owned by
`lunco-settings`. `DownloadSettings.max_attempts` includes the first request;
retry waits are exponential and capped. Native and browser fetches retain
received bytes and resume with HTTP `Range` when the origin supports it.
Callers must pass the shared settings resource through the existing asset/API
surface rather than adding a local retry loop or path.

**A bare relative path is not "wrong" — it is resolved against the anchoring
document's directory, keeping that document's scheme.** That is why it bites:

```usda
# ✅ engine library — works no matter who mounts this file
prepend references = @lunco://components/mobility/wheel.usda@
prepend references = @lunco://components/power/battery.usda@

# ✅ a file sitting next to a Twin scene
uniform asset info:sourceAsset = @twin://my_mission/gnc.rhai@

# ⚠️ only legal when this file is itself inside assets/ AND never Twin-mounted
uniform asset info:sourceAsset = @scenarios/foo.rhai@

# ❌ always — `..` escapes the root and returns NotFound
prepend references = @../../components/mobility/wheel.usda@
```

The active program contract is visible in the runtime status. A Modelica or
optional Python source with no declared `inputs:`/`outputs:` is reported as source-only;
it is not treated as a running participant. `AttachProgram` is the canonical
way to add the source and its explicit scalar contract. "My model does nothing"
should be diagnosed by checking `CosimStatus` and `GetBrokenConnections`, not by
assuming a hidden fallback. The production scene-test catalog reports read and
parse status for every discovered `.usd*` asset, and the sandbox smoke
scenario asserts that shipped USD sources are readable and parseable. Only
[`validate-assets`](../validate-assets/SKILL.md) catches a broken `references`
arc before you launch.

> Rhai `import` uses the same canonical asset identity as USD. Use a logical
> `lunco://…` or `twin://…` URI, an assets-root `/…` path, or a path relative to
> the importing script. `RhaiSourceLoader` loads every literal import as a Bevy
> dependency; unused scripts are not preloaded.

The source asset owner qualifies default Bevy paths as `lunco://` before it
publishes source text or prepared ASTs. Scenario roots, imports, and unloads use
the same canonical identity; Twin paths retain `twin://`. Consumers commit the
complete loaded import graph before binding or starting a source because Bevy
can report graph readiness before its `Added` messages are consumed.

## Add a USD component

Write one file = one spawnable thing. The full authoring reference is
[`author-usd-component`](../author-usd-component/SKILL.md); the *library* rules
are:

```usda
#usda 1.0
( defaultPrim = "Widget"   # ← lunco:spawnable is ONLY read here
  upAxis = "Y"  metersPerUnit = 1.0
  doc = """What this is." """ )

def Xform "Widget" ( kind = "component" prepend apiSchemas = ["LunCoCatalogAPI"] )
{
    uniform bool lunco:spawnable = true
}
```

**How it reaches the palette** (`crates/lunco-scene-catalog/src/catalog.rs`):

| Palette field | Derived from |
|---|---|
| `id` | the **file stem** |
| `display_name` | stem Title-cased (splits `_` and `-`) |
| `category` | the **immediate parent folder**, Title-cased |
| description | the stage's `doc` metadata |

So `components/power/solar_panel.usda` lands under **"Power"** — not
"Components". A file with no parent folder lands in "Other". Nothing is
hardcoded; **moving the file changes its category.**

- `lunco:spawnable` defaults to **false** — it is opt-in.
- It must sit on the stage's `defaultPrim`. On any other prim the palette never
  sees it (child `lunco:spawnable` is a different feature — subpart selection).
- An unreadable file is not spawnable and logs `CATALOG: … unreadable`.
- Editing an already-scanned file? Send **`RescanSpawnCatalog`** — the scan
  caches per asset. Adding a *new* file is picked up automatically on native
  (the filesystem is the manifest).

## Add a shader (`.wgsl`)

Drop it in `assets/shaders/`. It is walked into the manifest and registered into
the `ShaderCatalog` automatically (`RescanShaders` to re-read edits). Bind it
**directly on the gprim**:

```usda
uniform asset info:wgsl:sourceAsset = @lunco://shaders/rover_hull.wgsl@
```

The tunable surface is reflected from a `struct Material` at
`@group(2) @binding(0)`, annotated with `//!@` comments
(`crates/lunco-materials/src/dyn_params.rs`). There are exactly **three**
directives — there is no `//!@param`:

```wgsl
struct Material {
    //!@engine display_color
    display_color: vec3<f32>,
    //!@ui 0.0 1.0 "Wear"
    wear: f32,
    //!@default albedo 0.17,0.17,0.17
    //!@ui color "Accent"
    accent_color: vec3<f32>,
}
```

| Directive | Effect |
|---|---|
| `//!@engine <name>` | the **engine fills this uniform** — see the registry below |
| `//!@ui <name> [args] "Label"` | `color` / `int min max` / `min max` (slider) / else free |
| `//!@default <name> <v>[,<v>…]` | packed value when nothing else supplies one |

### Engine-filled uniforms

`crates/lunco-materials/src/engine_params.rs` is the **provider registry** — a
process-wide `OnceLock`, so the validator, the prop picker and the renderer all
read the same list.

| `//!@engine` name | Filled from | Usable on a prop? |
|---|---|---|
| `display_color` | the prim's composed `primvars:displayColor` **element 0** | ✅ |
| `sun_vis` | horizon ray-march visibility | ✅ |
| `sun_dir`, `sun_dir_world`, `sun_tan_radius` | sun direction / angular radius | ❌ |
| `hf_size`, `hf_res`, `csm_far`, `shadow_cache_on` | terrain heightfield + shadow state | ❌ |

**The colour contract: author `primvars:displayColor`, the shader consumes it.**

```usda
color3f[] primvars:displayColor = [(0.30, 0.72, 0.35)]   # ARRAY, linear
```

One authored attribute, in the standard USD place, whether the part renders
through plain PBR or through WGSL. An authored `inputs:<name>` on the bound
Shader **always wins** over the engine fill — but authoring `inputs:display_color`
hides the colour from every other tool that reads USD, so use `inputs:` only for
what displayColor cannot express (accents, panel scale, wear, dust).

A shader using any ❌ param is refused by the **prop material picker** (it would
render black on a rover part) but still works as a scene shader — that is exactly
the `not prop-pickable` warning from
[`validate-assets`](../validate-assets/SKILL.md). An unregistered `//!@engine`
name warns and packs to its `//!@default` (or zero) — nothing fills it.

## Add a Modelica behaviour (`.mo`)

For an acausal physical domain, author component class/nameplate facts and
connector topology in USD. The runtime projector compiles each connected island
into one Modelica model; never bind one solver program per electrical part.

Three gates, each of which silently does nothing when unmet:

1. **The language is the file extension**, nothing else. `.mo` → Modelica,
   `.py` → Python, `.rhai` → Rhai.
2. **No `inputs:`/`outputs:` ⇒ never stepped.** The cosimulation projector requires at least
   one port-prefixed attribute. A model with no ports is a documentation-only
   reference.
3. **`realtimeSafe` defaults to `false`**, and the wiring pass then refuses the
   prim a force/torque port on a client-predicted body. Author it `true` when
   the program drives a force.

And `sourceAsset` must be typed **`asset`**, never `string` — only an `asset` is
visible to the resolver, the reference closure, and packaging.

**Write it branch-free.** rumoca's solver path has no `if`/`when` in equations —
express clamps as `der(x) = expr` with `max()`/`min()`.
[`validate-assets`](../validate-assets/SKILL.md) enforces this as an error;
`assets/models/LunCo/Electrical/Battery.mo` plus the reusable rover electrical
assemblies are the worked examples. Rhai is event and mission glue only;
production scenarios must not define `on_tick`. Authored tests may use it for
bounded state sampling and verdicts. Keep equations in Modelica and ordinary
progression in task/events.

## Regenerate the web manifest

Native runtime walks the filesystem, so **adding a file needs no step at all**.
The **web** build has no filesystem: it fetches `assets/manifest.json`
(`crates/lunco-assets-runtime/src/discovery.rs`). After adding or removing any catalogued source
(`.usda`/`.wgsl`/`.rhai`/`.mo`):

```bash
./scripts/build_web.sh build luncosim
```

which rsyncs `assets/` into `dist/` and runs
`cargo run -p lunco-assets --bin build_asset_manifest -- <dist>/assets`
(`scripts/build_web.sh`). That generator calls the **same**
`discovery::scan_library` the native runtime uses, so the two cannot drift.
There is no standalone regenerate command.

Binary runtime assets are staged by the manifest-declared bundle target as part
of the same build. Add or change the `bundle` field on the authoritative
`Assets.toml` entry, then rebuild; do not add a second shell list. The staging
command validates the declared raw/processed artifact before copying it, so a
missing or incomplete asset fails the build instead of becoming a browser 404.
Bundle-qualified keys are for log identity; temporary download files use an
opaque process/attempt name, so adding a grouped asset never creates a
platform-dependent cache subdirectory.

## Validate before you run

```bash
"$LUNCOSIM_BIN" --validate assets/vessels/rovers/my_rover.usda
```

Seconds, no GPU, no app. Composes the whole reference closure — so it catches
the broken `@lunco://…@` that would otherwise be a mystery at load — and runs the
strict wheel reader. See [`validate-assets`](../validate-assets/SKILL.md).

## Anti-patterns

USD composition is owned by `lunco-usd-compose`: `lunco-assets-core` supplies
canonical IDs and traversal-safe roots, while `lunco-assets-runtime` supplies
source bytes; composition interprets USD arcs into an
inert stage. Modelica, Rhai, behavior trees, physics, and rendering bind later
in their own layers. A tutorial only projects metadata from that stage.

- ❌ A bare relative reference to a **shipped** asset — it resolves against the
  anchoring document, so the file breaks the moment a Twin mounts it. Use
  `@lunco://…@`.
- ❌ `@../../…@` anywhere — `..` escapes the root and returns NotFound. There
  are zero such refs in the tree; keep it that way.
- ❌ A dynamic/non-literal Rhai `import` — dependencies must be known while the
  asset is loading, so the loader rejects it visibly.
- ❌ `lunco:spawnable` on a prim that is not the stage `defaultPrim` — invisible
  to the palette.
- ❌ Encoding a category in the filename — the category IS the parent folder.
- ❌ A `LunCoProgramAPI` with no `inputs:`/`outputs:` and expecting it to run.
- ❌ `info:sourceAsset` typed `string` — must be `asset`.
- ❌ `if`/`when` in a `.mo` equation section — rumoca is branch-free.
- ❌ Authoring `inputs:display_color` instead of `primvars:displayColor` — it
  works, and it hides the colour from every other USD consumer.
- ❌ Adding a Rust struct for a specific vehicle/part/material. It is a file.
- ❌ Assuming the web build picked up a new asset without re-running
  `build_web.sh`.
