ADR-282: Decentraland Inspector

More details about this document
Latest published version:
https://adr.decentraland.org/adr/ADR-282
Authors:
cazala
Feedback:
GitHub decentraland/adr (pull requests, new issue, open issues)
Edit this documentation:
GitHub View commits View commits on githistory.xyz

Abstract

The @dcl/inspector package implements a scene editor interface for Decentraland, built on React, BabylonJS, and TypeScript. It provides a modular architecture that can be integrated into different environments through well-defined RPC interfaces and abstractions.

Core Components

Entity Hierarchy

A React component that renders and manages the scene's entity tree. It:

Component Inspector

Handles component editing through specialized inspectors:

Component-specific inspectors include:

A complete list of all available inspectors can be found in the EntityInspector folder of the SDK repository.

Features:

Level Editor

3D scene visualization and manipulation:

Asset Management

The Assets panel provides access to three main sources of content:

  1. Local Assets

  2. Custom Items

  3. Asset Packs

Additional functionality includes:

Integration Architecture

Data Layer RPC

The Data Layer provides a complete interface for all editor operations, handling both scene state and asset management. It serves as the primary communication channel between the Inspector and its host environment, responsible for:

  1. Scene State Management:

  2. Asset Management:

The Data Layer has two implementations:

  1. Local:

  2. Remote:

The storage behavior is determined by the File System Interface implementation used, not by the Data Layer type. For example:

The remote Data Layer is implemented as a protobuf-defined RPC service to ensure type safety and versioning:

service DataService {
  // Scene state synchronization
  rpc CrdtStream(stream CrdtStreamMessage) returns (stream CrdtStreamMessage)

  // Asset management
  rpc CreateCustomAsset(CreateCustomAssetRequest) returns (CreateCustomAssetResponse)
  rpc GetCustomAssets(Empty) returns (GetCustomAssetsResponse)
  rpc GetAssetCatalog(Empty) returns (AssetCatalogResponse)
  rpc DeleteCustomAsset(DeleteCustomAssetRequest) returns (Empty)
  rpc RenameCustomAsset(RenameCustomAssetRequest) returns (Empty)

  // File operations
  rpc GetFiles(GetFilesRequest) returns (GetFilesResponse)
  rpc SaveFile(SaveFileRequest) returns (Empty)
  rpc GetFile(GetFileRequest) returns (GetFileResponse)
  rpc CopyFile(CopyFileRequest) returns (Empty)

  // Editor state
  rpc Undo(Empty) returns (UndoRedoResponse)
  rpc Redo(Empty) returns (UndoRedoResponse)
  rpc Save(Empty) returns (Empty)
}

File System Interface

The File System Interface is a lower-level abstraction focused solely on file operations. It has three distinct implementations:

  1. In-Memory (feeded-local-fs.ts):

  2. Node.js (sdk-commands/start/data-layer/fs.ts):

  3. IFrame (iframe-storage.ts):

The interface definition remains intentionally simple:

export type FileSystemInterface = {
  dirname: (path: string) => string
  basename: (filePath: string) => string
  join: (...paths: string[]) => string
  existFile: (filePath: string) => Promise<boolean>
  readFile: (filePath: string) => Promise<Buffer>
  writeFile: (filePath: string, content: Buffer) => Promise<void>
  readdir: (dirPath: string) => Promise<{ name: string; isDirectory: boolean }[]>
  rm: (filePath: string) => Promise<void>
  cwd: () => string
}

Additional RPCs

All additional RPCs are implemented using the @dcl/mini-rpc library, which provides type-safe client/server communication. These include:

  1. Storage RPC: Implements the IFrame file system interface
  2. Camera RPC: Controls viewport and screenshots
  3. UI RPC: Manages Inspector UI state
  4. Scene Metrics RPC: Reports scene statistics

Each RPC uses postMessage for transport in IFrame implementations and follows the mini-rpc pattern of:

Relationship Between Layers

The Data Layer uses the File System Interface but adds:

Example:

While the File System Interface would only handle the raw file operations without understanding the asset structure or scene context.

Integration Types

IFrame Integration

The parent application embeds the Inspector in an IFrame and communicates through postMessage:

Storage RPC Setup

The parent application needs to set up the RPC bridge to handle file system operations:

