playverse-makers-spec
v0.1.2
Published
Engine-agnostic schema, migrations, and validation for Makers MapDocument. Use this when building adapters or play platforms that consume Makers maps without Babylon.
Maintainers
Readme
playverse-makers-spec
Engine-agnostic schema, migrations, and validation for the Playverse Makers authoring tool's
MapDocumentformat.Pure TypeScript. No Babylon. No DOM. No browser APIs. Use this when you need to read, validate, transform, or migrate maps without running the full Babylon runtime.
import { migrate, validateMap, collectAssetUrls } from 'playverse-makers-spec';
const doc = migrate(rawJson); // any version → current
const issues = await validateMap(doc, { requireLicenseOnModels: true });
const urls = collectAssetUrls(doc); // all model / texture URLs🤖 AI-targeted guide:
AGENTS.mdships in the published package — point your LLM coding assistant atnode_modules/playverse-makers-spec/AGENTS.mdfor a tight decision tree (when to call which function, how to generate valid maps, common mistakes).
Table of contents
specvsplayverse-makers- Install
- Use cases
- Quickstart
- What's exported
- Recipes
- Schema versioning policy
- Capabilities manifest
- License
spec vs playverse-makers
| | playverse-makers-spec (this) | playverse-makers |
| ---------------- | ------------------------------------------ | ------------------------------------------ |
| Layer | Data — types, validation, migration | Runtime — Babylon scene + physics + player |
| Dependencies | None | Babylon.js (peer) |
| Bundle size | ~20 KB gzip | ~33 KB gzip + Babylon |
| Browser required | No | Yes |
| Use it when | Building an adapter / validator / SSR tool | Drawing the map on screen |
If you're shipping a Babylon game, use playverse-makers — it
re-exports everything from spec so you don't need both. Reach for
spec directly when you want to avoid Babylon entirely.
Install
pnpm add playverse-makers-specZero runtime dependencies. ESM only. Requires TypeScript 5+ / ES2022.
Use cases
| You're building... | Why spec is the right answer |
| -------------------------------------------- | --------------------------------------------------------------------------------------- |
| Three.js / Unity / Godot adapter | Read MapDocument and instantiate scene in your engine of choice — no Babylon dragged in |
| Server-side validator (CI, publish hook) | Validate map shape, license, asset URLs without booting a browser |
| AI tooling (auto-generate, audit) | Inspect node graph, collect assets, extract capabilities manifest |
| Map list / detail UI | Load metadata server-side (SSR), render summary cards without scene |
| Migration utility (CLI, batch job) | Bring legacy v1 / v2 saves up to current schema |
| Static analysis / lint | Walk the document, enforce custom rules per organization |
Quickstart
Validate a map before publishing
import { validateMap, type ValidationIssue } from 'playverse-makers-spec';
const issues: ValidationIssue[] = await validateMap(doc, {
requireLicenseOnModels: true,
checkAssetReachability: true, // HEAD-fetches each model URL
});
if (issues.length > 0) {
console.error('Map failed validation:', issues);
process.exit(1);
}Migrate a legacy save
import { migrate, MAP_SCHEMA_VERSION } from 'playverse-makers-spec';
const oldDoc = JSON.parse(legacyJson); // schemaVersion: 1
const current = migrate(oldDoc); // schemaVersion: MAP_SCHEMA_VERSIONmigrate() is idempotent — passing a current-version document is a
no-op.
Inline external asset URLs (mirroring / offline bundles)
import { transformAssetUrls } from 'playverse-makers-spec';
const offlineDoc = transformAssetUrls(doc, async (url, kind) => {
// download to local CDN, return new URL
const local = await mirror(url);
return local;
});Build a Three.js renderer (no Babylon)
import * as THREE from 'three';
import type { MapDocument, MeshNode } from 'playverse-makers-spec';
import { migrate } from 'playverse-makers-spec';
function buildThree(scene: THREE.Scene, raw: unknown) {
const doc = migrate(raw);
for (const node of doc.nodes) {
if (node.kind === 'mesh') buildMesh(scene, node as MeshNode);
// ... light, model, spawn, trigger
}
}The Babylon runtime in playverse-makers is one such consumer — your
Three.js / Unity / Godot adapter mirrors it for a different engine.
What's exported
Schema types (re-exported under specific names too)
MapDocument, MapNode, MeshNode, ModelNode, LightNode,
SpawnNode, TriggerNode, SoundNode, ParticleNode, DecalNode,
TerrainNode, AnimationTimelineNode, ExtensionNode, MaterialSpec,
PhysicsSpec, EnvironmentSpec, LightmapBake, LooseMapDocument.
Plus the MAP_SCHEMA_VERSION constant (current major) and 2D variants
(SpriteAtlasGrid, SpriteAnimationClip, TileMetadata, View2DSpec,
BackgroundLayerSpec).
Migrations
| Symbol | Purpose |
| --------------------------------- | ------------------------------------------------------ |
| migrate(doc) | Bring any older document up to current schema |
| registerMigration(from, to, fn) | Register a migration step for a custom field/extension |
| listRegisteredMigrations() | Enumerate registered migrations (debugging) |
| UnsupportedSchemaError | Thrown when no path exists to current version |
Validation
| Symbol | Purpose |
| ------------------------ | ------------------------------------------------- |
| validateMap(doc, opts) | Full async validation (license, URL reachability) |
| validateMapShape(doc) | Sync — type/shape only, no network |
| collectAssetUrls(doc) | Every model / texture / audio URL referenced |
| collectCredits(doc) | License + author entries (attribution UIs) |
Asset URL transform
| Symbol | Purpose |
| ----------------------------- | --------------------------------------------------- |
| transformAssetUrls(doc, fn) | Rewrite asset URLs (mirroring, signing, bundling) |
| isExternalUrl(url) | True for http(s):// URLs (vs. blob/data/relative) |
Capabilities
| Symbol | Purpose |
| ----------------------- | ---------------------------------------------------- |
| extractManifest(doc) | Inventory of node kinds + trigger verbs the doc uses |
| applyManifest(doc, m) | Re-attach a manifest extracted earlier |
Custom node registration
| Symbol | Purpose |
| ---------------------------------------- | ---------------------------------------------------------- |
| registerCustomNodeSchema(kind, schema) | Register your own node kind for validateMap to recognize |
| getCustomNodeSchema(kind) | Retrieve a registered schema |
Document kind helpers
| Symbol | Purpose |
| ----------------------------------------- | -------------------- |
| documentKind(doc) | '2d' or '3d' |
| is2DDocument(doc) / is3DDocument(doc) | Type guards |
| newMapDocument2D(opts) | Empty 2D map starter |
| defaultView2D() | Default 2D viewport |
Recipes
Generate a manifest of a map's capabilities
import { extractManifest } from 'playverse-makers-spec';
const manifest = extractManifest(doc);
console.log(manifest.nodeKinds); // ['mesh', 'model', 'spawn', 'trigger']
console.log(manifest.triggerVerbs); // ['log', 'event', 'show', 'teleport']A play platform reads these to ensure it supports every feature the map uses before accepting it.
Strip out unsupported features from a map
const safeDoc: MapDocument = {
...doc,
nodes: doc.nodes.filter((n) => n.kind !== 'particle'), // drop particles
};Useful when targeting a renderer that doesn't implement a feature yet.
CLI — validate every map in a directory
#!/usr/bin/env node
import { readdir, readFile } from 'node:fs/promises';
import { migrate, validateMap } from 'playverse-makers-spec';
const dir = process.argv[2];
let failed = 0;
for (const file of (await readdir(dir)).filter((f) => f.endsWith('.json'))) {
const raw = JSON.parse(await readFile(`${dir}/${file}`, 'utf8'));
const doc = migrate(raw);
const issues = await validateMap(doc, { requireLicenseOnModels: true });
if (issues.length > 0) {
console.error(`✗ ${file}:`, issues);
failed += 1;
} else {
console.log(`✓ ${file}`);
}
}
process.exit(failed > 0 ? 1 : 0);Schema versioning policy
MapDocument follows a strict additive evolution policy:
- Additive minor (e.g. v3 adds
SoundNode) — never breaks existing saves.migrate()no-ops for older fields you don't touch. - Breaking major (e.g. v4 renames a field) — always ships with a
migration registered in this package, plus a
MIGRATION.mdentry. Older saves load forward without consumer code changes.
The current schema version is exported as MAP_SCHEMA_VERSION. Migrations
register at module load via a side-effect import — you don't need to
register them manually.
Capabilities manifest
Play platforms publish a manifest declaring which node kinds and trigger verbs they implement. Authoring tools can warn map creators when they use a feature not all platforms support. See capabilities-manifest spec.
