@whatever-engine/api
v0.8.0
Published
TypeScript scripting API for Whatever Engine mods
Readme
@whatever-engine/api
The engine embeds this SDK, a TypeScript compiler and isolated QuickJS Wasm VMs. Mods distribute JS/TS/JSON source, compiled on the destination machine by the embedded compiler. The npm package supplies development types. Native queries and scene mutations execute synchronously inside the engine.
Usage
import { Engine, Input, Time, Window, File, Scene, Entity, Physics, Mods, Message, Console, Audio } from "@whatever-engine/api";
Engine.on("init", ({ mod_id }) => {
Engine.log("info", `loaded as ${mod_id}`);
});
Engine.on("tick", () => {
if (Input.isPressed("KeyW")) {
// Use Time.deltaSeconds when recording movement commands.
}
});The engine-owned VM contains this API and resolves @whatever-engine/api before loading the mod. No runtime install or workspace ancestry is required. Leave this import external when bundling mods. The npm package remains available for editor types.
Mods may import local JS/TS/JSON source inside their own directory. Use the engine API for file I/O and logging. See docs/SANDBOX.md for the import and capability allowlists and build instructions.
API
Component reference fields
session: RunSession | null = null— infer a reference to a concrete mod-local component; call its methods directlyType.Component(RunSession)— explicit descriptor, usable withNullable,ArrayandStruct
Experimental engine backend and build instructions: docs/EMBEDDED_SCRIPTING.md.
Prefab
Prefab.spawn(path)— create an independent hierarchy immediately; returnsPrefabInstancePrefabInstance.root,.get(scriptId),.entities()— access the fresh root and its local named descendants
Prefab JSON and library imports: docs/ASSETS.md.
Engine
Engine.mode— read-only"client" | "server", available before top-level evaluationEngine.on(event, handler)— subscribe to an engine event (init,exit,tick,mod_message,physics)Engine.log(level, message)— log through the engine logger ("info","warn","error")Engine.setTickRate(ticks_per_second)— override the game tick rate at runtimeEngine.setFpsCap(fps | null)— set FPS cap;null= uncappedEngine.setVsync(enabled)— enable or disable vertical syncEngine.setMainCamera(entityOrId)— designate a live or pending entity as the active cameraEngine.getMainCamera()→Entity | nullEngine.requestMainCamera()→Pending<Entity | null>Engine.clearCamera()— remove the active camera (screen goes black with a warning)
Tick payloads include mouse_captured; mouse_delta is [0, 0] while uncaptured.
Pending mouse motion is discarded on release, including for busy async handlers.
Input
Read-only input for the current tick, available in component hooks and any other function without a tick listener.
Input.keysPressed→PressedKeysInput.isPressed(key: KeyCode)→booleanInput.isAnyPressed(...keys: KeyCode[])→booleanInput.areAllPressed(...keys: KeyCode[])→booleanInput.isShiftPressed()/Input.isControlPressed()→booleanInput.isAltPressed()/Input.isSuperPressed()→booleanInput.mouseDelta→readonly [number, number]Input.mouseCaptured→boolean
Time
Read-only timing for the current tick, available throughout the mod.
Time.tickNumber→numberTime.deltaSeconds→number
PressedKeys / KeyCode
Input.keysPressed and tick keys_pressed expose immutable held-key snapshots. KeyCode
provides autocomplete for supported key names. Existing .includes() checks work.
keys.isPressed(key: KeyCode)→booleankeys.isAnyPressed(...keys: KeyCode[])→booleankeys.areAllPressed(...keys: KeyCode[])→booleankeys.isShiftPressed()/keys.isControlPressed()→booleankeys.isAltPressed()/keys.isSuperPressed()→booleankeys.includes(key: KeyCode)→booleankeys.size→numberkeys.toArray()/keys.toJSON()→readonly KeyCode[][...keys]→KeyCode[]new PressedKeys(codes?: readonly KeyCode[])→PressedKeys
Profiler
Profiler.now()→ monotonic millisecondsProfiler.latest()→DeepReadonly<TickTimings> | nullProfiler.runtime()→ previous VM tick/script timings (scene-replication fields remain zero)Profiler.memory()→ read-only Wasm memory bytesSceneSnapshot.revision,.has(id),.get(id),.entities()
Window
Window.setTitle(title)— change the window titleWindow.setSize(width, height)— request a new inner size in physical pixels (no-op in fullscreen modes)Window.setMode(mode)— set display mode:"windowed","borderless", or"fullscreen"
File
Sandboxed per-mod file I/O. Paths must not contain ...
File.write(path, data)→Promise<void>File.read(path)→Promise<string>File.delete(path)→Promise<void>
Pending<T> / PendingEntity
pending.get()→Promise<T>— wait for the recorded result; never flushes or repeats workPendingEntity— an entity reference with chainable mutations before its native ID exists; reads are synchronous; explicitrequest*methods returnPending<T>
Scene/physics calls execute immediately. Entity creation only allocates a compatibility result Promise if .get() is used. See tick timing.
Component
class MyComponent extends Component— exported classes define custom componentsstatic readonly type = "mod_id:name"— optional stable identity; defaults tomod_id:ClassNamecomponent.entity→Entity;component.id→ component type IDstart()/tick(deltaSeconds)→void | Promise<void>onEnable()/onDisable()/destroy()→voidfield(type, default, { label?, tooltip? })→ inferred field value
Shared schemas
Type.I32/F64/VarInt.with({ min?, max? })Type.Bool,Type.String.with({ maxLength? })Type.Array(element, { maxLength? }),Type.Optional(inner),Struct(fields)Type.Nullable(inner),Type.Enum(choices),Type.Tuple(...elements)Type.Entity,Type.AssetInfer<typeof descriptor>,DataType<T>,ComponentClass<T>,ComponentData<T>,FieldEditor
Entity
A live entity in the scene. Returned by Scene.createEntity, Scene.listEntities, Scene.query, and Scene.spawnSprite. Native component IDs and custom component classes infer their return types.
entity.id— generation-aware ID; provisional until creation is confirmedentity.exists,entity.isPending,entity.nameentity.hasComponent(kind)→booleanentity.getActive()→booleanentity.editComponent(kind, callback)→thisentity.setEulerDegrees(x, y, z)→thisentity.requestComponent(kind)→Pending<T | null>entity.requestParent()→Pending<Entity | null>entity.requestChildren()→Pending<Entity[]>entity.destroy()— fire-and-forgetentity.setComponent(component)→this— registered native/custom class instances onlyentity.removeComponent(componentIdOrClass)→thisentity.getComponent(componentIdOrClass)→ComponentSnapshot<T> | null— typed for built-in componentsentity.move(position)→this— updatecore:transformposition, preserve rotation/scaleentity.setParent(parent | null)→this— accepts entities, references or IDsentity.getParent()→Entity | null— return the parent entityentity.getChildren()→readonly Entity[]— return all direct children
EntityRef / EntityReference
new EntityRef(entityOrId)/Scene.ref(entityOrId)— typed references in nested component datareference.id— opaque ID, provisional until its target is confirmedComponentValue— component JSON with typed entity referencesentity.addTag(tag)/entity.removeTag(tag)→thisentity.getTags()→readonly string[]entity.hasTag(tag)→boolean
Scene
Entity and component management. Methods that accept a raw entity_id string are provided for cases where only an ID is available; prefer Entity methods when possible.
Scene.revision→numberScene.find(id)→Entity | nullScene.snapshot()→SceneSnapshotScene.requestComponent(id, kind)→Pending<T | null>Scene.requestQuery(kinds)→Pending<QueryResult<Entity>[]>Scene.createEntity()→PendingEntityScene.loadScene(path)→LoadedSceneScene.ref(entityOrId)→EntityRefScene.destroyEntity(entity_id)— fire-and-forgetScene.listEntities()→readonly Entity[]Scene.removeComponent(entity_id, component_type)— fire-and-forgetScene.getComponent(entity_id, component_type)→ComponentSnapshot<T> | null— typed for built-in componentsScene.findByTag(tag, { includeDisabled? })→readonly Entity[]Scene.findByTags({ all?, any?, none?, includeDisabled? })→readonly Entity[]Scene.query(componentIdsOrClasses)→readonly { entity: Entity; components: Readonly<Record<string, any>> }[]— each row hasentity: EntityandcomponentsScene.spawnSprite(texture, shader, position, scale?)→PendingEntity— convenienceScene.spawnMesh(mesh, shader, position, options?)→PendingEntity— convenienceScene.spawnText(text, position, options)→PendingEntity— convenienceScene.moveEntity(entity_id, position)→void— convenienceScene.addAmbientLight(color?, intensity?)→PendingEntity— convenienceScene.addDirectionalLight(direction, color?, intensity?)→PendingEntity— convenienceScene.addPointLight(position, color?, intensity?, range?)→PendingEntity— convenienceScene.setParent(entity_id, parent_id | null)— fire-and-forgetScene.getParent(entity_id)→string | nullScene.getChildren(entity_id)→readonly string[]
Built-in component types: core:transform, core:sprite_renderer, core:mesh_renderer, core:text_renderer, core:camera, core:ambient_light, core:directional_light, core:point_light. getComponent returns live fields and class helpers; retained handles stay live across turns. Field assignments, nested edits and component removal cleanup take effect immediately. editComponent remains a compatibility helper.
Entity and component reads fetch only the requested data. Repeated reads within a turn reuse cached data until an engine write affects it.
LoadedScene
scene.get(scriptId)→Entityscene.entities()→Entity[]scene.entityCount→numberscene.loadTime→number(milliseconds)
BuiltInComponents.Transform
Methods (all setters chainable, return this):
getX/Y/Z(),setX/Y/Z(v),addX/Y/Z(v)— individual position componentsgetPosition()→[x, y, z],setPosition(x, y, z)— full positiongetScaleX/Y/Z(),setScaleX/Y/Z(v)— individual scale componentsgetScale()→[x, y, z],setScale(x, y, z),setScaleUniform(s)— full scalegetRotation()→[x, y, z, w],setRotation(x, y, z, w)— raw quaterniongetEulerRadians/Degrees()→[rx, ry, rz]— intrinsic XYZ decompositionsetEulerRadians/Degrees(rx, ry, rz)— set from intrinsic XYZ Euler anglesrotateX/Y/Z(degrees)— incremental world-space rotation around axisdistance(other)→number— Euclidean distance between positions
BuiltInComponents.SpriteRenderer
getTexture(),setTexture(path)— VFS texture pathgetShader(),setShader(path)— explicit VFS shader path (seedocs/SHADER.md)
BuiltInComponents.MeshRenderer
getMesh(),setMesh(path)— VFS mesh path (.json,.obj,.glb,.gltf)getShader(),setShader(path)— explicit VFS shader path (seedocs/SHADER.md)getTexture(),setTexture(path | null)— VFS texture path;null= 1×1 white fallback
BuiltInComponents.TextRenderer
getText(),setText(text)— displayed stringgetFont(),setFont(path)— explicit VFS path to a TTF/OTF fontgetFontSize(),setFontSize(size)— font size in logical pixels (default:24)getColor(),setColor(r, g, b, a)— RGBA colour, each channel[0.0, 1.0](default: white)
BuiltInComponents.Camera
Makes an entity usable as a scene camera. Requires core:transform on the same entity. Activate with Engine.setMainCamera(entity.id).
getFov(),setFov(degrees)— vertical FOV in degrees (default:45)getZNear(),setZNear(v)— near clip plane (default:0.1)getZFar(),setZFar(v)— far clip plane (default:1000)
BuiltInComponents.AmbientLight
Uniform fill light. Multiple ambient lights are additive. No position needed.
getColor(),setColor(r, g, b)— RGB colour, channels in[0.0, 1.0](default: white)getIntensity(),setIntensity(v)— multiplier (default:0.1)
BuiltInComponents.DirectionalLight
Infinitely distant light (sun-like). Max 4 active simultaneously.
getDirection(),setDirection(x, y, z)— normalised world-space direction (default:[0, -1, 0])getColor(),setColor(r, g, b)— RGB colour (default: white)getIntensity(),setIntensity(v)— multiplier (default:1.0)
BuiltInComponents.PointLight
Omnidirectional point light. Position comes from core:transform. Max 8 active simultaneously.
getColor(),setColor(r, g, b)— RGB colour (default: white)getIntensity(),setIntensity(v)— multiplier (default:1.0)getRange(),setRange(v)— attenuation radius in world units (default:10)
Physics
Native Rapier 3D; see the full physics reference.
new Physics.World(options?)—core:physics_worldPhysics.defaultWorld.get()→Promise<Entity>;Physics.capabilitiesPhysics.RigidBody.dynamic(options?),.fixed(options?),.kinematicPosition(options?),.kinematicVelocity(options?)—core:rigid_bodynew Physics.Collider(options)—core:colliderPhysics.Joint.fixed(options),.revolute(options),.prismatic(options),.spherical(options),.generic(options)—core:jointPhysics.Shape.sphere(radius),.box(options),.capsule(options),.cylinder(options),.cone(options),.plane(normal?),.convexHull(points),.triMesh(vertices, indices, flags?),.compound(parts)Physics.body(entityOrId)→BodyHandlebody.applyImpulse(vector, options?),.applyTorqueImpulse(vector, wake?),.addForce(vector, options?),.addTorque(vector, wake?)body.resetForces(wake?),.resetTorques(wake?),.setLinearVelocity(vector, options?),.setAngularVelocity(vector, wake?)body.teleport(position, options?),.setKinematicTarget(position, rotation?),.sleep(),.wake()body.get()→Promise<void>;body.state()→Physics.BodyStatePhysics.world(entityOrId?)→WorldHandle;world.castRay(origin, direction, options)→Physics.RayHit | nullPhysics.onEvent(handler);Physics.QueryFilterFlag,Physics.CollisionType,Physics.TriMeshFlag- Read-only ECS views:
core:rigid_body_state/Physics.BodyState,core:physics_status/Physics.Status - Typed expert settings:
Physics.IntegrationOptions,BodyRapierOptions,ColliderRapierOptions,JointRapierOptions
Audio
Play audio files loaded from the VFS. Supported formats: WAV, OGG/Vorbis, MP3.
Audio.play({ path, seek?, speed?, volume? })— fire-and-forget; engine frees the handle automatically when playback endsAudio.load({ path, play?, closeStrategy?, volume?, speed?, loop? })→Promise<AudioHandle>— load and optionally start playback; returns a controllable handle
AudioHandle methods:
handle.isStopped()—boolean(sync):trueafterstop()or engine-initiated closehandle.play(opts?)→Promise<number>— resume playback; returns position in ms;optscan overridevolumeandspeedhandle.pause()→Promise<number>— pause; returns position in mshandle.stop()— void (sync): stop and free the handle; firesclosehandlers immediatelyhandle.position()→Promise<number>— current position in mshandle.isPlaying()→Promise<boolean>handle.volume()→Promise<number>handle.speed()→Promise<number>handle.isLooping()→Promise<boolean>handle.metadata()→Promise<AudioMetadata>—{ duration_ms, sample_rate, channels }; cached after first callhandle.seekTo(ms)→Promise<number>— seek to absolute position; returns previous positionhandle.seek(offsetMs)→Promise<number>— seek forward/backward; returns new position; clamped to[0, duration-1], pauses if past endhandle.loop(enabled)— void (sync): enable or disable looping; resets position to 0 when changedhandle.on("close", fn)— fired on both manualstop()and engine-initiated close
CloseStrategy: "Auto" (default) — engine frees handle when playback ends; "Manual" — mod must call stop().
Mods
Mods.list()→Promise<ModManifest[]>— all loaded mods in load orderMods.get(id)→Promise<ModManifest>— manifest for a specific mod
Message
Inter-mod communication. Payloads must be JSON-serializable.
Message.sendAndForget(id, message)— fire-and-forgetMessage.send(id, message, timeout)→Promise<JsonValue>— send and await a replyMessage.registerMessageHandler(handler)— handle incoming messages; return a value to reply
Console
Unique commands use plain names. Collisions qualify all participants as mod_id:name (built-ins use core:name). Duplicate commands within one namespace, including a core mod clashing with a built-in, stop loading.
Console.register(spec)— register developer console commands with typed args and subcommands. Argument types:string,int,float,bool,mod_id,entity_id. The engine completes and validates loaded mod IDs and living entity IDs; handlers receive ID strings.boolsuggestsfalse/trueby default. Open the console with F2; Up/Down previews suggestions and Tab accepts.
Network
Network.Type—I32,F64, signed 32-bit ZigZagVarInt,Bool,String,Array(type, options?),Optional(type)Network.Type.I32/F64/VarInt.with({ min?, max? })— immutable numeric constraintsNetwork.Type.String.with({ maxLength })— UTF-8 byte limit; arraymaxLengthcounts elementsNetwork.Struct(fields)/Network.Packet(fields)/Network.Specification(id, states)— immutable definitionsNetwork.Infer<typeof descriptor>— inferred payload/client-data typeNetwork.registerClient(spec)/Network.registerServer(spec, { clientData, initialClientData })— await native validation/registrationregistration.packets/id/specificationId/fingerprint— engine-assigned metadataregistration.encodePacket(name, payload)/decodePacket(bytes)— native offline codecs; packet ID and payload, no stream length prefixregistration.on(name, handler)— typed receive subscription; returns unsubscriberegistration.send(...)/server.broadcast(...)— validated bounded-queue sends; optional{transport: "stream" | "datagram"}server.onConnect(handler)/onDisconnect(handler)/clients/getClient(id)— typed connection lifecycle and owned clientsserver.validateClientData(data)— offline native validation/copyclient.data/client.flushData()— typed per-peer mutation proxy and native update acknowledgementclient.connect({address, trust?: "strict" | "tofu"})/disconnect()/connected/connection— QUIC lifecycle; strict by defaultclient.onConnect(handler)/onDisconnect(handler)— lifecycle subscriptionsserver.listen({address, maxClients?})/stop()/listening/listener— native QUIC listenerserver.disconnect(client, reason?)— disconnect an owned peerNetwork.PacketContext— receive transport metadata (client second argument, server third)Network.ConnectOptions/ListenOptions/SendOptions/TrustPolicy— public option typesregistration.close()— disconnect peers and release native registration
Development
# Regenerate type declarations after editing index.ts
bun run build:typesFull API documentation is in docs/SCRIPTING.md at the repo root.
The private Wasm boundary exposes checked engine operations. Mods receive no OS handles or process objects.
Init payloads include mode. Simulation ticks run independently of rendering. Window/audio operations and presentation settings reject server use. Mod manifests expose supported sides and optional shared/client/server script entries; only mods active on the current side appear in Mods.list() and Mods.get().
Tick failures and budget traps stop the affected VM. Healthy mods and engine shutdown continue.
Entity lifecycle and startup scenes
Entity.clone(options?: CloneOptions): PendingEntityEntity.setEnabled(enabled: boolean): thisEntity.getEnabled(): booleanScene.initialScene(): LoadedScene | nullLoadedScene.path: stringPhysics.Joint.fixed({ connectedBody, ...options }): Physics.JointPhysics.Joint.revolute({ connectedBody, ...options }): Physics.JointPhysics.Joint.prismatic({ connectedBody, ...options }): Physics.JointPhysics.Joint.spherical({ connectedBody, ...options }): Physics.JointPhysics.Joint.generic({ connectedBody, ...options }): Physics.JointEntity.getComponent("core:world_transform"): readonly transform data | null— read-only inherited transform