function initRpc(iframe: HTMLIFrameElement) {
  const transport = new MessageTransport(window, iframe.contentWindow!)
  const storage = new StorageRPC(transport)

  // Handle file operations
  storage.handle("read_file", async ({ path }) => {
    return fs.readFile(path)
  })

  storage.handle("write_file", async ({ path, content }) => {
    await fs.writeFile(path, content)
  })

  storage.handle("exists", async ({ path }) => {
    return fs.exists(path)
  })

  storage.handle("delete", async ({ path }) => {
    await fs.rm(path)
  })

  storage.handle("list", async ({ path }) => {
    const files = await fs.readdir(path)
    return Promise.all(
      files.map(async (name) => ({
        name,
        isDirectory: await fs.isDirectory(path.join(path, name))
      }))
    )
  })

  return {
    storage,
    dispose: () => storage.dispose()
  }
}

React Component Integration

Example of embedding the Inspector in a React application:

const CONTENT_URL = "http://localhost:3000" // URL to your iframe content

function InspectorComponent() {
  const iframeRef = useRef()

  const handleIframeRef = useCallback((iframe) => {
    if (iframe) {
      iframeRef.current = initRpc(iframe)
    }
  }, [])

  useEffect(() => {
    return () => iframeRef.current?.dispose()
  }, [])

  const params = new URLSearchParams({
    dataLayerRpcParentUrl: window.location.origin // this is the url of the parent application
  })
  const url = `${CONTENT_URL}?${params}` // url where the inspector is being served

  return <iframe onLoad={handleIframeRef} src={url} />
}

CLI Integration

The CLI integration provides a development-focused approach using WebSocket communication:

Server Setup

Using the Decentraland CLI (@dcl/sdk-commands):

# Start the CLI server with data layer enabled
npx sdk-commands start --data-layer --port 8001

This creates:

Usage

  1. Serve the inspector. You can do so by running:
cd packages/@dcl/inspector
npm start

Or you can also install the @dcl/inspector package in your project, and then serve it from node_modules, like:

npm install @dcl/inspector
npx http-server node_modules/@dcl/inspector/public
  1. Access the Inspector with CLI integration by visiting:

Now access the inspector from the browser, passing the dataLayerRpcWsUrl parameter to connect to the CLI's WebSocket server:

http://localhost:3000/?dataLayerRpcWsUrl=ws://127.0.0.1:8001/data-layer

Where localhost:3000 is the Inspector and 127.0.0.1:8001 is the CLI's WebSocket server.

The Inspector will automatically:

Configuration

The Inspector can be configured through URL parameters or by injecting a global InspectorConfig object:

type InspectorConfig = {
  // Data Layer Configuration
  dataLayerRpcWsUrl: string | null // WebSocket URL for CLI integration
  dataLayerRpcParentUrl: string | null // Parent window URL for IFrame integration

  // Smart Items Configuration
  binIndexJsUrl: string | null // URL to smart items runtime (used for development of the @dcl/asset-packs module)
  disableSmartItems: boolean // Disable smart items functionality

  // Content Configuration
  contentUrl: string // URL for asset packs content

  // Analytics Configuration
  segmentKey: string | null // Segment.io write key
  segmentAppId: string | null // Application identifier
  segmentUserId: string | null // User identifier
  projectId: string | null // Current project identifier
}

URL Parameters

Example URL with parameters:

https://localhost:8000/?dataLayerRpcWsUrl=ws://127.0.0.1:8001/data-layer&disableSmartItems=true

Global Object

Example global configuration:

globalThis.InspectorConfig = {
  dataLayerRpcWsUrl: "ws://127.0.0.1:8001/data-layer",
  contentUrl: "https://builder-items.decentraland.org"
}

The configuration is resolved in the following order:

  1. URL parameters
  2. Global object
  3. Default values

Real-World Integration Examples

This section provides concrete examples of how different applications have integrated the Inspector, demonstrating the flexibility of its architecture. Each example showcases a different integration approach, from CLI-based development environments to web and desktop applications, illustrating how the Inspector's modular design accommodates various use cases.

VSCode Extension

The VSCode extension is a separate product that:

Web Editor

The Web Editor integration:

Creators Hub

The Creators Hub integration:

Consequences

Positive

Negative

References

License

Copyright and related rights waived via CC0-1.0. Final