Skip to main content
Crate name: arcane_sdk (package arcane-sdk). The whole public surface is one type you build at launch, plus its errors. Error codes are shared with the C ABI — see Errors for the full debug table.

ArcaneClient

ArcaneClient is Debug + Clone, and Send + Sync. Clones share one play session — it ends when the last one is dropped, or on shutdown — one achievement cache, and one lobby event queue. There is no friends cache to share: friends().list() always asks the Arcane desktop app.

init

Build the client and check ownership. Call once at launch, with no argument.
The ids come from the launch environment: Arcane Powered sets ARCANE_GAME_ID to your title’s game id and ARCANE_USER_ID to the signed-in account on the game process. To run your build outside the launcher, set them yourself — see Local development.
Reads and validates the game id, short-circuits to DrmDisabled when the cached flag says DRM is off, otherwise verifies the cached ticket. On ticket_missing / ticket_expired it contacts the Arcane desktop loopback (and may open the app) to refresh, then re-verifies. See Ownership model. It then opens the play session and starts the arcane-session thread. That never blocks init and never fails it: if the desktop app is unreachable, tracking stays Pending and retries every 60 seconds, without ever opening the deep link. See Session. Errors: missing_game_id, invalid_game_id, ticket_missing, ticket_expired, ticket_invalid, device_mismatch, clock_rollback, not_owned, network_required, not_authenticated, arcane_unavailable, ambiguous_session, internal. Full causes and fixes: Errors.

refresh

Re-run the check against Arcane desktop and update the client in place. Nothing calls this for you — there is no background revalidation.
On failure the client keeps its previous state. Errors: same as init minus missing_game_id and invalid_game_id.

Accessors

All read cached state — no I/O, no failure. game_id() is the value read from ARCANE_GAME_ID, so it always answers. user_id() is resolved in order from ARCANE_USER_ID, the ticket file, session.json, and what the desktop app reports. It is an Option because an older Arcane desktop build may not report it, and because a DRM-disabled title launched without ARCANE_USER_ID may have no ticket file to read it from. user_id is a plain copied field: a clone carries the value held when the clone was made, and refresh() updates only the client it is called on. The play session and the achievement cache are shared between clones; user_id is not.

frame

Counts one rendered frame. Outside an FPS sampling window it is a single relaxed atomic load; inside one it adds a relaxed increment. No lock, no allocation, no clock read — call it every frame at any frame rate. A game that never calls it reports no FPS samples.

set_graphics

Records the current display settings, attached to the FPS samples that follow. Both strings are free-form; empty strings clear them. Takes a short lock — call it at startup and when settings change, never per frame.

session

A copy of the session state, read from memory. Never fails.

achievements

Borrows an Achievements<'_> accessor. It holds no state, so there is nothing to keep around. See Achievements.

friends

Borrows a Friends<'_> accessor, carrying only this client’s game_id so in_game can be derived. See Friends.

p2p

Borrows a P2p<'_> accessor. This is the one accessor with a side effect: the first call arms lobby event polling on the arcane-session thread. A game that never calls p2p() never polls. See Lobbies.

Achievements

list and unlock are synchronous — one loopback round trip each, on the calling thread, around a millisecond. Call them when something happens, never per frame. is_unlocked only reads memory.

list

Every achievement the title defines, with this player’s state, and it fills the client’s cache so is_unlocked can answer afterwards. Errors: not_owned, not_authenticated, network_required, arcane_unavailable, feature_unavailable.

unlock

Idempotent: unlocking twice succeeds and answers already_unlocked, so a game can call it every time its condition holds. A queued answer (desktop app offline) is also a success. The cache is updated either way.
Errors: invalid_argument when the key is empty, over MAX_ACHIEVEMENT_KEY_LEN bytes, made only of dots, or outside a–z 0–9 _ - . — keys are lowercase — raised before any network call, and also when Arcane rejects the key; unknown_achievement; plus the codes of list.

is_unlocked

Reads the cache list filled. None means the SDK has nothing to answer with: list has never succeeded, or the key was not among the achievements it returned. Never fails, no I/O.

