Client SDK Reference
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)
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
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
| Hook | Returns |
|---|---|
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
| Hook | Returns |
|---|---|
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
| Hook | Returns |
|---|---|
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)
| Hook | Returns |
|---|---|
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
| Hook | Returns |
|---|---|
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
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 itWithout 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.
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
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
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
| Export | Purpose |
|---|---|
useSignalValue(signal) | subscribe to a Preact signal (module-scope state shared between WorldLayer ↔ HudPanel) |
createMulberry32(seed), hashStringToSeed(str) | deterministic helpers |
PLUGIN_LIMITS, PLUGIN_API_VERSION | the contract constants |