Skip to main content

Quick start

import asyncio

import crowdypy

API = "https://ck.prod.crowdedkingdoms.com"


async def main() -> None:
# The identity client: sign-in, account and studio calls, minting.
async with crowdypy.AsyncCrowdyClient(http_url=API) as identity:
await identity.auth.login("player@example.com", "correct-horse-battery")
if not await identity.auth.player_legal_acceptance():
# Only after the player ticked the box: this records their agreement.
await identity.auth.record_player_consents(accept_legal=True)
minted = await identity.portal.mint_app_token("42")

# The game client: one per app, at the app's own datacenter.
async with crowdypy.AsyncCrowdyClient(
http_url=minted.game_api_url or API, discovery_url=minted.discovery_url
) as game:
game.set_app_token(minted)
await game.udp.connect(minted) # assign a server, open the UDP socket
print(await game.users.me())


asyncio.run(main())

Without an event loop, crowdypy.sync.CrowdyClient has the same methods without await.

Two tokens, two clients​

  1. Signing in yields an identity session token, for account, studio administration and minting. It is not accepted for gameplay.

  2. Gameplay needs a short-lived app token per app (portal.mint_app_token(app_id)), which is also the HMAC key for native UDP. client.refresh_gameplay_token() refreshes it, and concurrent callers share one refresh. A live UDP connection keeps its replication server across the refresh.

    The age and the terms (CrowdyPy 0.6.0 and 0.23.0). No app token is issued until the player has an age and has agreed to the current required legal documents, the Player Terms and the Player Privacy Policy; until then minting raises AGE_REQUIRED or LEGAL_ACCEPTANCE_REQUIRED (crowdypy.is_legal_acceptance_required_error). Show your own checkboxes, linking each document (auth.player_legal_documents() and the app's own auth.app_required_documents(app_id)), then call auth.record_player_consents(accept_legal=True, app_id=...). auth.register(..., accept_legal=True, birth_date=..., country=..., parental_confirmation=...) creates an account that starts accepted, with the answers of a neutral age screen; an account with no age gives it once with auth.record_player_age(...). See Terms, age and country.

  3. Build one identity client and one client per game. The game client points at the app's datacenter (game_api_url), and discovery_url lets it find the app again if that instance stops answering.

Persist sessions with crowdypy.FileTokenStore: FileTokenStore.session_path(directory, api_origin) names the identity session file, and FileTokenStore.app_path(directory, app_id) an app token's.

CrowdyPy sends no Origin header, so direct sign-in (auth.login, auth.register) is available to it. A game running in a browser on its own domain signs players in through the hosted /authorize page instead, using CrowdyJS.

Errors​

Every error is a crowdypy.CrowdyError. A GraphQL refusal is a CrowdyGraphQLError carrying the server's stable code (error.code == "FORBIDDEN") and its extensions. HTTP, network and timeout failures have their own classes. A WRONG_DATACENTER refusal moves the client to the app's datacenter and retries once, without your code doing anything.

Some refusals carry more (0.8.0): access_refusal_of(error) reads ACCESS_REVOKED, ACCESS_SUSPENDED (with when it lifts) and ACCESS_NOT_GRANTED from a mint; actor_exists_of(error) reads ACTOR_EXISTS and whether the actor is already yours; and app_paused_of(error) reads APP_PAUSED and its reason. A paused app still mints: check is_app_paused(token.runtime_gate) and tell the player the world is paused. From 0.11.0, account_blocked_of(error) reads ACCOUNT_BLOCKED and when a timed block lifts, and group_member_banned_of(error) reads GROUP_MEMBER_BANNED from joining a channel or team you are banned from. For org-managed players:

  • From 0.16.0, assertion_rejected_of(error) reads ASSERTION_REJECTED and the failed check, and managed_player_not_allowed_of(error) reads MANAGED_PLAYER_NOT_ALLOWED.
  • From 0.17.0, org_legal_acceptance_required_of(error) reads ORG_LEGAL_ACCEPTANCE_REQUIRED: the player's organization has not accepted the Org-Managed Players Addendum, which only an admin of the organization can fix, in Studio.

A ck-exec connection that the server closes with 4403 ("access ended") does not reconnect: pending calls fail Denied, and the listener you gave on_access_ended hears the reason.

From 0.23.0, the player gate's refusals have readers too, in crowdypy.errors: legal_acceptance_required_of (with missing_documents), age_required_of, age_requirement_not_met_of (a boolean: it surfaces no age, and a game shows none), territory_not_supported_of (with country), safety_log_required_of, parental_consent_unavailable_of and social_limit_reached_of. From 0.24.0, safety_moderator_required_of reads SAFETY_MODERATOR_REQUIRED, and parental_consent_unavailable_of returns the refusal with its reason (not_configured, provider_refused or provider_unavailable) instead of a boolean; apps.app() reads hasSafetyModerator, and platform.territory_policies() rows carry regionCode.

Player safety​

CrowdyPy 0.23.0 and 0.24.0 wrap the teen safety program. With the player's app token, client.safety holds their own controls in the app: comm_settings, set_comm_settings, request_parental_consent, contacts, contact_requests, request_contact, accept_contact, remove_contact, blocks, block, unblock and file_report. apps.recording(app_id) says what the safety log records, for the notice and the indicator. Moderators use client.moderation (reports, resolve_report, escalate_report) with their identity session. See Voice, video, contacts and blocks and Safety reports and moderation.