Skip to content

Client SDK Reference

js
import { ... } from '@vibelands/plugin-sdk/client';

The only host import allowed in plugin client entries, alongside the shared singletons (react, react-dom, three, @react-three/fiber, @preact/signals-react) and the curated @vibelands/plugin-sdk/physics surface. Public marketplace plugins declare entries.client; the hosted builder rewrites shared singleton imports to the host registry and the marketplace review flow gates releases. All hooks require being mounted by the host - they throw outside plugin surfaces.

Physics (apiVersion 2)

jsx
import { DynamicBox, ReplicatedDynamicBox, StaticBox, KinematicBox, usePhysicsRaycast } from '@vibelands/plugin-sdk/physics';

The physics module requires physics.colliders. It exposes engine-neutral static, kinematic, and host-owned dynamic collider components (StaticBox, StaticBall, StaticMesh, Heightfield, KinematicBox, DynamicBox) and a closest-hit usePhysicsRaycast() hook. DynamicBox synchronizes its visual children to a client-only body without exposing the mutable body handle. For shared gameplay, spawn a server entity with physics.type: 'dynamic-box' and render it as <ReplicatedDynamicBox entityId={id}>…</ReplicatedDynamicBox>; the host integrates locally between authoritative 10 Hz snapshots and reconciles without per-snapshot React rendering. It deliberately does not expose the host world, mutable body handles, engine internals, or lifecycle methods.

Surfaces

jsx
export default definePluginClient({
  WorldLayer,   // R3F component inside the world scene
  HudPanel,     // DOM component inside the HUD overlay (wrapped in <div data-plugin-hud="<id>">)
  objectRenderers: {
    'my-item': MyItemRenderer,  // one R3F component per manifest editorItems id
  },
  objectBatchRenderers: {
    'my-item': MyItemBatchRenderer, // optional gameplay batch renderer for many visible objects
  },
});

At least one surface is required. HudPanel positions itself with absolute styles; use pointerEvents: 'none' unless you need input.

objectRenderers components render every placed world object with preset plugin:<pluginId>:<itemId> and receive { itemId, properties, worldObject, renderMode }. renderMode is 'world' | 'ghost' | 'library' - skip gameplay side effects when it isn't 'world'. The host automatically applies ghost preview opacity/tint around plugin object renderers, so simple renderers do not need to handle preview materials. All SDK hooks work inside renderers. See the manifest editorItems reference.

objectBatchRenderers are optional gameplay renderers for dense repeated objects. They receive { itemId, objects }, where objects are active visible world objects for that editor item. Use them for InstancedMesh batching; keep objectRenderers for editor previews, library thumbnails, ghost placement and fallback rendering.

World reads

HookReturns
useWorldInfo(){ worldId, worldName, terrain } (terrain: seed, size, seaLevel, biome, …)
useTerrainSampler()getGroundHeight(x, z) - the host's cached terrain height sampler
useAtmosphereFrame()ref to the live atmosphere frame state - read .current inside frame callbacks: daylight, weatherType, weatherIntensity, timeOfDay, sun direction
useWeather()replicated { type, intensity } (re-renders on change)
useTimeOfDayGetter()() => number (0..1, 0.5 = noon) - a getter, frame-safe, never re-renders
useGraphicsQuality()'low' | 'balanced' | 'high' - scale your content like core systems

Players

HookReturns
useLocalPlayerGetter()() => localPlayerState | null (server-echoed, ~20 Hz)
usePlayersLite()snapshot array { sessionId, userId, name, level, mode, dead, position } - re-renders only on join/leave; positions are frozen at render time
usePlayerGetter()(sessionId) => live player state | null - frame-safe peek for animations
useLocalAvatarProximity(target, opts)nearby result for a world-space point or XZ bounds; follows the player or driven vehicle
useNearestWorldObject(preset, opts){ object, player, distanceSq } | null for nearby-object prompts; opts supports range, yRange, requireMode, pollMs

Position-following content

Subscribe structurally with usePlayersLite(), then read live positions each frame through usePlayerGetter() inside usePluginFrame - never re-render per movement.

Plugin scope

HookReturns
usePluginConfig()per-world config, already resolved against your configSchema
usePluginPermissions()the permission ids your manifest declared
useDeterministicRng(salt?)mulberry32 seeded from worldSeed ^ hash(pluginId + salt) - identical sequence on every client

Replication (tier B)

HookReturns
usePluginState()parsed KV blob set by ctx.state.set() on the server
usePluginEntityList()live array of this plugin's entities: { id, kind, position, rotation, scale, velocity, properties, ownerSessionId } - narrow subscription, re-renders on add/remove/update
usePluginEntityIds()stable id array; re-renders only when entities are added/removed, suited to large physics structures
usePluginEntity(id)one live entity snapshot with an entity-scoped subscription
usePluginMessage(type, handler)subscribe to server→client messages
useSendPluginMessage()(type, data) => void client→server (host rate-limits: 20/s, burst 40, ≤ 8 KB)

Assets

HookReturns
usePluginAssetUrl(path)resolved /plugins/<id>/<version>/assets/<path> URL
usePluginGltf(path)loaded GLTF (suspends; cached per URL)
usePluginTexture(path)loaded THREE.Texture (suspends)
usePluginAudioBuffer(path)AudioBuffer | null, decoded with the host audio context - requires audio

Audio

js
const audio = usePluginAudio();   // requires the 'audio' permission
audio.getAudioContext()           // host AudioContext (null without permission)
audio.getMasterGain()             // ← connect ALL output here (interior/underwater filters)
audio.resumeAudioContext()
audio.isAudioEnabled()            // user/perf toggle - respect it

Without the audio permission you get a disabled stub and a one-time console warning - your plugin keeps working, silently.

HUD notifications

Use the host notification center instead of positioning routine hints or toast messages inside HudPanel.

jsx
const notifications = usePluginNotifications();

notifications.set('nearby-shop', {
  tone: 'info',
  title: 'Shop',
  message: 'Browse the nearby store.',
  actions: [{ key: 'E', label: 'open' }],
});
notifications.clear('nearby-shop');

notifications.notify({
  tone: 'success',
  title: 'Purchase complete',
  message: 'The item was added to your inventory.',
}, { durationMs: 3000 });

set(id, notice) creates or updates a persistent keyed notice. clear(id) removes it. notify(notice, options?) creates a temporary notice and returns an id that can be removed early with dismiss(id). The host owns placement, styling, timeout limits, and cleanup when the plugin unmounts.

HUD key actions

jsx
usePluginKeyAction({
  code: 'KeyF',
  enabled: Boolean(nearby?.object),
  onAction(event) {
    event.preventDefault();
    send('pressBox', { objectId: nearby.object.id });
  },
});

The helper listens on window, ignores repeated keydowns and text-entry targets by default, and cleans up when the HUD surface unmounts. Host gameplay handlers run first: if core code claims the key with preventDefault(), the plugin action is skipped. This keeps vehicle and other core interactions above nearby plugin actions when they share a key.

Frame loop

js
usePluginFrame((state, delta, frame) => { ... }, priority?)

useFrame with two host services: per-plugin frame cost attribution (debug panel + frameMetrics()), and error containment (after 50 thrown errors your callback is disabled instead of breaking the render loop).

Utilities

ExportPurpose
useSignalValue(signal)subscribe to a Preact signal (module-scope state shared between WorldLayer ↔ HudPanel)
createMulberry32(seed), hashStringToSeed(str)deterministic helpers
PLUGIN_LIMITS, PLUGIN_API_VERSIONthe contract constants

VibeLands Creator - Plugin SDK apiVersion 2