Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion packages/asset-packs/scripts/validate.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,41 @@
import { getTriggerComponent, assertValidTriggerComponent } from '../src/types';
import { validateComposite } from '../src/validation';
import type { ValidationError } from '../src/validation';
import { LocalFileSystem } from './utils/local';

async function main() {
const local = new LocalFileSystem('./packs');
const assetPacks = await local.getAssetPacks();
const allErrors: ValidationError[] = [];

for (const assetPack of assetPacks) {
const assetsPath = local.getAssetsPath(assetPack.name);
const assets = await local.getAssets(assetsPath);
for (const asset of assets) {
console.log(asset.name, 'βœ…');
// Existing validation: trigger action refs must have id and name
const triggerComponent = getTriggerComponent(asset);
if (triggerComponent) assertValidTriggerComponent(asset.name, triggerComponent);

// Composite component dependency validation
const errors = validateComposite(asset.composite.components, asset.name);
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, 'βœ…');

}

console.log(asset.name, 'βœ…');
}
console.log(assetPack.name, 'βœ…');
}

if (allErrors.length > 0) {
console.error(`\n${allErrors.length} validation error(s) found:`);
allErrors.forEach(e => console.error(` ❌ [${e.rule}] ${e.message}`));
process.exit(1);
}

console.log('\nAll assets validated successfully βœ…');
}

main().catch(error => {
Expand Down
55 changes: 55 additions & 0 deletions packages/asset-packs/src/validation/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { AssetComposite } from '../types';

// SDK defaults for GltfContainer collision masks (from @dcl/ecs)
const CL_POINTER = 1;
const CL_PHYSICS = 2;
const GLTF_DEFAULT_VISIBLE_MASK = 0; // CL_NONE
const GLTF_DEFAULT_INVISIBLE_MASK = CL_PHYSICS;

type Component = AssetComposite['components'][number];

export function getComponent(components: Component[], name: string): Component | undefined {
return components.find(c => c.name === name);
}

export function hasComponent(components: Component[], name: string): boolean {
return components.some(c => c.name === name);
}

export function getComponentData(components: Component[], name: string): any | undefined {
return getComponent(components, name)?.data?.['0']?.json;
}

/**
* Check if entity has a collider with the given mask.
* GltfContainer is considered a valid pointer collider because GLTF models
* can have internal collider meshes (*_collider) that the SDK uses for raycasting.
*/
export function hasPointerCollider(components: Component[]): boolean {
const meshCollider = getComponentData(components, 'core::MeshCollider');
if (meshCollider && (meshCollider.collisionMask & CL_POINTER) !== 0) return true;

// 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;
}

return false;
}

/**
* Check if entity has any explicit collision mask > 0 set.
* Uses SDK defaults when values are not specified in the composite.
*/
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.


const gltf = getComponentData(components, 'core::GltfContainer');
if (gltf) {
const visibleMask = gltf.visibleMeshesCollisionMask ?? GLTF_DEFAULT_VISIBLE_MASK;
const invisibleMask = gltf.invisibleMeshesCollisionMask ?? GLTF_DEFAULT_INVISIBLE_MASK;
if (visibleMask > 0 || invisibleMask > 0) return true;
}

return false;
}
12 changes: 12 additions & 0 deletions packages/asset-packs/src/validation/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { AssetComposite } from '../types';
import type { ValidationError } from './rules';
import { rules } from './rules';

export type { ValidationError } from './rules';

export function validateComposite(
components: AssetComposite['components'],
assetName: string,
): ValidationError[] {
return rules.flatMap(rule => rule.validate(components, assetName));
}
Loading
Loading