JavaScript SDK
A custom app or widget runs in an iframe. Reaching its own Lua works without RoadVR, and being reached from its own Lua works throughsendToRoadVrApp. The
SDK is the third direction: the page asking RoadVR itself what is going on,
and being told when it changes.
Loading it
public/ belongs in it. There
is nothing to add to your own files {} block — the file is served by RoadVR,
which means there is no copy in your resource that can go stale against a
newer headset.
Use the
https://cfx-nui- form, not nui://. Both reach the same file, but
the address decides what your page is standing on: a browser sends a
Referer only from an http(s) document, so a page loaded over nui://
reaches every service on the web without one — and a service that checks it,
YouTube being the loud example, refuses to serve you. The cfx-nui- form
gives your page a real web origin, which is also what makes fetch and
browser storage behave the way you expect.The global is
RoadVr, matching the resource name. The product is written
RoadVR; the identifier is not.Getting started
Working examples
Three complete resources ship in
examples/: roadvr_exampleapp,
roadvr_examplewidget and roadvr_mediaapp — the last one is the Media app
itself, and it uses watch('placement') and setActions() for real.RoadVr.ready()
Promise<context>
Settles once the headset has answered, with the context object below.
surface: null and usable defaults — rather than leaving
your page waiting forever.
Calling it twice is free: once it has settled it resolves immediately.
RoadVr.context
The same objectready() resolves with, kept up to date. Every event updates
it before the handlers run, so a handler that would rather read the whole
picture finds it already current.
string | null
The SDK contract version, not the resource version. It moves when a field is
added, so you can check whether the headset you happen to be running in knows
about something.
null means nothing answered.string | null
'app', 'widget', or null outside a headset.object | null
Apps only.
width and height are metres, not pixels — your page always
lays out at 1280×800 CSS pixels however large the window stands in the world.
Reading these as pixels gives you a layout that changes with window size.anchor is 'body' (follows the player) or 'world' (stays put).object | null
Widgets only.
size is metres of width — one number, because the height
follows from aspect. mount is 'recess' or 'raised'.object
light is a boolean, accent a hex colour, iconTheme the icon set name.
palette is RoadVR’s own CSS custom properties — see
applyTheme.object
language is the player’s own interface choice, not the server’s
Config.Locale. Those are two different settings, and mixing them up shows
German text to a player who set English.object
state is off, booting, passthrough, focus or shutdown. focus is
the mode in which the player can actually click things.focused is this window being the focused one, not some window being
focused.visible is false while the pause menu, the map or the cinema screen is up,
and during a fast head turn. One field for all four, because the answer is
the same in all four: stop animating.object | null
null until you call watch('world'). See Weather and the
clock.RoadVr.on()
string
required
One of the events below.
function
required
Receives the changed subtree, not the whole context.
function
The counterpart. Calling it removes this one handler.
The events
message and the raw listener
RoadVr.on('message', …) and a plain window.addEventListener('message', …)
carry the same pushes from your own Lua. The difference is that the SDK keeps
its own traffic out of the first one; a raw listener sees both and has to tell
them apart itself.
Everything RoadVR forwards carries source: 'roadvr'. SDK traffic additionally
carries channel: 'sdk'.
RoadVr.applyTheme()
HTMLElement
default:"document.documentElement"
Where to write the custom properties.
ready().
The names are the ones the interface uses itself: --vr-text,
--vr-text-dim, --vr-text-3, --vr-glass-top, --vr-glass-bottom,
--vr-chrome-top, --vr-chrome-bottom, --vr-separator, --vr-stroke,
--fill-idle, --fill-hover, --fill-selected, --on-selected,
--view-recessed, plus --vr-accent.
Point your own variables at them with a fallback and the page still opens in a
browser:
Watched topics
Two things are not sent unless you ask for them, because both tick and a ticker running for a page that never reads it is load nobody chose. Each one starts with its first watcher and stops with its last.Weather and the clock
weather, temperature, hours,
minutes, windSpeed, windBearing, sunrise, sunset, day, month,
year and weekday. RoadVr.context.world is null until you watch it.
How far away your window is
Apps only — a widget hangs in a wall and does not move.distance is metres from the player’s head to your window, four times a second
and only while it actually changes. It is what the Media app turns its volume
down with as you walk away, and what a display carrying too much detail to read
from across the room can simplify itself on.
Nothing else about where anybody is standing travels — not the window’s
position and not the player’s. One number is the whole answer to “am I near
it”; the position would be a different question with a different answer.
A frame that goes away without calling
unwatch still stops counting — a
ticker does not stay on for a window nobody can see.RoadVr.setActions()
Puts buttons in your own window’s ornament — the bar along the bottom edge that already holds the close button, the grabber and the tilt. Yours go to the left of the close button.array
required
Up to three items of
{ id, label, icon, onSelect }. An empty array takes
them away again.string
Yours. It comes back as the payload of the
action event, and it is how the
SDK finds your onSelect again.string
Finished text, not a locale key. Your page carries its own strings and
RoadVR does not translate them — switch them yourself on the
locale event
and call setActions again.string
Either a name from RoadVR’s own bundled set, spelled
solar:alt-arrow-left-bold, or an SVG path in a 24×24 box. A name gets you
the same filled glyph the close button beside it uses, but only for the icons
RoadVR happens to bundle — a name it does not have logs a warning and draws
nothing. A path always works and is drawn as an outline.function
Runs in your page. The press travels as the id and the SDK finds your
function again, so a second
setActions with a shorter list cannot leave a
handler behind for a button that is gone.RoadVr.close()
Closes your own window.It does nothing for a widget. A widget hangs in a wall and belongs to the
player; taking one off is theirs to do.
What the SDK cannot do
By design, this version is read plus events, plus two actions — closing your own window and hanging buttons in its ornament. There is no way to play a sound, post a notification into the control centre, resize your own window, or open another app. Nor does it hand out game data — vehicle, health, position and the rest belong in your own Lua, and RoadVR is not a general game API. The message protocol is built so those can be added later without breaking anything already written against it.Related
Custom Apps
Getting your page onto the home screen
Custom Widgets
Getting your page onto a wall
Client API
The Lua half
Discord Support
Questions about integrating