The messages between the launcher and the net service it starts. Version 0, a draft: it changes without notice until version 1.
The Rust reference is crates/control. The launcher shows; the net service acts (ADR 0006).
- The launcher starts the net service as a child process and talks to it over the child's standard input and output. Nobody else holds those streams, so the channel needs no secret.
- Each message is one JSON object on one line, in UTF-8, ended by
\n. A line has at most 1 MiB. - The launcher sets
ANYPLAY_CLIENTin the net service's environment tolauncher/<its version>(such aslauncher/0.3.0). The net service sends it to the cloud asX-AnyPlay-Clienton every request and on the event connection (cloud API); without it, or when it is not a valid value, it sendsnet/<its own version>, which no cloud gates. - The net service writes its log to standard error. The launcher keeps it in its log folder.
- The net service ends when its standard input closes and no game is connected to it. While a game is still connected it waits for that game to end.
The launcher sends commands; each has a number id and a name do:
{"id": 7, "do": "add_friend", "handle": "Lumen"}No command has a field of its own named id, since it would take the place of the number. A command about an invitation or a dialog names it invitation or dialog.
The net service answers every command once, with the same id, either ok or error:
{"id": 7, "ok": {"result": "requested"}}
{"id": 7, "error": {"code": "not_found", "message": "No account has this handle."}}It also sends events on its own, named by event:
{"event": "state", "cloud": "online", "account": {"id": 41, "handle": "Alec", "email": "a@example.org", "appear_offline": false}, "pending": null}Fields the receiver does not know are ignored, and so are events it does not know. A command it does not know is answered with the error unknown_command.
Error codes are those of the cloud API, plus unknown_command, offline (the cloud cannot be reached), not_signed_in, no_game and bad_state (the command does not fit what is happening, such as a code without an e-mail before it).
An error can carry more than a code and a message. version_mismatch adds expected. restricted and banned add restriction, so the launcher can say what is limited and until when instead of showing a generic failure:
{"id": 9, "error": {"code": "restricted", "message": "A moderator muted this account.", "restriction": {"kind": "muted", "until": 1793000000, "rule": "spam", "reason": "Links in every message.", "action": 41, "appeal": {"status": "available", "deadline": 1808000000}}}}A restriction has a kind and, when they apply, until, rule, reason, action and appeal. It is the cloud's statement of reasons (cloud API) reduced to what the player reads; it never names a reporter or a moderator, and the appeal token of a ban stays in the net service.
kind |
Meaning | Comes with |
|---|---|---|
muted |
A moderator muted the account: no friend requests and no messages. | until, rule, reason, action, appeal |
new_account |
The account is too new to write to people who are not friends, or to send more friend requests today. Nothing a moderator did. | until (when it may) |
banned |
The account is banned and signed out. | until (absent for good), and when the cloud has a statement rule, reason, action, appeal |
When the cloud retires this launcher's version (cloud API), a command that reaches the cloud fails with client_too_old, which adds minimum (the oldest version the cloud accepts, as a string):
{"id": 9, "error": {"code": "client_too_old", "message": "This launcher is too old. Update to 0.3.0 or newer.", "minimum": "0.3.0"}}kind is a plain string and appeal.status too (available, open, upheld, reversed, expired): a launcher treats a value it does not know as a restriction it cannot name, and still shows until and reason. A restricted failure from a net service that predates this field has none; the launcher then falls back to the message.
do |
Fields | ok |
|---|---|---|
hello |
launcher (its version), cloud (the cloud's URL) |
{service, protocol} |
browser_sign_in |
create (default false) |
{url}: open it in the system browser |
cancel_sign_in |
{} |
|
account_page |
{url}: open it in the system browser |
|
appeal |
action (a restriction's action), text (1 to 1,000 characters) |
{}; the restrictions event follows with the appeal's new status. limit when the action was appealed before, forbidden after its deadline, not_found for an action that is not the player's |
email (deprecated) |
email |
{code_length} |
code (deprecated) |
code |
{result: "signed_in" | "new_account"} |
sign_up (deprecated) |
handle, age_confirmed |
{} |
sign_out |
{} |
|
delete_account |
{} |
|
add_friend |
handle |
{result: "requested" | "friends"} |
accept |
account |
{} |
decline |
account |
{}: declines an incoming or withdraws an outgoing request |
remove_friend |
account |
{} |
block |
account |
{} |
unblock |
account |
{} |
report |
account, reason, details |
{} |
playing |
game: {title_id, title, version} or null |
{} |
check_join |
session, title_id, version, build, version_rule (exact, major or any; absent is exact), override_version (true for "Try Anyway"; absent is false) |
{}, or the error version_mismatch |
game_start |
title_id, title, version, build, join (a session ID or null), host (Play Together: {max_members, visibility, party}, or absent), version_rule and override_version (as in check_join, passed on to the cloud when join is set) |
{env, session} |
game_end |
{} |
|
invite |
accounts |
{sent} |
answer_invitation |
invitation (its id), accept |
{session, title_id} when accepted, else {} |
dialog |
dialog (the id of the invite_dialog event), accounts (null when the player closed the picker) |
{} |
discord_status |
{state, user, friends}: see the discord event |
|
discord_connect |
{} at once; the discord event follows. not_signed_in, and bad_state when this build has no Discord SDK |
|
discord_disconnect |
{}: ends the connection and forgets the kept refresh token |
|
relay_only |
on |
{} |
upload_trophies |
title_id, trophies |
{added} |
trophies |
account |
{titles}, or the error not_found when they are hidden |
rarity |
title_id |
{title_id, players, trophies} |
compare |
account, title_id |
TrophyCompare of Comparing: the player (you) next to that account (them) in one title, with points and level per side; ids only, the launcher adds names and art from the game's own trophy file. not_found when that account does not exist or its trophies are hidden; bad_request for the player's own account |
secret_trophies |
title_id, numbers |
{changed}: marks the trophies as secret (a game hides them; the launcher knows which); idempotent |
reveal_trophies |
title_id, numbers |
{changed}: shows secret trophies to everyone who may see the player's trophies; there is no way back |
run_report |
title_id, version, toolkit, verdict (no_start, menus, in_game, playable) |
{}; the service adds the player's system |
run_reports |
title_id, version (or null) |
{title_id, version, reports, verdict, counts} |
cards |
accounts |
{cards} |
set_appear_offline |
on |
{appear_offline}: the new setting, see Appearing offline; offline when the cloud cannot be reached (nothing is changed then) |
public_profile |
{public_profile}: whether the page at /u/<handle> is shown to everyone, false until the player turns it on (Public profile) |
|
set_public_profile |
on |
{public_profile}: the new setting |
played_with |
{remember, people}: the people the player played with recently (Played with recently) |
|
clear_played_with |
{}: empties that list |
|
set_remember_played_with |
on |
{remember_played_with}: the new setting |
privacy |
the player's Privacy |
|
set_privacy |
privacy |
the new Privacy |
profile |
account |
ProfilePage: the Profile with the parts the player may see of that account, their trophy counts (null when hidden) and the connections they show (links) |
upload_playtime |
entries |
{changed} |
feed |
before, limit |
{items, next}: a page of the feed, newest first; both fields may be left out |
react |
item, reaction |
{reactions, mine}; reaction is nice, gg, wow or same, or null to take the reaction back. not_found when the player cannot see the item. (The item is named item: id is the number of the command.) |
party |
{party}: the player's Party, or null |
|
party_create |
{party}: a party of one with the player as its leader, or the party they are in already; no invitation is needed |
|
party_invite |
accounts, call (default false) |
{sent}: friends are invited; a party is made when the player is in none. With call they are rung: the invitation lives 30 seconds |
party_link |
url |
{}: the leader sets the party's Discord link; the empty string removes it. bad_request for a link that is not a Discord one |
missed_calls |
{missed} |
|
calls_seen |
account (null: all) |
{} |
party_answer |
invitation (its id), accept |
{party} when accepted, else {} |
party_leave |
{} |
|
party_kick |
account |
{} (leader) |
party_leader |
account |
{} (leader) |
party_move |
session |
{sent}: invites the other members into the leader's session (leader) |
join_code |
{code, url, expires}: a new join code for the player; not_found when there is nothing to join |
|
join_code_revoke |
{} |
|
join_redeem |
code |
{kind, invitation}: the invitation the code made, to be shown like any other (kind is session or party) |
voice |
on |
{} |
mic_mute |
muted |
{}: mutes the player's own microphone in party voice, whatever the mode |
deafen |
deafened |
{}: nobody is heard, and the microphone is silenced too |
push_to_talk |
talking |
{} |
voice_noise |
enabled |
{}: noise suppression (RNNoise) on the microphone; on in a new service |
voice_echo |
enabled |
{}: echo cancellation (WebRTC AEC3) on the microphone, so the speakers are not heard back; on in a new service |
voice_mode |
mode: push_to_talk or voice_activity |
{} |
peer_volume |
account, percent (0 to 200) |
{}; bad_request above 200 |
peer_mute |
account, muted |
{} |
voice_devices |
{input, output, selected_input, selected_output}: names; a selected name is null for the system's default |
|
set_voice_device |
kind: input or output, name (null: the system's default) |
{}; not_found when no device has the name |
conversations |
{conversations, requests} |
|
open_conversation |
accounts, name |
the Conversation: with one account the direct one, with more a new group |
messages |
conversation, after, before, changed_after, limit |
{messages, last, rev}; all but conversation may be left out, and at most one of the first three given |
send_message |
conversation, text, reply |
the Message; reply (optional) is the number of the message it answers |
edit_message |
conversation, seq, text |
the Message, within 15 minutes of writing it |
unsend_message |
conversation, seq |
the Message with deleted true |
send_media |
conversation, path |
the Message with media, when the upload is done (a clip can take minutes; other commands are answered meanwhile). The service reads the file at path, checks that it is a PNG, JPEG or MP4 within the limits, and refuses before sending anything otherwise |
fetch_media |
media, path |
{} once the file is whole at path; it is written through path.part and nothing is left behind on a failure |
lfg |
title_id, mode, language |
{listings}: the open listings of the game; mode and language may be left out |
lfg_mine |
{posted, asked} |
|
lfg_post |
title_id, mode, language, note, slots |
the Listing |
lfg_close |
listing |
{} |
lfg_ask |
listing |
the Listing with ask |
lfg_withdraw |
listing |
{} |
lfg_answer |
listing, account, accept |
the Listing; accepting sends a party invitation to account, declining is silent |
react_message |
conversation, seq, reaction |
the Message; reaction is nice, love, laugh, gg, wow, sad, or null to take it back |
typing |
conversation, typing |
{}: the net service passes on at most one true every 3 seconds per conversation |
conversation_settings |
conversation, muted, pinned |
the Conversation; a field left out stays |
mark_read |
conversation, seq |
{} |
accept_request |
conversation |
the Conversation |
leave_conversation |
conversation |
{}: leaves a group or declines a request |
add_to_conversation |
conversation, accounts |
{added} |
screen_start |
source: game (default) or test_pattern; window: the game's window as a number (a Windows HWND), pids: the game's process ids, both optional |
{}: the player's game is streamed to the party |
screen_stop |
{} |
|
screen_watch |
account |
{}: watch the stream that member offers |
screen_leave |
{} |
|
share_start |
account, mode (watch, hand_over or together) |
{}: the invitation went out; the share event follows. Fails with not_signed_in, not_allowed (the cloud has share play off), busy, no_game (no game runs through AnyPS5), not_in_party (the account is not a member of the player's party), no_encoder |
share_answer |
invitation, accept |
{}: the guest accepts (the viewer opens, the share event follows) or declines. Fails with not_found (the invitation is gone), busy, no_decoder |
share_mode |
mode |
{}: the host changes the mode of the running share at once (bad_state for anyone else) |
share_request |
mode |
{}: the guest asks the host for a mode; nothing changes until the host answers with share_mode (bad_state for anyone else) |
share_stop |
{}: the host stops the share or the guest leaves it; also {} when there is none |
|
screen_worker |
path |
{encoders, decoders, capture, libraries}: where the launcher put the verified share worker; libraries (absent when the worker loads none) is [{name, state, detail}], see Screen share |
hellocomes first. Until it is answered every other command getsbad_state. The net service then resumes the device's saved sign-in, if there is one.browser_sign_inis how the launcher signs a player in or creates an account (ADR 0016). The net service opens a listener on127.0.0.1with a random port, makes a PKCE verifier and astate, and answers the address of the cloud's sign-in page; the launcher opens it in the system browser. The net service then sends astateevent withpendingbrowser, and when the player has finished in the browser, astateevent with the account (andpendingnull). It fails withnot_foundwhen the cloud has no account web, and withbad_statewhen an account is signed in. A browser that is not finished in 10 minutes, or a sign-in the player stops, ends with anotice(sign_in_timed_out,sign_in_stopped) andpendingnull.createopens the page at "Create account". Nothing is typed in the launcher.cancel_sign_inends abrowser_sign_inthat is waiting. It answers{}even when none is.account_pageanswers a link that opens the account web signed in as the player (60 seconds, one use): e-mail, passkeys, devices, a copy of the data, deleting the account. It needs a signed-in account whose device has a key (unauthorizedotherwise: sign in again) and a cloud with an account web (not_found).email,codeandsign_upstill work and are deprecated: usebrowser_sign_in.emailstarts a sign-in;codefinishes it or, for a new address, asks forsign_up.game_startanswers the variables the launcher adds to the game's environment, at presentANYPS5_NET_ENDPOINTandANYPS5_NET_TOKENfrom the bridge protocol. Withjoin, the net service holds the join intent for that session.game_endfollows when the game's process has ended.game_startwithhostis Play Together (spec): before it answers, the net service opens alansession for the game withmax_members(2 to 64) andvisibility(friendsorinvite), and answers its ID insession. Withpartytrue it also asks the player's party in: the party moves into the session when the player leads it, else each other member gets an invitation (the cloud delivers it to friends only).joinwins when both are given. If the session cannot be opened (the network is not ready, the player is in the most sessions allowed, the cloud answered an error) the game starts all the same,sessionis absent, and anoticewith the codehost_failedand the reason follows at once. The game is then a game like any other, and the launcher says why nobody can join.inviteinvites friends to the session the player is in.profileonly reads. A player edits their profile, avatar and visibility on the cloud's web site, which uses the HTTP API directly (cloud API); the launcher shows whatprofileandcardsanswer.upload_playtimecan be sent again with the same entries; the cloud keeps the highest minutes per title and day.- The party and message commands are the cloud API one to one, with the errors of its requests.
party_answerandparty_leaveupdate the held party before the answer. - The join commands are the cloud API one to one, with the errors of its requests.
join_redeemtakes the code alone or a whole link (anyps5://v1/join/<code>,https://…/join/<code>) and reads the code out of it; text that holds no code isbad_request. The invitation also arrives as the usualinvitationorparty_invitationevent. Neither command joins anything. - The voice commands work on party voice.
voicewithontrue switches it on: from then on, whenever the player is in a party and signed in with tunnels up, the net service opens the microphone and speakers, joins the party's voice and tells the party it takes part. Switched off, or without a party, the devices are closed: the microphone is never open otherwise. The setting lasts untilvoiceis sent again or the service ends; a new service starts with voice off, so the launcher sendsvoiceafterhelloif the player wants it. The answer does not wait for the devices: if they cannot be opened, the net service switches voice off again, sends anoticevoice_devicewith the reason and avoiceevent withenabledfalse. push_to_talkis the key being down (talkingtrue) or up. Invoice_activitymode it has no effect.voice_mode,peer_volumeandpeer_muteare remembered by account for as long as the service runs, also while voice is off or the member is away, and apply when voice starts. Volume is in percent of what was sent, 100 normally.voice_deviceslists the names the system has and which are chosen.set_voice_devicechooses by one of those names (or null for the default) and, while voice runs, closes the devices and opens them again, so the connections to the members are made again too. A service built without sound devices lists none and fails to open any; release builds have them.- Share play (its spec) is the invited, one-to-one stream with a pad coming back. The
share_*commands are answered{}; what happened arrives as theshareevent, whoseshareis null when the player is in none and otherwise hasid,role(hostorguest),peer(the other account),title,mode,phase(invited,connecting,liveorstalled),link,codec, the numberswidth,height,fps,kbps,rtt_ms,loss_percent, andrequested(the mode the guest asked for, to the host).can_startsays the player could invite someone now. The worker is the onescreen_workernamed. The command fieldinvitationnames the invitation becauseidis the command's number. - Share screen (watch only, to the party, no cloud involved) is specified in its own file: the
screen_*commands and thescreenevent.screen_startfails withno_game,not_signed_in,not_in_party,no_encoderorbusy;screen_watchwithnot_found,busyorno_decoder. The event carrieshosting(the player's own stream with one entry per member it reaches),offers(streams the player may watch) andwatching, and no field of it is namedid. - The Discord commands and event are phase 0 of ADR 0022.
stateisunavailable(no SDK in this build, or the library is missing or refused to load: nothing else changes),signed_out,connecting(the SDK asks the player, the cloud trades the code, or the SDK connects) orconnected.useris{id, name}while connected (idis a decimal string,namethe display name) andfriendsthe number of Discord friends; both are null otherwise, and the friends themselves never leave the net service.discord_connectuses the refresh token the service kept in the credential store for this account, and only asks the player (Discord's overlay, app or browser) when there is none or Discord refuses it; the cloud keeps nothing (cloud API). A failure comes as anoticewith the codediscord_failed. - Share play adds commands and events, drafted in the share play spec. They are not implemented.
appear_offlineis a setting of the account, so a launcher needs one command and one field:set_appear_offlinewithonwrites it to the cloud, andaccount.appear_offlineof thestateevent (sent after sign-in, after the command, and when another device or the portal changes it) says what it is. The player's own friends list and presence stay as they are; the launcher shows its "Appearing offline" hint from this field. The cloud stops friends from seeing the player as online; the net service does not hide anything itself, and keeps the event connection and every other command working.relay_onlykeeps every tunnel on a relay, so other players never learn the PC's addresses, or lets them go direct again. Running tunnels start again and the peers reconnect, so the launcher sends it while no game runs.anyplay-net --relay-onlystarts with it on.
event |
Fields | When |
|---|---|---|
state |
cloud, account, pending |
after hello and whenever one of them changes |
discord |
state, user, friends |
after hello and whenever the Discord state changes |
friends |
friends, incoming, outgoing, blocked |
after sign-in and whenever a list or a friend's presence changes |
invitation |
invitation |
a friend invited the player |
invitation_gone |
id |
an invitation expired, was answered or its session closed |
session |
session or null |
the session the player is in changed, began or ended |
invite_dialog |
id, title, max |
the game opened its invitation dialog; answer with dialog |
session_ended |
session, title_id, title, host (an Account) |
a Play Together session closed, or took the player out, while the player's game runs. Not sent when the player's own game ended, or for a session the player leads. The game is not touched |
notice |
code, message |
something failed that no command waits for |
restrictions |
restrictions |
what limits the signed-in account now: a list of restrictions, empty when nothing does. Sent after the account goes online, when a moderator mutes or warns it, when an appeal is filed or decided, and after a restricted failure the list did not contain |
banned |
restriction |
the account was banned (the event connection saw it, or a request answered banned); the account is signed out. A notice with the code banned is sent too, for launchers that predate this event |
client_too_old |
minimum |
the cloud refuses this launcher because its version is below minimum. Sent once per run of the net service, by a request or by the event connection, whichever finds out first |
party |
party or null |
the party the player is in changed, began or ended; a copy with an older revision is dropped before it gets here |
missed_calls |
missed |
after sign-in and after every reconnect: every call nobody answered |
missed_call |
call |
a friend called and the player did not answer |
missed_call_gone |
account |
the missed calls from that account were seen |
call_ended |
account, reason |
a call the player placed ended unanswered (declined, no_answer) |
voice |
state, enabled, mode, muted, deafened, me, peers |
the voice state changed, or while voice runs a level or a link changed (at most ten a second) |
party_invitation |
invitation |
the leader of a party invited the player |
party_invitation_gone |
id |
an invitation expired, was answered or used up |
conversations |
conversations, requests |
after sign-in and after every reconnect |
message |
conversation, seq |
a new message; fetch it with messages |
message_request |
conversation |
a stranger wrote for the first time |
read |
conversation, account, seq |
a member read up to seq |
delivered |
conversation, account, seq |
a member's device received a direct conversation up to seq |
typing |
conversation, account, typing |
a member types, or stopped; show it for 6 seconds |
message_update |
conversation, seq, rev |
a message was edited, unsent or reacted to; fetch messages with changed_after |
conversation |
id |
a conversation is new, or its members, name, mute or pin changed |
conversation_gone |
id |
the player is no longer in the conversation |
screen |
can_share, can_watch, hosting, offers, watching |
share screen changed; while a stream runs its numbers at most twice a second |
share |
can_start, share |
share play changed; while a share runs its numbers at most twice a second |
share_invitation |
invitation |
a party member invited the player to share play; answer with share_answer |
share_invitation_gone |
id |
the invitation expired, was answered or its share ended |
share_request |
mode |
the guest asks the host for another mode |
lfg_request |
listing, title_id, from |
someone asked to join a listing of the player; fetch lfg_mine |
lfg_accepted |
listing, title_id, by |
the poster accepted the player's request; their party invitation arrives as party_invitation |
feed |
the feed has news; fetch feed. The net service sends at most one every two seconds, with one trailing event for news inside that time |
client_too_oldends the event connection for good: thecloudstate goes toofflineand stays there, and the net service does not try again until it is started again, so the launcher shows its update screen and does not wait for the connection. The sign-in stays kept. Every command that reaches the cloud afterwards still fails with theclient_too_oldfailure (withminimum), without a second event. A launcher that does not know the event ignores it, and a failure with an unknown code is a failure like any other.restrictionslists only the mutes that have not ended or been lifted; a warning limits nothing and is not listed, a new account's limit comes with therestrictedfailure. The net service sends it only when the list changed. A restriction withuntilin the past is over: the launcher drops it at that time without waiting for an event.- To appeal: a mute is appealed with the
appealcommand. A ban signs the account out, so it is appealed on the account web: the launcher startsbrowser_sign_in, and the cloud's sign-in ends on the page with the statement and the form (after a ban the player proves who they are there, as in ADR 0015). cloudisoffline,connectingoronline.accountis{id, handle, email}or null.pendingiscode(an e-mail is out),handle(the account needs a handle),browser(the system browser has the sign-in) or null.friendscarries the whole lists each time, with each friend's presence as in the cloud API. The net service sends at most four of them per second.reporttakes the optionalconversationandseqtogether: it reports that message of the account (cloud API). It takes an optionallistinginstead to report a looking-for-group listing (cloud API).- Edits, unsends, reactions, typing and the settings are the cloud API one to one, with its errors.
typingis fire and forget: the launcher sends it on every key if it likes, and the net service drops all but onetruein three seconds for a conversation (falsealways goes) and answers{}. messagecarries no text: the launcher keeps the highest number it has for each conversation and callsmessageswithafter. Theconversationsevent after a reconnect carries every conversation's newest number (last), so what arrived while the connection was down is fetched the same way.mutedanddeafenedin thevoiceevent say whatmic_muteanddeafenset (both false after the service starts, and remembered while it runs). Both silenceme: itsspeakingis false.voice.stateisoff(switched off, no party, or the devices could not be opened),connecting(switched on in a party: the devices are opening, or members who take part are not reached yet) oron(the devices run and at least one member who takes part is reached, or none takes part).enabledis whether the player switched voice on, which stays true across parties.modeispush_to_talkorvoice_activity.meis{level, speaking}for the player's own microphone after the gate.peershas one entry for each other member of the party:{account, level, speaking, link}, withlinkconnecting,directorrelayfor the voice connection to that member and null when there is none (they have voice off, or have not been reached).levelis the recent loudness in percent (0 to 100) before the player's volume setting;speakingis what a speaking indicator should show. While voice is off,meis silent andpeersis empty. While voice runs, the net service reports levels and links only when they changed.- The
partyevent and the party answers leave out the members' tunnel keys and relays (endpointis"",relaynull), assessiondoes; each member'svoicesays who takes part in voice. sessionis the cloud'sSessionwithoutbuild,propertiesand the members'endpoint, and with one more field per member:link, which isconnecting,directorrelayfor the other players and null for the player.