Skip to content
⌘K

For game developers

How web games run in Rare OS

The sandboxed window each web game gets in Rare OS, the injected Rare Wallet, controller input modes, per-game profiles and the on-screen keyboard.

Rare OS treats a browser game like an installed game. It gets its own fullscreen window, its own storage, controller support, an on-screen keyboard and Rare Wallet, without any change to your code. This page explains what your page sees in that window and how to make the most of it.

The game window

Each web game opens in its own webview window. On Rare OS the engine is WebKitGTK, so a page that works in Safari is the closest match.

What happens
AddressesOnly https URLs open. The one exception is a page on localhost.
StorageEach game gets its own storage profile: cookies, localStorage and IndexedDB are kept per game and never shared with other games or with the launcher. They persist between sessions.
Native accessNone. The page gets no launcher or system API. Rare's runtime is a plain script injected into the page.
NavigationThe page can navigate and redirect freely inside its window. The rare-wallet:// scheme is reserved for Rare Wallet requests.
Pop-upsThe runtime adds no handling for new windows. Use same-window redirects for sign-in and payment flows rather than window.open or target="_blank".
SizeFullscreen on the device. On a desktop the window takes the launcher window's size.
Guide buttonHides the game and returns to the launcher. The game keeps running; Resume brings it back and Quit game closes it.

Rare's runtime is injected before your page's own scripts run, so the wallet and the controller are ready by the time your code loads.

Chrome fallback

A game that needs something WebKitGTK lacks (WebGPU, some codecs, Chrome-only APIs) can be opened in a Chromium browser instead, as a fullscreen app window with its own profile per game. Every web game's page in the launcher also has Open in Google Chrome. In that mode the game runs in the browser as is: Rare's injected runtime (wallet, controller mapping, keyboard) is not part of it. No listed game needs the fallback today.

Rare Wallet

Rare Wallet registers itself in every game window as a Wallet Standard wallet. Any Sui wallet adapter that discovers Wallet Standard wallets (dapp-kit and others) lists it next to the wallets it already knows.

PropertyValue
nameRare Wallet
version1.0.0
chains["sui:mainnet"]
iconAn SVG data URI

Features

FeatureVersionNotes
standard:connect1.0.0Asks the player to connect. Returns one account.
standard:disconnect1.0.0Clears the account in the page.
standard:events1.0.0change events when accounts change.
sui:signTransaction2.0.0Signs without submitting.
sui:signAndExecuteTransaction2.0.0Signs and submits. Returns bytes, signature, digest and effects (base64 BCS).
sui:signPersonalMessage1.1.0Returns bytes and signature.
sui:signTransactionBlock1.0.0Older name, kept for older dapp-kit pages.
sui:signAndExecuteTransactionBlock1.0.0Older name, kept for older dapp-kit pages.

Each account carries address, publicKey, label, chains: ["sui:mainnet"] and the five sui:* features above.

The wallet works on Sui mainnet only.

Connecting and signing

  1. The game asks to connect

    Rare shows a connect sheet with your game's title and its origin. The player picks one of their Rare Wallet accounts and allows or denies. An allowed origin is remembered on that device.

  2. The game asks for a signature

    Only an origin the player connected may ask for signatures. Every transaction and message opens Rare's review sheet: the transaction is simulated first, and the sheet shows what leaves the wallet, what arrives and the fee. Nothing is signed until the player approves with their PIN, passkey or hardware wallet.

  3. The game gets the result

    The promise resolves with the wallet's result, or rejects with an Error whose message says why.

While a sheet is open the controller drives the sheet, and focus goes back to the game when it closes.

Transactions can be passed as a Transaction (serialized with its toJSON()) or as base64 transaction bytes. If your transaction sets a sender, it must be the connected account.

Error messages your game can receive:

MessageCause
User rejected the requestThe player declined.
Connect this game to Rare Wallet firstA signing request before a connect from this origin.
Add an account to Rare Wallet firstThe player has no account yet.
This transaction is for a different accountThe transaction's sender is not the connected account.
This account is watch only. Add a signing account to continue.The selected account can't sign.

Call connect from a button press rather than on page load, so the player sees the connect sheet when they expect it.

Connect with dapp-kit

With dapp-kit's providers set up for mainnet, Rare Wallet shows up in ConnectButton's list. Signing works through the usual hooks:

