Skip to content

Feat/asset packs validation - #1263

Open
alejandralevy wants to merge 2 commits into
mainfrom
feat/asset-packs-validation
Open

Feat/asset packs validation#1263
alejandralevy wants to merge 2 commits into
mainfrom
feat/asset-packs-validation

Conversation

@alejandralevy

@alejandralevy alejandralevy commented Mar 26, 2026

Copy link
Copy Markdown
Contributor

Improve Smart items validations — CI validation rules for asset packs

Adds a composite validation system to the asset-packs CI pipeline that checks component dependencies in smart item composite.json files. This catches broken items before they reach production.

What it does

Expands make validate-asset-packs (already runs in CI on every asset-packs PR) with 9 validation rules that check component relationships across all 214 smart items.

Validation rules

Errors (block CI):

  1. pointer-events-requires-collider 🎯 — If an entity has PointerEvents or an on_input_action/on_click trigger, it must have a collider (MeshCollider or GltfContainer)
  2. invisible-collider-needs-collision-mask 👻 — If an entity has VisibilityComponent(visible: false) + GltfContainer, at least one collision mask must be > 0 (otherwise it has no effect at runtime). Skips items that use set_visibility to toggle at runtime
  3. trigger-action-references-must-resolve 🔫 — Self-referencing trigger actions must point to an action name that actually exists in the entity's Actions component
  4. **animator-requires-gltf 👾 ** — If an entity has Animator, it must have GltfContainer (animations live inside GLTF models)
  5. video-player-requires-display 🎥 — If an entity has VideoPlayer, it must have a texture source (GltfNodeModifiers or Material) and a render surface (GltfContainer or MeshRenderer)
  6. trigger-conditions-reference-valid-components 🔫 — Trigger conditions referencing states (when_state_is) or counters (when_counter_equals) must have the corresponding States or Counter component on the entity

