-
Notifications
You must be signed in to change notification settings - Fork 0
P2P
Since v0.6.0. steam.p2p is the curated layer over ISteamNetworking: send
bytes to another Steam user, read what they send back, and manage the session
in between. Addressing is by Steam id and nothing else. There is no host, no
port, no socket and no handshake to write.
Valve deprecated ISteamNetworking in favour of ISteamNetworkingMessages,
and this binding cannot wrap the replacement. Every call on the new interface
takes a SteamNetworkingIdentity, which is a C union. steam_api.json cannot
describe a union, so that struct has no offset table and there is no supported
way to fill the buffer the generated steam.networkingMessages asks for. The
old interface is therefore the peer to peer path that works today. It still
works: Valve keeps it alive for shipped games, over the same relay network.
The first packet from a new peer does not arrive. It raises
P2PSessionRequest_t on the receiver instead, and nothing gets through until
that side calls acceptSession, so subscribe with
onSessionRequest before you expect traffic. The sending
side needs none of this: sending to a peer accepts their session implicitly, so
whoever speaks first is already done.
The P2P instance is created lazily and cached on the Steam object.
The host accepts sessions and drains its channel on a timer. Steam queues packets while nobody reads, so a host that stops reading falls behind.
// host.ts
import { init } from 'steamwand.js';
const steam = init({ appId: 480 });
console.log('host is', steam.steamId());
steam.p2p.onSessionRequest((steamId) => steam.p2p.acceptSession(steamId));
setInterval(() => {
for (const packet of steam.p2p.readAll()) {
console.log(packet.steamId, 'said', packet.data.toString());
steam.p2p.send(packet.steamId, 'pong');
}
}, 50);// client.ts
import { init } from 'steamwand.js';
const steam = init({ appId: 480 });
const host = 76561198000000000n; // the id host.ts printed
steam.p2p.send(host, 'ping');
setInterval(() => {
for (const packet of steam.p2p.readAll()) console.log('host said', packet.data.toString());
}, 50);Both processes need a running Steam client logged in as the account they claim to be. Two accounts on two machines is the honest test; the same account twice is not one. This is also why the package has no live test for this layer.
send(steamId: bigint, data: Buffer | string, opts?: { reliable?: boolean; channel?: number }): booleanSends one packet. A string is encoded as UTF-8. reliable (default true)
picks k_EP2PSendReliable, which resends until the packet arrives and keeps its
order; false picks k_EP2PSendUnreliable, which sends once and may be dropped
or reordered. channel (default 0) must match the channel the peer reads.
Returns Steam's own answer. False means the packet was not queued at all, for example because the Steam id is not a valid user. True means queued, not delivered: delivery is what sessionState and onSessionConnectFail are for.
available(channel?: number): number | nullSize in bytes of the next packet waiting on channel (default 0), or null
when nothing is waiting. read calls this itself.
read(channel?: number): { steamId: bigint; data: Buffer } | nullReads the next packet, or null when nothing is waiting. The read buffer is
allocated at exactly the size Steam reported, so a packet is never truncated
and never over-allocated, and data is a copy you own.
readAll(channel?: number): { steamId: bigint; data: Buffer }[]Drains everything waiting on channel, oldest first. This is the call a game
loop makes once per frame.
acceptSession(steamId: bigint): voidAccepts a session another user asked for, from an onSessionRequest listener. Accept the peers you expect and ignore the rest: an unaccepted session costs nothing, and accepting an unknown Steam id lets that user send you traffic.
closeSession(steamId: bigint): voidCloses the whole session with a peer, on every channel. Packets still queued for that peer are dropped. Call it when the player leaves, or Steam keeps the session open until the process exits.
closeChannel(steamId: bigint, channel: number): voidCloses one channel of a session and leaves the others open.
sessionState(steamId: bigint): P2PSessionState | nullReads the state of the session with one peer, or null when there is none.
This is the diagnostic call: whether the connection came up, whether it goes
through a Valve relay, and how much is still queued.
allowRelay(allow: boolean): voidAllows or forbids relaying packets through Valve's servers. Relaying is on by default and is what makes peer to peer work behind a NAT, at the cost of some latency. Turn it off only when your game has its own fallback.
onSessionRequest(listener: (steamId: bigint) => void): () => voidSubscribes to peers asking to open a session, and returns an unsubscribe function. Accept only the peers you expect; see acceptSession.
onSessionConnectFail(listener: (event: { steamId: bigint; error: number }) => void): () => voidSubscribes to sessions that failed to come up or dropped, and returns an
unsubscribe function. This is the only signal that a peer went away: there is
no disconnect event otherwise. error is an EP2PSessionError: 2 no rights to
app, 4 timeout.
| Field | Type | Meaning |
|---|---|---|
steamId |
bigint |
Steam id of the peer that sent it. |
data |
Buffer |
The payload bytes, exactly as long as Steam reported. |
| Field | Type | Meaning |
|---|---|---|
connectionActive |
boolean |
True while packets can flow in both directions. |
connecting |
boolean |
True while Steam is still opening the session. |
error |
number |
EP2PSessionError: 0 none, 2 no rights to app, 4 timeout. |
usingRelay |
boolean |
True when traffic goes through a Valve relay. |
bytesQueued |
number |
Bytes still waiting to go out to this peer. |
packetsQueued |
number |
Packets still waiting to go out to this peer. |
remoteIp |
string |
Peer address as a dotted quad, empty when Steam does not know it. |
remotePort |
number |
Peer port, 0 when Steam does not know it. |
acceptSession, closeSession and
closeChannel throw a plain Error when Steam returns false,
which means an invalid Steam id. Nothing here throws a SteamResultError:
ISteamNetworking has no EResult anywhere. send returns its false
rather than throwing, because a refused packet is a normal thing to handle in a
game loop.
-
ISteamNetworkingMessages,ISteamNetworkingSocketsandISteamNetworkingUtils. The generated classes exist onsteam.networkingMessages,steam.networkingSocketsandsteam.networkingUtils, but their identity and address parameters are raw buffers with no layout to fill, for the union reason above. -
The socket half of
ISteamNetworking:CreateP2PConnectionSocket,SendDataOnSocket,GetSocketInfoand the rest, still onsteam.networking. The packet half does the same job. - Finding the other player, which is Lobbies.
Flat API explains the calling convention.
Next: Lobbies, which is where the Steam ids you send to come from.