TypeScript
import { ConnectButton, useCurrentAccount, useSignAndExecuteTransaction } from "@mysten/dapp-kit";
import { Transaction } from "@mysten/sui/transactions";

export function Claim() {
  const account = useCurrentAccount();
  const { mutateAsync: signAndExecute } = useSignAndExecuteTransaction();

  async function claim() {
    const tx = new Transaction();
    tx.moveCall({ target: "0xYOUR_PACKAGE::game::claim_reward", arguments: [] });
    const { digest } = await signAndExecute({ transaction: tx });
    console.log("claimed", digest);
  }

  return account ? <button onClick={claim}>Claim reward</button> : <ConnectButton />;
}

To list Rare Wallet first, pass preferredWallets={["Rare Wallet"]} to WalletProvider.

Connect with Wallet Standard directly

TypeScript
import { getWallets } from "@wallet-standard/app";

const wallets = getWallets();
const findRare = () => wallets.get().find((w) => w.name === "Rare Wallet");

let rare = findRare();
wallets.on("register", () => {
  rare ??= findRare();
});

export async function connectAndSign() {
  if (!rare) throw new Error("Rare Wallet is not available in this browser");

  const { accounts } = await (rare.features["standard:connect"] as any).connect();
  const account = accounts[0];

  const { bytes, signature } = await (rare.features["sui:signPersonalMessage"] as any).signPersonalMessage({
    account,
    message: new TextEncoder().encode("Sign in to My Game"),
  });
  return { address: account.address, bytes, signature };
}

Checking for a wallet named Rare Wallet is also how your page can tell it is running in Rare OS.

Controller input

Rare reads the controller natively and hands it to your page in one of three input modes. Real controllers are always hidden from the page, so input never arrives twice. The mode comes from your game's profile (see below); without a profile the game gets keys.

keys (default)

Buttons become keyboard events and the right stick moves an on-screen pointer.

ButtonKey
D-pad, left stickArrow keys
AEnter
BEscape
XSpace
YTab
LB / RBQ / E
LT / RTShift / Control
MenuEscape
ViewM
  • The right stick moves a pointer that sends pointermove and mousemove events, with a finer response near the centre. RB (or a right-stick click) clicks with pointerdown, mousedown, pointerup, mouseup and click. When the pointer is on, RB is the click and does not send its key.
  • With pointer lock, the stick sends movementX and movementY instead of moving the cursor.
  • The pointer hides after 4 seconds without movement.
  • Key events go to the focused element, or to the first <canvas> when nothing is focused, and bubble up. They carry key, code, keyCode and which.

gamepad

For games that read the Gamepad API. Rare exposes one virtual controller and sends no keys.

  • navigator.getGamepads() returns the virtual pad at index 0, after a gamepadconnected event.
  • mapping is "standard", with 17 buttons and 4 axes (left x, left y, right x, right y; down is positive). Triggers are analog.
  • Button order: 0 A, 1 B, 2 X, 3 Y, 4 LB, 5 RB, 6 LT, 7 RT, 8 View, 9 Menu, 10 L3, 11 R3, 12 to 15 d-pad up, down, left, right, 16 Guide.
  • id is Rare Controller (STANDARD GAMEPAD Vendor: 045e Product: 028e). There is no vibration actuator.

Poll getGamepads() every frame, as you would for any standard pad.

focus

For games built from DOM controls: menus, card games, word games. The d-pad moves a focus ring over your page's own buttons, links and inputs, and A presses the one under the ring.

  • The ring moves to the nearest usable control in the pressed direction. When there is none, up and down scroll the page.
  • It skips controls that are hidden, disabled, aria-hidden, nearly transparent or covered by something else (a modal above them).
  • The ring starts on the profile's focusStart selector, or else on the largest control near the centre of the screen, which is usually your Play button.
  • B sends Escape and Menu sends Enter, unless the profile maps them to other keys.

Use real <button>, <a href>, role="button" or tabindex elements for anything the player should reach.

Writing input code that works everywhere

  • Rare's events are synthetic, so event.isTrusted is false. Don't ignore untrusted events.
  • Listen on window or document as well as on your canvas: events bubble from the focused element.
  • Don't depend on a native default action from a synthetic key. An Enter key event does not submit a form by itself, so handle Enter in a keydown listener.
  • The device's touchscreen sends normal touch events.

Per-game profiles