shutdown

Ends the play session, reporting the final playtime to the Arcane desktop app with a 2-second timeout, and consumes the client. Dropping the last clone does the same best-effort, so this is optional.

Friends

list is synchronous — one loopback round trip on the calling thread, around a millisecond. Call it when a friends menu opens or on a timer of your own, never per frame. The SDK caches nothing: the Arcane desktop app caches the list for 15 seconds and reports stale when it served that cache while offline — a success, not an error.
in_game is true when the friend is playing the same game_id as this client — the id in ARCANE_GAME_ID; a friend playing anything else is online but not in_game. Errors: not_authenticated, network_required (including under ARCANE_OFFLINE_ONLY, raised before any call), arcane_unavailable, feature_unavailable.

P2p

Arcane hosts the lobby, the join code and the invitations, and carries one opaque payload per member. There is no transport: your game connects with its own netcode. See Lobbies. Every call but poll_events and a cached launch_join_code is synchronous — one loopback round trip on the calling thread. Never per frame.

create_lobby, join_by_code, join

All three answer the same Lobby snapshot. payload is your connection blob, at most MAX_LOBBY_PAYLOAD_LEN (4096) raw bytes, base64 on the wire and never read by Arcane. A join code is uppercased before it is checked.
Errors: invalid_argument for an oversized payload, a join code that is not six characters of A–H J–N P–Z 2–9, or a lobby_id outside A–Za–z0–9- — all raised before any network call; lobby_not_found, lobby_full, lobby_closed, not_friends, not_owned, not_authenticated, network_required, arcane_unavailable, feature_unavailable.

get_lobby

The lobby as Arcane knows it right now — the answer to a Resync, or to any moment you would rather ask than replay events. It joins nothing and leaves nothing, so it never changes what this client is in or how fast the session thread polls. Errors: invalid_argument for a malformed id, lobby_not_found, lobby_closed, not_friends, plus the codes of create_lobby.

invite, leave, close

invite sends one friend an invitation: an Invite event if they are already playing this title, the code for their next launch otherwise. leave takes this player out; close ends a lobby this player hosts, and its members get LobbyClosed. There is no host migration. A leave or close that never reached Arcane leaves the player in the lobby, and the SDK with them: it stops treating the lobby as open only on success, or when Arcane says it is already gone (lobby_not_found, lobby_closed). Errors: as above, plus not_friends from invite when that account is not a friend.

launch_join_code

The code the launcher stashed when the player started the game from a friend’s “Join”. Read from the Arcane desktop app on the first call and cached for the client’s lifetime — the desktop app clears it once served. Never fails: None covers a normal launch, an older desktop app, and ARCANE_OFFLINE_ONLY.

poll_events

Drains the queue the session thread fills — memory only, no I/O, no failure. Events come oldest first and exactly once. Once a second is plenty; the queue keeps the 256 most recent events if you never call it.
client.session().lobby_events says whether the polling behind it is Off, Active, or Unavailable because the desktop app predates the routes.

SdkError

Implements Display (all four fields on one line), std::error::Error, Debug, Clone, PartialEq. Match on code() for a stable string contract, or on error_code() for the typed enum. ErrorCode is #[non_exhaustive] — new codes can appear in a minor release, so keep a _ arm.

Types

OwnershipStatus implements Display ("owned" / "drm_disabled"), and so do TrackingState ("active" / "pending" / "disabled"), LobbyPollingState ("off" / "active" / "unavailable") and Visibility ("friends" / "code" / "friends_and_code"). SessionSnapshot is Debug + Clone; Achievement, Unlock, Friend, FriendList, Lobby, LobbyMember and LobbyEvent are Debug + Clone + PartialEq. LobbyEvent::lobby_id() reads the lobby id of any variant, and None for Resync, which is about every lobby this client is in.

Crate types

Use rlib from Rust games; link cdylib / staticlib for the C ABI.

Try it

Prints every client field, renders a fake loop calling frame(), prints the session snapshot and ends with shutdown() — or the full error breakdown (code, message, hint, context, JSON) on failure. Run it without the variable and you get missing_game_id.