Warnings (don't block CI):

  1. states-must-be-referenced 😄 — If an entity defines States, they should be referenced somewhere in triggers or actions
  2. actions-unique-names📛 — Action names within an Actions component should be unique
  3. text-shape-mutually-exclusive ✍️: — TextShape should not coexist with MeshRenderer or GltfContainer on the same entity (they are mutually exclusive per SDK spec). Multi-entity composites where TextShape is on a child entity are fine

@github-actions

Copy link
Copy Markdown
Contributor

Test @dcl/asset-packs package

  • Install via NPM:
    npm install "https://sdk-team-cdn.decentraland.org/creator-hub/branch/feat/asset-packs-validation/@dcl/asset-packs/dcl-asset-packs-2.15.0-commit-acdfacee2087f044fe07be9000537c134fc67697.tgz"

Note: If new assets are added in this PR, they won't be available on the CDN until the PR is merged. This package can be used to test changes to the library code or catalog.json, but won't work for testing newly added items.

@github-actions

Copy link
Copy Markdown
Contributor

Test @dcl/inspector package

  • Preview: link
  • Install via NPM:
    npm install "https://sdk-team-cdn.decentraland.org/creator-hub/branch/feat/asset-packs-validation/@dcl/inspector/dcl-inspector-7.31.1-commit-acdfacee2087f044fe07be9000537c134fc67697.tgz"

@github-actions

Copy link
Copy Markdown
Contributor

Test this pull request on macos-latest

Download the correct version for your architecture:

mac-x64
mac-arm64

Click here if you don't know which version to download

For running this unsigned version of the app, you will need to run the xattr command on it:

  1. Extract the app from the downloaded .dmg file (double-click it)
  2. Place the extracted app anywhere you like in your file system
  3. Open a terminal on the directory where the app is
  4. Run xattr -c app-name, replacing "app-name" for the actual name of the app
  5. Double-click the app ✅

@github-actions

Copy link
Copy Markdown
Contributor

Test this pull request on windows-latest

Download the correct version for your architecture:

win-x64

@alejandralevy alejandralevy left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Code Review

Nice validation system! The architecture is clean — rules are well-isolated, helpers are reusable, and the CI integration is solid. A few things I noticed:

Issues

1. ✅ printed even when asset has errors (validate.ts)
After printing errors for an asset, the code unconditionally logs asset.name, '✅'. This is misleading in CI logs — someone skimming would think everything passed. Suggest:

if (errors.length === 0) console.log(asset.name, '✅');

2. hasPointerCollider may be too permissive (helpers.ts)
It returns true for any GltfContainer regardless of collision masks. But the SDK defaults defined in the same file show GLTF_DEFAULT_VISIBLE_MASK = 0 and GLTF_DEFAULT_INVISIBLE_MASK = CL_PHYSICS — neither includes CL_POINTER. So an asset with a GltfContainer but no pointer collision mask would pass this check incorrectly.

If GltfContainer visible meshes are implicitly raycastable by the engine (regardless of masks), this is fine and worth a comment. Otherwise, consider checking the actual masks for the CL_POINTER bit like hasAnyCollisionMask does for its checks.

3. Collision mask compared with > 0 instead of !== 0 (helpers.ts)
Minor: meshCollider.collisionMask > 0 and visibleMask > 0 — for bitmask checks, !== 0 is idiomatic and safer (handles theoretical cases where high bits could produce negative values in 32-bit signed interpretation). Not a real risk with small DCL masks, but a good habit.

Suggestions

4. No tests for the validation rules
This is a CI-blocking validation system with 9 rules and non-trivial logic (bitwise ops, cross-component checks, self-reference detection). Unit tests for each rule would be straightforward since validateComposite takes plain data — no mocks needed. Particularly worth testing:

  • Edge cases in hasPointerCollider / hasAnyCollisionMask with various mask values
  • trigger-action-references-must-resolve with different {self} patterns
  • invisible-collider-needs-collision-mask skip logic (set_visibility, triggers/actions)
  • Empty/missing component arrays

5. Rule numbering gap (rules.ts)
Rules jump from 3 → 5 → 6 → 7 → 10 → 11 → 12 → 13. Looks like leftover numbering from a design doc. Consider renumbering for clarity, or add a comment referencing the design doc.

What looks good ✅

  • Clean separation: helpers → rules → index → validate script
  • Smart skip logic in invisible-collider-needs-collision-mask (checking for set_visibility actions across all entities)
  • text-shape-mutually-exclusive correctly checks per-entity key overlap instead of just component presence
  • Error vs warning severity split is well-thought-out
  • The flatMap pattern in validateComposite is elegant

for (const error of errors) {
const prefix = error.severity === 'error' ? '❌' : '⚠️';
console.error(`${prefix} [${error.rule}] ${error.message}`);
if (error.severity === 'error') allErrors.push(error);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This console.log(asset.name, '✅') runs even when the asset has validation errors — misleading in CI logs. Wrap it:

if (errors.length === 0) console.log(asset.name, '✅');

// GltfContainer models support pointer raycasting via their visible meshes
// and internal *_collider meshes, even without explicit collision masks
if (hasComponent(components, 'core::GltfContainer')) return true;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This returns true for any GltfContainer regardless of masks. But the defaults above show GLTF_DEFAULT_VISIBLE_MASK = 0 (no pointer) and GLTF_DEFAULT_INVISIBLE_MASK = CL_PHYSICS (no pointer). Is this intentional because the engine always raycasts visible GLTF meshes? If so, a comment would help. Otherwise, check the masks for CL_POINTER bit like:

const gltf = getComponentData(components, 'core::GltfContainer');
if (gltf) {
  const vis = gltf.visibleMeshesCollisionMask ?? GLTF_DEFAULT_VISIBLE_MASK;
  const invis = gltf.invisibleMeshesCollisionMask ?? GLTF_DEFAULT_INVISIBLE_MASK;
  if ((vis & CL_POINTER) !== 0 || (invis & CL_POINTER) !== 0) return true;
}

*/
export function hasAnyCollisionMask(components: Component[]): boolean {
const meshCollider = getComponentData(components, 'core::MeshCollider');
if (meshCollider && meshCollider.collisionMask > 0) return true;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: meshCollider.collisionMask > 0 — for bitmask checks, !== 0 is more idiomatic and technically safer. Same for visibleMask > 0 || invisibleMask > 0 below.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

1 participant