# Controller input

Send a game controller's state as bytes on a lossy channel. There is no controller layer and no
schema to install: you pack the struct on the client and unpack it on the host.

## Wire format

Pack a fixed struct with `DataView` and send it on a lossy channel. The host declares `maxHz`,
which paces the sender, and `latestOnly`, which drops a stale packet; the client names the channel.
Use a bitfield for the buttons and one `int16` per axis, in an order you pick.

The client joins with your client key: see
[Custom client pages](/docs/guides/client-page/#custom-client-pages).

```js
import { BitskiffClient } from '@bitskiff/client';

// buttons: a bitfield, button i is bit i. axes: an array of int16, a fixed order you pick.
function packPad(buttons, axes) {
  const bytes = new Uint8Array(4 + axes.length * 2);
  const v = new DataView(bytes.buffer);
  v.setUint32(0, buttons, true);
  axes.forEach((a, i) => v.setInt16(4 + i * 2, a, true));
  return bytes;
}

const client = new BitskiffClient({ key: 'bsk_pk_...' });   // your client key
const pad = client.channel('move');           // the host's row makes it lossy at 60 Hz; name, then join
await client.join('1CAbCdEfG1HPWtg');

const readPad = () => [0, 12000, -3000];   // your own source: buttons, x, y

const [buttons, x, y] = readPad();
pad.send(packPad(buttons, [x, y]));
```

```ts
import { BitskiffClient } from '@bitskiff/client';

// buttons: a bitfield, button i is bit i.
function packPad(buttons: number, axes: number[]): Uint8Array {
  const bytes = new Uint8Array(4 + axes.length * 2);
  const v = new DataView(bytes.buffer);
  v.setUint32(0, buttons, true);
  axes.forEach((a, i) => v.setInt16(4 + i * 2, a, true));
  return bytes;
}

const client = new BitskiffClient({ key: 'bsk_pk_...' });   // your client key
const pad = client.channel('move');           // the host's row makes it lossy at 60 Hz; name, then join
await client.join('1CAbCdEfG1HPWtg');

const readPad = (): [number, number, number] => [0, 12000, -3000];   // your own source

const [buttons, x, y] = readPad();
pad.send(packPad(buttons, [x, y]));
```

The host reads it back on the channel of the same name, the same way round:

```js
import { BitskiffHost } from '@bitskiff/host';

const host = new BitskiffHost({ key: 'bsk_pk_...' });   // your host key

host.channel('move', { delivery: 'lossy', maxHz: 60, latestOnly: true }).on('message', (m) => {
  if (typeof m.data === 'string') return;
  const v = new DataView(m.data.buffer, m.data.byteOffset, m.data.byteLength);
  console.log(m.client.id, v.getInt16(4, true), v.getInt16(6, true));  // buttons: v.getUint32(0, true)
});

const session = await host.open();                      // declare and listen, then open
```

```ts
import { BitskiffHost, type Message } from '@bitskiff/host';

const host = new BitskiffHost({ key: 'bsk_pk_...' });   // your host key

host.channel('move', { delivery: 'lossy', maxHz: 60, latestOnly: true }).on('message', (m: Message) => {
  if (typeof m.data === 'string') return;
  const v = new DataView(m.data.buffer, m.data.byteOffset, m.data.byteLength);
  console.log(m.client.id, v.getInt16(4, true), v.getInt16(6, true));  // buttons: v.getUint32(0, true)
});

const session = await host.open();                      // declare and listen, then open
```

The channel name `move` is the sample client page's name for its joystick, and this struct is the
one that page sends, so the host above reads the sample client page as it stands. On a page of your
own the name is yours: pick one and spell it the same way on both halves.

Nothing in the middle reads those bytes. Put your own axis units, your own button order and your
own timestamp in the struct. The service carries a string or bytes and looks at neither.

## Send rate

Send the pad's whole state once per tick: your game loop, a `requestAnimationFrame`, or the
controller's own poll. Do not send on every event the browser or the pad hands you. This carries on
from the `pad` above.

```js
const packCurrentPad = () => new Uint8Array(8);   // whatever you read, packed

function tick() {
  pad.send(packCurrentPad());       // the whole state, once a frame
  requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
```

```ts
const packCurrentPad = (): Uint8Array => new Uint8Array(8);   // whatever you read, packed

function tick(): void {
  pad.send(packCurrentPad());       // the whole state, once a frame
  requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
```

A stick fires far more often than a frame, so the extra sends buy nothing. Past `maxHz`, the SDK
drops the send at the sender and returns `{ dropped: 'rate' }` -- the packet in hand, which may be
the one that mattered. Sampling gives you one packet a frame, always carrying the newest state.

The `latestOnly` option on the host's declaration finishes the job at the far end. It is a receive
rule: a packet that arrives behind one the same sender already delivered is dropped rather than
handed over, so a reordered frame never moves a stick backwards. The same row's `maxHz` is what
paces the client that sends.

## Lost release packets

A lossy channel does not retransmit, so the packet that says "button up" or "stick back to centre"
can be the one that is lost, and the host goes on showing the button held. Do both of these:

- **Keep sending for a few ticks after the last input.** The neutral state then arrives even when
  one packet of it is lost. Every stream of level values on a lossy channel needs this, not just
  a pad.
- **Zero the pad when a client goes quiet.** On the host, a `clientChanged` of `disconnected` or
  `away`, and `clientLeft`, all mean no more input is coming from that client, so clear its pad
  there instead of waiting for a packet that never arrives. [Lifecycle](/docs/guides/lifecycle/) has what each of those changes means.

## Multiple layouts

Give each layout its own channel name. Two pads, or a pad and a wheel, are two channels. Do not put
a tag byte in front of one struct. The channel name already says which kind of message this is.

The host declares each layout's channel, and a client names only the ones it handles. Changing a
layout later means changing a name, rather than keeping a tag in step across both halves.

## The sample client page

The sample client page's joystick sends the same struct. Its two axes go out as a packed struct on
`move`, and the sample host reads them back and shows them. The sample client samples the stick
sixty times a second and names `move`.

The sample host declares `move` as
`{ delivery: 'lossy', maxHz: 60, latestOnly: true }` and paints the latest position it has. The
host's row sets the rate: without `maxHz`, lossy is paced at 30.

The sample host is the quickstart's host plus that read-back and the sample page's `text` line:
`press` and `readout` come from the quickstart, `move` from this page.

## Cost

A pad costs what its bytes cost. They are metered by direction like every other message, and the
[console](https://console.dev.bitskiff.com) shows the usage. To spend less, send a smaller struct or
sample it at a lower rate.

## Next steps

- [Channels](/docs/guides/channels/)
- [Lifecycle](/docs/guides/lifecycle/)
- [Client pages](/docs/guides/client-page/)
