mirror of
https://github.com/imayushsaini/Bombsquad-Ballistica-Modded-Server.git
synced 2026-08-15 13:04:30 +00:00
751 lines
23 KiB
Python
751 lines
23 KiB
Python
# Released under the MIT License. See LICENSE for details.
|
|
#
|
|
"""Cloud related functionality.
|
|
|
|
.. warning::
|
|
|
|
This is an internal api and subject to change at any time. Do not use
|
|
it in mod code.
|
|
"""
|
|
|
|
from enum import Enum
|
|
from dataclasses import dataclass, field
|
|
from typing import TYPE_CHECKING, Annotated, override
|
|
|
|
from efro.message import Message, Response
|
|
from efro.dataclassio import ioprepped, IOAttrs
|
|
from bacommon.analytics import AnalyticsEvent
|
|
from bacommon import securedata
|
|
from bacommon.transfer import DirectoryManifest
|
|
from bacommon.locale import Locale
|
|
from bacommon.login import LoginType
|
|
from bacommon.docui import DocUIRequest, DocUIResponse
|
|
import bacommon.displayitem as ditm
|
|
import bacommon.clienteffect as clfx
|
|
|
|
if TYPE_CHECKING:
|
|
pass
|
|
|
|
|
|
class WebLocation(Enum):
|
|
"""Set of places we can be directed on ballistica.net."""
|
|
|
|
ACCOUNT_EDITOR = 'e'
|
|
ACCOUNT_DELETE_SECTION = 'd'
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class CloudVals:
|
|
"""Engine config values provided by the master server.
|
|
|
|
Used to convey things such as debug logging.
|
|
"""
|
|
|
|
#: Fully qualified type names we should emit extra debug logs for
|
|
#: when garbage-collected (for debugging ref loops).
|
|
gc_debug_types: Annotated[
|
|
list[str], IOAttrs('gct', store_default=False)
|
|
] = field(default_factory=list)
|
|
|
|
#: Max number of objects of a given type to emit debug logs for.
|
|
gc_debug_type_limit: Annotated[int, IOAttrs('gdl', store_default=False)] = 2
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class LoginProxyRequestMessage(Message):
|
|
"""Request send to the cloud to ask for a login-proxy."""
|
|
|
|
@override
|
|
@classmethod
|
|
def get_response_types(cls) -> list[type[Response] | None]:
|
|
return [LoginProxyRequestResponse]
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class LoginProxyRequestResponse(Response):
|
|
"""Response to a request for a login proxy."""
|
|
|
|
# URL to direct the user to for sign in.
|
|
url: Annotated[str, IOAttrs('u')]
|
|
|
|
# URL to use for overlay-web-browser sign ins.
|
|
url_overlay: Annotated[str, IOAttrs('uo')]
|
|
|
|
# Proxy-Login id for querying results.
|
|
proxyid: Annotated[str, IOAttrs('p')]
|
|
|
|
# Proxy-Login key for querying results.
|
|
proxykey: Annotated[str, IOAttrs('k')]
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class LoginProxyStateQueryMessage(Message):
|
|
"""Soo.. how is that login proxy going?"""
|
|
|
|
proxyid: Annotated[str, IOAttrs('p')]
|
|
proxykey: Annotated[str, IOAttrs('k')]
|
|
|
|
@override
|
|
@classmethod
|
|
def get_response_types(cls) -> list[type[Response] | None]:
|
|
return [LoginProxyStateQueryResponse]
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class LoginProxyStateQueryResponse(Response):
|
|
"""Here's the info on that login-proxy you asked about, boss."""
|
|
|
|
class State(Enum):
|
|
"""States a login-proxy can be in."""
|
|
|
|
WAITING = 'waiting'
|
|
SUCCESS = 'success'
|
|
FAIL = 'fail'
|
|
|
|
state: Annotated[State, IOAttrs('s')]
|
|
|
|
# On success, these will be filled out.
|
|
credentials: Annotated[str | None, IOAttrs('tk')]
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class LoginProxyCompleteMessage(Message):
|
|
"""Just so you know, we're done with this proxy."""
|
|
|
|
proxyid: Annotated[str, IOAttrs('p')]
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class PingMessage(Message):
|
|
"""Standard ping."""
|
|
|
|
@override
|
|
@classmethod
|
|
def get_response_types(cls) -> list[type[Response] | None]:
|
|
return [PingResponse]
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class PingResponse(Response):
|
|
"""pong."""
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class TestMessage(Message):
|
|
"""Can I get some of that workspace action?"""
|
|
|
|
testfoo: Annotated[int, IOAttrs('f')]
|
|
|
|
@override
|
|
@classmethod
|
|
def get_response_types(cls) -> list[type[Response] | None]:
|
|
return [TestResponse]
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class TestResponse(Response):
|
|
"""Here's that workspace you asked for, boss."""
|
|
|
|
testfoo: Annotated[int, IOAttrs('f')]
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class WorkspaceFetchState:
|
|
"""Common state data for a workspace fetch."""
|
|
|
|
manifest: Annotated[DirectoryManifest, IOAttrs('m')]
|
|
iteration: Annotated[int, IOAttrs('i')] = 0
|
|
total_deletes: Annotated[int, IOAttrs('tdels')] = 0
|
|
total_downloads: Annotated[int, IOAttrs('tdlds')] = 0
|
|
total_up_to_date: Annotated[int | None, IOAttrs('tunmd')] = None
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class WorkspaceFetchMessage(Message):
|
|
"""Can I get some of that workspace action?"""
|
|
|
|
workspaceid: Annotated[str, IOAttrs('w')]
|
|
state: Annotated[WorkspaceFetchState, IOAttrs('s')]
|
|
|
|
@override
|
|
@classmethod
|
|
def get_response_types(cls) -> list[type[Response] | None]:
|
|
return [WorkspaceFetchResponse]
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class WorkspaceFetchResponse(Response):
|
|
"""Here's that workspace you asked for, boss."""
|
|
|
|
state: Annotated[WorkspaceFetchState, IOAttrs('s')]
|
|
deletes: Annotated[list[str], IOAttrs('dlt', store_default=False)] = field(
|
|
default_factory=list
|
|
)
|
|
downloads_inline: Annotated[
|
|
dict[str, bytes], IOAttrs('dinl', store_default=False)
|
|
] = field(default_factory=dict)
|
|
|
|
done: Annotated[bool, IOAttrs('d')] = False
|
|
|
|
#: If set, the client should treat the sync as failed and display
|
|
#: this message. Allows the server to communicate user-facing errors
|
|
#: without relying on the protocol's ``forward_clean_errors`` flag.
|
|
error: Annotated[
|
|
str | None, IOAttrs('e', soft_default=None, store_default=False)
|
|
] = None
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class MerchAvailabilityMessage(Message):
|
|
"""Can we show merch link?"""
|
|
|
|
@override
|
|
@classmethod
|
|
def get_response_types(cls) -> list[type[Response] | None]:
|
|
return [MerchAvailabilityResponse]
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class MerchAvailabilityResponse(Response):
|
|
"""About that merch..."""
|
|
|
|
url: Annotated[str | None, IOAttrs('u')]
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class SignInMessage(Message):
|
|
"""Can I sign in please?"""
|
|
|
|
login_type: Annotated[LoginType, IOAttrs('l')]
|
|
sign_in_token: Annotated[str, IOAttrs('t')]
|
|
|
|
# For debugging. Can remove soft_default once build 20988+ is ubiquitous.
|
|
description: Annotated[str, IOAttrs('d', soft_default='-')]
|
|
apptime: Annotated[float, IOAttrs('at', soft_default=-1.0)]
|
|
|
|
@override
|
|
@classmethod
|
|
def get_response_types(cls) -> list[type[Response] | None]:
|
|
return [SignInResponse]
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class SignInResponse(Response):
|
|
"""Here's that sign-in result you asked for, boss."""
|
|
|
|
credentials: Annotated[str | None, IOAttrs('c')]
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class ManageAccountMessage(Message):
|
|
"""Message asking for a manage-account url."""
|
|
|
|
weblocation: Annotated[WebLocation, IOAttrs('l')] = (
|
|
WebLocation.ACCOUNT_EDITOR
|
|
)
|
|
|
|
@override
|
|
@classmethod
|
|
def get_response_types(cls) -> list[type[Response] | None]:
|
|
return [ManageAccountResponse]
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class ManageAccountResponse(Response):
|
|
"""Here's that sign-in result you asked for, boss."""
|
|
|
|
url: Annotated[str | None, IOAttrs('u')]
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class StoreQueryMessage(Message):
|
|
"""Message asking about purchasable stuff and store related state."""
|
|
|
|
@override
|
|
@classmethod
|
|
def get_response_types(cls) -> list[type[Response] | None]:
|
|
return [StoreQueryResponse]
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class StoreQueryResponse(Response):
|
|
"""Here's that store info you asked for, boss."""
|
|
|
|
class Result(Enum):
|
|
"""Our overall result."""
|
|
|
|
SUCCESS = 's'
|
|
ERROR = 'e'
|
|
|
|
@dataclass
|
|
class Purchase:
|
|
"""Info about a purchasable thing."""
|
|
|
|
purchaseid: Annotated[str, IOAttrs('id')]
|
|
|
|
# Overall result; all data is undefined if not SUCCESS.
|
|
result: Annotated[Result, IOAttrs('r')]
|
|
|
|
tokens: Annotated[int, IOAttrs('t')]
|
|
gold_pass: Annotated[bool, IOAttrs('g')]
|
|
|
|
available_purchases: Annotated[list[Purchase], IOAttrs('p')]
|
|
token_info_url: Annotated[str, IOAttrs('tiu')]
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class SecureDataCheckerRequest(Message):
|
|
"""Can I get a checker over here?."""
|
|
|
|
@override
|
|
@classmethod
|
|
def get_response_types(cls) -> list[type[Response] | None]:
|
|
return [SecureDataCheckerResponse]
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class SecureDataCheckerResponse(Response):
|
|
"""Here's that checker ya asked for, boss."""
|
|
|
|
checker: Annotated[securedata.Reader, IOAttrs('c')]
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class SecureDataSigningTestRequest(Message):
|
|
"""Ask basn to sign a test payload two ways for client verification.
|
|
|
|
Test-only round-trip used to confirm that ed25519 verify
|
|
(``_babase.verify_ed25519`` in the app binary; ``cryptography``
|
|
fallback in pytest) accepts both master-signed and
|
|
delegate-signed payloads against the embedded
|
|
:data:`~bacommon.securedata.STATIC_DATA_PUBLIC_KEYS`. basn
|
|
handles this without forwarding to bamaster.
|
|
"""
|
|
|
|
@override
|
|
@classmethod
|
|
def get_response_types(cls) -> list[type[Response] | None]:
|
|
return [SecureDataSigningTestResponse]
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class SecureDataSigningTestResponse(Response):
|
|
"""Master- and delegate-signed archives over the same payload.
|
|
|
|
The verifier recovers the original payload via
|
|
:meth:`~bacommon.securedata.Reader.read` regardless of which
|
|
signing flow produced the archive.
|
|
"""
|
|
|
|
#: Archive built with basn's cached static-data master key
|
|
#: (no cert).
|
|
master_archive: Annotated[securedata.Archive, IOAttrs('m')]
|
|
|
|
#: Archive built with basn's delegated
|
|
#: :class:`~bacommon.securedata.Writer` (carries a
|
|
#: master-signed cert).
|
|
delegate_archive: Annotated[securedata.Archive, IOAttrs('d')]
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class ResolvedFlavorManifest:
|
|
"""A resolved flavor-manifest blob, delivered inline (Tier 1).
|
|
|
|
Client-facing counterpart to the basn↔master
|
|
``basntobamaster.ResolvedFlavorManifest``; basn relays it unchanged
|
|
when returning a resolved manifest to the client.
|
|
|
|
The flavor-manifest's canonical JSON bytes travel inline in
|
|
:attr:`data` (they are small — a ``logical_path -> {hash, size}``
|
|
map). The client writes them verbatim into its CAS store under
|
|
:attr:`hash` (sha256-verified), references that hash from its
|
|
top-level cache manifest exactly as the bundled manifest does, and
|
|
parses :attr:`data` (the ``{"e": {logical_path: {"h", "s"}}}``
|
|
shape) to discover the data-blob hashes to fetch via
|
|
``GET /casblob/{hash}`` (Tier 2).
|
|
"""
|
|
|
|
#: sha256 hex of :attr:`data` — the flavor-manifest's CAS hash.
|
|
hash: Annotated[str, IOAttrs('h')]
|
|
|
|
#: Canonical flavor-manifest JSON bytes (stored verbatim client-side
|
|
#: as a CAS blob; its sha256 equals :attr:`hash`).
|
|
data: Annotated[bytes, IOAttrs('d')]
|
|
|
|
|
|
class AssetPackageResolveError(Enum):
|
|
"""Why an asset-package resolve failed (structured for client branching).
|
|
|
|
Travels back to the client on :class:`ResolveAssetPackageResponse` so
|
|
the runtime can react precisely (e.g. prompt for sign-in on
|
|
``AUTH_REQUIRED``) rather than parsing the human-readable ``error``
|
|
string.
|
|
"""
|
|
|
|
#: Caller is unauthenticated and the version is non-public; signing in
|
|
#: with an account that has access may resolve it.
|
|
AUTH_REQUIRED = 'auth'
|
|
#: Caller is authenticated but lacks access to this (non-public)
|
|
#: version (not the owner / not on the package's dev team).
|
|
ACCESS_DENIED = 'access'
|
|
#: The requested asset-package-version id is unknown / invalid.
|
|
NOT_FOUND = 'notfound'
|
|
#: A requested dimension value was invalid (texture profile/quality,
|
|
#: language, etc.).
|
|
INVALID = 'invalid'
|
|
#: An internal/assemble error occurred server-side.
|
|
INTERNAL = 'internal'
|
|
#: The client build is too old to address current asset-package
|
|
#: manifests (which use clean source-named logical paths); the user
|
|
#: must update. Clients predating the build-number field also land
|
|
#: here.
|
|
CLIENT_TOO_OLD = 'tooold'
|
|
#: The package's own source content failed to build — a problem the
|
|
#: package author can fix (e.g. a malformed sound or texture file).
|
|
#: The human-readable ``error`` names the offending source file(s);
|
|
#: clients should surface it verbatim. Old clients see this as
|
|
#: ``INTERNAL`` via ``enum_fallback``.
|
|
CONTENT = 'content'
|
|
|
|
|
|
class AssetPackageBuildPhase(Enum):
|
|
"""Coarse phase of an in-progress server-side asset-package build.
|
|
|
|
Client-facing and deliberately decoupled from internal cloud-build
|
|
state — the master translates its build status into this. Combined
|
|
with the optional counts on :class:`AssetPackageBuildProgress` it
|
|
lets the client render a localized progress message (today English
|
|
only; structured so it can be translated later).
|
|
"""
|
|
|
|
#: Spinning up / queued — the build hasn't begun real work yet.
|
|
PREPARING = 'prep'
|
|
#: Building the package's constituent assets.
|
|
BUILDING = 'build'
|
|
#: Assembling built outputs into the final manifest.
|
|
FINALIZING = 'final'
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class AssetPackageBuildProgress:
|
|
"""Progress of a server-side asset-package build, for the client.
|
|
|
|
Returned on :class:`ResolveAssetPackageResponse` when a resolve
|
|
can't be satisfied immediately because the master is (re)building
|
|
the requested flavors (a build can take noticeably longer than a
|
|
plain download). The client renders this and re-sends the same
|
|
resolve to poll until the manifest is ready.
|
|
|
|
Intentionally a resolve-specific, client-facing type — it does
|
|
**not** expose internal cloud-build types. The master translates
|
|
its build status into this at the resolve boundary.
|
|
"""
|
|
|
|
#: Coarse phase, for a phase-appropriate (localized) message.
|
|
phase: Annotated[AssetPackageBuildPhase, IOAttrs('p')]
|
|
|
|
#: Optional 'done of total' unit counts (e.g. assets built) for a
|
|
#: progress readout. Typically both set or both unset.
|
|
units_done: Annotated[int | None, IOAttrs('ud', store_default=False)] = None
|
|
units_total: Annotated[int | None, IOAttrs('ut', store_default=False)] = (
|
|
None
|
|
)
|
|
|
|
#: Optional free-form text shown as-is (untranslated) — an escape
|
|
#: hatch for custom/modder build status the fixed phases can't
|
|
#: express.
|
|
detail: Annotated[str | None, IOAttrs('d', store_default=False)] = None
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class ResolveAssetPackageMessage(Message):
|
|
"""Resolve an asset-package-version's manifest for download (Tier 1).
|
|
|
|
Sent by a client to the basn node it is connected to when it needs
|
|
to download an asset package. basn resolves the manifest via
|
|
bamaster on the client's behalf and returns, per bucket, the
|
|
resolved flavor-manifest blob (its CAS hash + canonical bytes,
|
|
inline) plus a short-lived capability token. The client then fetches
|
|
each data blob the flavor-manifests reference from the same node via
|
|
``GET /casblob/{hash}`` (Tier 2), presenting the token.
|
|
|
|
Texture dimensions travel as plain strings (the ``TextureProfile``
|
|
/ ``TextureTier`` enum values) so this module stays decoupled
|
|
from the master-only ``baserver.workspace.assetsv1``; the master
|
|
converts + validates them.
|
|
|
|
The requesting account is conveyed via the standard
|
|
account-session-channel sidecar that basn auto-attaches to every
|
|
client message (no explicit field needed): a signed-in client
|
|
resolves against its account (required for non-public DEV/TEST
|
|
versions), while an anonymous client resolves PROD versions only
|
|
(which are public).
|
|
"""
|
|
|
|
#: Fully-qualified ``account.package.version`` id to resolve.
|
|
apverid: Annotated[str, IOAttrs('a')]
|
|
|
|
#: Chosen locale for the ``language`` bucket.
|
|
language: Annotated[Locale, IOAttrs('l')]
|
|
|
|
#: Chosen ``TextureProfile`` value (e.g. ``'fallback_v1'``).
|
|
texture_profile: Annotated[str, IOAttrs('tp')]
|
|
|
|
#: Chosen ``TextureTier`` value (e.g. ``'regular'``). Wire key stays
|
|
#: the historical ``'tq'`` (it predates the tier/quality rename) so
|
|
#: this message stays compatible with un-migrated basn nodes and
|
|
#: older clients — construct-mode asset resolve sends it on every
|
|
#: boot, so the key must not break across versions.
|
|
texture_tier: Annotated[str, IOAttrs('tq')]
|
|
|
|
#: The client's engine build number. basn relays it to master, which
|
|
#: gates the resolve on it (clients too old to address current
|
|
#: source-named manifests get a ``CLIENT_TOO_OLD`` error). Soft-
|
|
#: defaults to 0 so older clients (and un-migrated basn) read as
|
|
#: build 0 -- always below the floor.
|
|
build_number: Annotated[int, IOAttrs('bn', soft_default=0)] = 0
|
|
|
|
@override
|
|
@classmethod
|
|
def get_response_types(cls) -> list[type[Response] | None]:
|
|
return [ResolveAssetPackageResponse]
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class ResolveAssetPackageResponse(Response):
|
|
"""Resolved flavor-manifests + capability token for a download.
|
|
|
|
``buckets`` maps a ``bucket/flavor`` coordinate (e.g.
|
|
``'textures/fallback_v1.gamma.regular'``, ``'constant'``) to that
|
|
flavor's resolved flavor-manifest blob (CAS hash + canonical bytes,
|
|
delivered inline). On failure ``error`` carries a human-readable
|
|
message and ``buckets`` is empty / ``token`` is ``None``.
|
|
"""
|
|
|
|
#: Human-readable failure message (bad apverid, access denied, bad
|
|
#: dimension value, assemble failure), or ``None`` on success.
|
|
error: Annotated[str | None, IOAttrs('e')]
|
|
|
|
#: Structured failure reason accompanying ``error`` (lets the client
|
|
#: branch — e.g. prompt for sign-in — without parsing the message).
|
|
#: ``None`` on success, or when an older server didn't supply one.
|
|
#: ``enum_fallback`` makes a *future* unrecognized code degrade to
|
|
#: ``INTERNAL`` (message decode is lossy) instead of failing the whole
|
|
#: response — so a new error reason never bricks an older client's
|
|
#: resolve; it just surfaces the human ``error`` under a generic code.
|
|
error_code: Annotated[
|
|
AssetPackageResolveError | None,
|
|
IOAttrs(
|
|
'ec',
|
|
soft_default=None,
|
|
enum_fallback=AssetPackageResolveError.INTERNAL,
|
|
),
|
|
]
|
|
|
|
#: ``bucket/flavor`` coordinate -> the resolved flavor-manifest blob
|
|
#: for that flavor. Empty when ``error`` is set.
|
|
buckets: Annotated[dict[str, ResolvedFlavorManifest], IOAttrs('b')]
|
|
|
|
#: Short-lived capability token the client presents to
|
|
#: ``GET /casblob/{hash}`` to fetch the resolved blobs. ``None``
|
|
#: when ``error`` is set.
|
|
token: Annotated[securedata.Archive | None, IOAttrs('tok')]
|
|
|
|
#: Set when the manifest isn't ready yet because the master is
|
|
#: building the requested flavors; the client renders this progress
|
|
#: and re-sends the same resolve to poll. ``None`` once resolved
|
|
#: (then ``buckets`` / ``token`` are populated) or on ``error``.
|
|
build_progress: Annotated[
|
|
AssetPackageBuildProgress | None, IOAttrs('bp', soft_default=None)
|
|
]
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class CloudValsRequest(Message):
|
|
"""Can a fella get some cloud vals around here?."""
|
|
|
|
@override
|
|
@classmethod
|
|
def get_response_types(cls) -> list[type[Response] | None]:
|
|
return [CloudValsResponse]
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class CloudValsResponse(Response):
|
|
"""Here's them cloud vals ya asked for, boss."""
|
|
|
|
vals: Annotated[CloudVals, IOAttrs('v')]
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class ChestActionMessage(Message):
|
|
"""Request action about a chest."""
|
|
|
|
class Action(Enum):
|
|
"""Types of actions we can request."""
|
|
|
|
# Unlocking (for free or with tokens).
|
|
UNLOCK = 'u'
|
|
|
|
# Watched an ad to reduce wait.
|
|
AD = 'ad'
|
|
|
|
action: Annotated[Action, IOAttrs('a')]
|
|
|
|
# Tokens we are paying (only applies to unlock).
|
|
token_payment: Annotated[int, IOAttrs('t')]
|
|
|
|
chest_id: Annotated[str, IOAttrs('i')]
|
|
|
|
@override
|
|
@classmethod
|
|
def get_response_types(cls) -> list[type[Response] | None]:
|
|
return [ChestActionResponse]
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class ChestActionResponse(Response):
|
|
"""Here's the results of that action you asked for, boss."""
|
|
|
|
# Tokens that were actually charged.
|
|
tokens_charged: Annotated[int, IOAttrs('t')] = 0
|
|
|
|
# If present, signifies the chest has been opened and we should show
|
|
# the user this stuff that was in it.
|
|
contents: Annotated[list[ditm.Wrapper] | None, IOAttrs('c')] = None
|
|
|
|
# If contents are present, which of the chest's prize-sets they
|
|
# represent.
|
|
prizeindex: Annotated[int, IOAttrs('i')] = 0
|
|
|
|
# Printable error if something goes wrong.
|
|
error: Annotated[str | None, IOAttrs('e')] = None
|
|
|
|
# Printable warning. Shown in orange with an error sound. Does not
|
|
# mean the action failed; only that there's something to tell the
|
|
# users such as 'It looks like you are faking ad views; stop it or
|
|
# you won't have ad options anymore.'
|
|
warning: Annotated[str | None, IOAttrs('w', store_default=False)] = None
|
|
|
|
# Printable success message. Shown in green with a cash-register
|
|
# sound. Can be used for things like successful wait reductions via
|
|
# ad views. Used in builds earlier than 22311; can remove once
|
|
# 22311+ is ubiquitous.
|
|
success_msg: Annotated[str | None, IOAttrs('s', store_default=False)] = None
|
|
|
|
# Effects to show on the client. Replaces warning and success_msg in
|
|
# build 22311 or newer.
|
|
effects: Annotated[
|
|
list[clfx.Effect], IOAttrs('fx', store_default=False)
|
|
] = field(default_factory=list)
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class FulfillDocUIRequest(Message):
|
|
"""Can a fella get a doc-ui round here?"""
|
|
|
|
request: Annotated[DocUIRequest, IOAttrs('r')]
|
|
domain: Annotated[str, IOAttrs('d')]
|
|
|
|
@override
|
|
@classmethod
|
|
def get_response_types(cls) -> list[type[Response] | None]:
|
|
return [FulfillDocUIResponse]
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class FulfillDocUIResponse(Response):
|
|
"""Here's that doc-ui you asked for, boss."""
|
|
|
|
response: Annotated[DocUIResponse, IOAttrs('r')]
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class AnalyticsEventMessage(Message):
|
|
"""Have a nice analytics event!"""
|
|
|
|
event: Annotated[AnalyticsEvent, IOAttrs('e')]
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class AuthRequestMessage(Message):
|
|
"""Request access to a server for a current account."""
|
|
|
|
global_app_instance_uuid: Annotated[str, IOAttrs('a')]
|
|
|
|
@override
|
|
@classmethod
|
|
def get_response_types(cls) -> list[type[Response] | None]:
|
|
return [AuthRequestResponse]
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class AuthRequestResponse(Response):
|
|
"""Here's that access ya asked for boss."""
|
|
|
|
error: Annotated[str | None, IOAttrs('e')]
|
|
token: Annotated[str | None, IOAttrs('t')]
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class TransientAPIKeyRequest(Message):
|
|
"""Request a transient API key for the currently signed-in account."""
|
|
|
|
@override
|
|
@classmethod
|
|
def get_response_types(cls) -> list[type[Response] | None]:
|
|
return [TransientAPIKeyResponse]
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class TransientAPIKeyResponse(Response):
|
|
"""Response to a transient API key request."""
|
|
|
|
class Error(Enum):
|
|
"""Failure modes."""
|
|
|
|
INTERNAL_ERROR = 'ie'
|
|
KEY_LIMIT_REACHED = 'klr'
|
|
|
|
key: Annotated[str | None, IOAttrs('k')]
|
|
error: Annotated[Error | None, IOAttrs('e')]
|