A profile tells Rare where your game really plays and how the controller maps onto it. Profiles ship with Rare Launcher in launcher/src/games/profiles.json, keyed by your catalog slug (see Get your game listed). A game the player adds by URL picks up a profile through its hosts.

FieldTypeMeaning
titlestringDisplay name, for games that ship with the launcher.
playUrlstring or nullWhere the game plays. It wins over the catalog's playUrl and website. null means not playable.
hostsstring[]Hostnames that belong to this game. A www. prefix is ignored.
input"keys", "gamepad" or "focus"Input mode. Default "keys".
pointerbooleanRight-stick pointer and RB click. Default true.
keysobjectButton to key overrides, for example { "up": "w" }. Buttons: up, down, left, right, a, b, x, y, lb, rb, lt, rt, start (Menu), select (View). Values: a letter, a digit, Enter, Escape, " ", Tab, Backspace, Shift, Control, Alt or an arrow key name.
focusStartstring or string[]CSS selectors for where the focus ring starts (focus mode). The first that matches a usable control wins.
keyboard"keys"In keys mode, lets View open the on-screen keyboard as a key sender even when no text field is focused.
controlsstringOne plain line shown to players on the game's page.
browser"chrome"Open in the Chrome fallback instead.
unavailablestringWhy the game can't be played right now, shown instead of Play.
builtinbooleanShips pre-installed with Rare OS.
genre, descriptionstringDisplay text, for games that ship with the launcher.
nativeobjectFor games with a native build: store, storeUrl, steamAppIds?, titles?, how.

Example profile for a top-down web game played with WASD and the mouse:

profiles.json
{
  "games": {
    "my-game": {
      "playUrl": "https://play.mygame.example/",
      "hosts": ["play.mygame.example", "mygame.example"],
      "input": "keys",
      "pointer": true,
      "keys": { "up": "w", "down": "s", "left": "a", "right": "d", "x": "f" },
      "controls": "Left stick walks, right stick moves the pointer, RB clicks, X interacts."
    }
  }
}

And one for a word game played with its own on-screen letters:

profiles.json
{
  "games": {
    "suiordle": {
      "playUrl": "https://suiordle.com/",
      "hosts": ["suiordle.com", "www.suiordle.com"],
      "input": "focus",
      "focusStart": ["button[data-key=\"Q\"]"],
      "keys": { "x": "Backspace", "start": "Enter", "b": "Escape" },
      "controls": "D-pad picks a letter on the game's keyboard, A types it. X deletes, Menu enters the word. View opens Rare's keyboard to type faster."
    }
  }
}

Profiles are maintained by Rare. Tell us the mode and mapping your game needs when you get listed.

On-screen keyboard

Rare draws its own keyboard at the bottom of your page. It has a number row, a QWERTY layout with @ . - _, and Shift, Space, Del and Done keys. A types, X deletes, Y toggles Shift, Menu finishes and B closes it.

When it opens

  • A is pressed while a visible text field is focused (keys and gamepad modes).
  • A is pressed on a text field under the focus ring (focus mode).
  • The pointer clicks a text field, or the player taps one on the touchscreen.
  • View is pressed. In focus and gamepad modes View always opens it; in keys mode only when a text field is focused, or when the profile sets "keyboard": "keys".

A text field is an <input> (not a checkbox, radio, button, range, color, file, hidden, image or reset input), a <textarea> or a contenteditable element. It counts as visible when it is at least 40 by 14 pixels, not visibility: hidden and not nearly transparent. Hidden inputs that games keep focused to capture keys are ignored.

What it sends

  • Into a field: each key sets the field's value through the native value setter and fires an input event, so controlled inputs in React and other frameworks update. In a contenteditable element it inserts text. Done closes the keyboard and sends an Enter key press to the field.
  • With no field: it sends keydown and keyup for each key to the focused element or the page: letters, digits, Space, Backspace for Del and Enter for Done. This is how word games that listen to the keyboard get typed letters.

Make your text inputs work with it

  • Use real <input> or <textarea> elements, visible and at least 40 by 14 pixels.
  • Read the value on input events, not only on keydown. Typing into a field fires input events, not key events.
  • Submit on Enter from a keydown listener on the field, since the keyboard's Enter is a synthetic key press.
  • If your game reads typed letters from key events with no field, use focus mode or ask for "keyboard": "keys" so View opens the keyboard as a key sender.

Next steps

--
TPS:--