bacommon package

Functionality and data shared by all Ballistica components.

This includes clients, various servers, tools, etc.

Subpackages

Submodules

bacommon.analytics module

Analytics support.

class bacommon.analytics.AnalyticsEvent[source]

Bases: IOMultiType[AnalyticsEventTypeID]

Top level class for our multitype.

classmethod get_type(type_id: AnalyticsEventTypeID) type[AnalyticsEvent][source]

Return the subclass for each of our type-ids.

classmethod get_type_id() AnalyticsEventTypeID[source]

Return the type-id for this subclass.

classmethod get_type_id_storage_name() str[source]

Return the key used to store type id in serialized data.

The default is a short obscure value so that it is unlikely to conflict with members of individual type attrs, but in some cases one might prefer to serialize it to something simpler like ‘type’ by overriding this call. One just needs to make sure that no encompassed types serialize anything to that same name themself (dataclassio will error if they do).

class bacommon.analytics.AnalyticsEventTypeID(*values)[source]

Bases: Enum

Type ID for each of our subclasses.

CLASSIC = 'c'
class bacommon.analytics.ClassicAnalyticsEvent(eventtype: EventType, extra: str | None = None)[source]

Bases: AnalyticsEvent

Analytics event related to classic.

class EventType(*values)[source]

Bases: Enum

Types of classic events.

JOIN_NEARBY_PARTY = 'jn'
JOIN_PARTY_BY_ADDRESS = 'ja'
JOIN_PRIVATE_PARTY = 'jpr'
JOIN_PUBLIC_PARTY = 'jpb'
START_COOP_SESSION = 'sc'
START_FFA_SESSION = 'sf'
START_TEAMS_SESSION = 'st'
START_TOURNEY_COOP_SESSION = 'stc'
eventtype: EventType
extra: str | None = None
classmethod get_type_id() AnalyticsEventTypeID[source]

Return the type-id for this subclass.

bacommon.app module

Common high level values/functionality related to Ballistica apps.

class bacommon.app.AppArchitecture(*values)[source]

Bases: Enum

Processor architecture an app can be running on.

ARM = 'arm'
ARM64 = 'arm64'
UNKNOWN = 'unknown'
X86 = 'x86'
X86_64 = 'x86_64'
class bacommon.app.AppInterfaceIdiom(*values)[source]

Bases: Enum

A general form-factor or method of experiencing a Ballistica app.

Note that it may be possible for a running app to switch idioms (for instance if a mobile device or computer is connected to a TV).

CONSOLE = 'console'

Screen with medium amount of detail visible; assumed to have game controller(s) as primary input. Note that this covers handheld or arcade cabinet scenarios in addition to tv-connected consoles.

DESKTOP = 'desktop'

Screen with high amount of detail visible; assumed to have keyboard/mouse as primary input.

HEADLESS = 'headless'

The app has no interface (generally is acting as a server).

PHONE = 'phone'

Small screen; assumed to have touch as primary input.

TABLET = 'tablet'

Medium size screen; assumed to have touch as primary input.

XR_HEADSET = 'xr_headset'

Displayed over or in place of of the real world on a headset; assumed to have hand tracking or spatial controllers as primary input.

XR_PHONE = 'xr_phone'

Displayed over or instead of the real world on a small screen; assumed to have device movement augmented by physical or touchscreen controls as primary input.

XR_TABLET = 'xr_tablet'

Displayed over or instead of the real world on a medium size screen; assumed to have device movement augmented by physical or touchscreen controls as primary input.

class bacommon.app.AppPlatform(*values)[source]

Bases: Enum

Overall platform a build can target.

Each distinct flavor of an app has a unique combination of AppPlatform and AppVariant. Generally platform describes a set of hardware, while variant describes a destination or purpose for the build.

ANDROID = 'android'
IOS = 'ios'
LINUX = 'linux'
MACOS = 'macos'
TVOS = 'tvos'
UNKNOWN = 'unknown'
WINDOWS = 'windows'
class bacommon.app.AppVariant(*values)[source]

Bases: Enum

A unique Ballistica build variation within a single platform.

Each distinct permutation of an app has a unique combination of AppPlatform and AppVariant. Generally platform describes a set of hardware, while variant describes a destination or purpose for the build.

AMAZON_APPSTORE = 'amazon_appstore'
APPLE_APP_STORE = 'apple_app_store'
ARCADE = 'arcade'
CARDBOARD = 'cardboard'
DEMO = 'demo'
EPIC_GAMES_STORE = 'epic_games_store'
GENERIC = 'generic'

Default builds.

GOOGLE_PLAY = 'google_play'
META = 'meta'
SERVER = 'server'

Server bundles we distribute for third-party operators (such as those on the ballistica.net downloads page).

SERVER_BASN = 'server_basn'

Our own cloud-hosted game servers.

STEAM = 'steam'
TEST_BUILD = 'test_build'

Particular builds intended for public testing (may have some extra checks or logging enabled).

WINDOWS_STORE = 'windows_store'
class bacommon.app.ExitCode(*values)[source]

Bases: Enum

Process exit codes a Ballistica app may return on a clean shutdown.

These accompany a clean (non-crash) shutdown: the app tears down gracefully via the normal shutdown sequence and simply returns one of these as its process exit code. This is distinct from a fatal-error/abort exit (which routes through crash reporting); a nonzero value here means “we shut down cleanly, but the run was a failure”. Supervisors such as the headless server-wrapper script (and BASN task orchestration) interpret nonzero values to decide whether restarting is worthwhile.

ASSET_BRINGUP_FAILED = 10

A headless app could not bring up assets it requires to run (e.g. a construct-mode asset resolve failed terminally). Not transient — a restart with the same inputs would fail the same way — so supervisors should treat it as terminal and not auto-restart.

SUCCESS = 0

Normal successful exit.

bacommon.assetcas module

Shared content-addressed asset-blob download primitive.

Used by both the game client’s asset subsystem and the bacloud CLI to fetch content-addressed blobs from a basn node’s /casblob endpoint, verify them, and write them into a local content-addressed cache. The logic here is engine-agnostic: the caller supplies the urllib3 pool, the node base URL, the encoded capability token, and the destination cache root, and owns concurrency, retry, and progress reporting itself.

bacommon.assetcas.CAS_INTEGRITY_MARKER = 'CAS-INTEGRITY'

Marker prefixed to the two failures that mean “we received bytes and they were wrong” (hash mismatch, decompress failure) as opposed to a server saying no. Both are logged at ERROR and are greppable by this token, because we currently have no data on whether they happen in the field – they would be either corruption in transit or a node serving bytes that don’t match their hash, and those want very different responses. Retrying them is deliberately NOT done until there’s evidence; see docs/followups.md (2026-08-07).

exception bacommon.assetcas.CasDownloadError[source]

Bases: Exception

A CAS-blob fetch, hash-verify, or write failed.

bacommon.assetcas.blob_present(roots: list[str], filehash: str, size: int) bool[source]

Is this CAS blob already present at the expected size in any root?

Present-but-wrong-size counts as absent (so it gets refetched and overwritten). The free st_size check is a cheap catch for external truncation or tampering, not a content proof.

bacommon.assetcas.cas_blob_path(root: str, filehash: str) str[source]

Return the path a CAS blob occupies under a cache root.

Blobs are sharded 256 ways by the first two hex chars of the hash to keep per-directory counts low for cache scanning.

bacommon.assetcas.cas_write(dest_root: str, filehash: str, data: bytes) None[source]

sha256-verify data then atomically write it into dest_root.

Verify, write to a temp file in the destination directory, fsync, then os.replace (atomic on the same filesystem). A file at its CAS path is therefore always whole-and-correct: a crash mid-write leaves only a temp file, never a partial blob at the final path. Raises CasDownloadError on a hash mismatch.

bacommon.assetcas.download_cas_blob(pool: PoolManager, base_url: str, filehash: str, size: int, *, token_header: str, dest_root: str, timeout_seconds: float) None[source]

Fetch one CAS blob from a node and atomically write it.

Issues GET {base_url}/casblob/{filehash}?size={size} with the capability token in the X-Asset-Token header and an X-Accept-Compression header advertising every encoding this build can decode, so the node fulfills the request in whichever supported encoding is smallest/available. filehash and size are the blob’s canonical (uncompressed) identity. The encoding of the received bytes is taken solely from the node’s X-Cas-Compression response header – it is authoritative, so whatever the node actually served is decoded correctly regardless of how it was cached. (The flavor-manifest no longer carries compression; encoding is purely a transfer-layer concern negotiated here.) The bytes are decompressed back to canonical before cas_write() sha256-verifies them against filehash and writes them – so the local cache always holds uncompressed blobs and the hash check still validates content. One attempt only; the caller owns concurrency and retry. Callers do retry – the client’s asset subsystem loops on transient network failures – so do not add a loop here as well, or attempts (and the per-attempt timeout budget) multiply. Raises CasDownloadError on a non-200 response, a missing/unknown served-encoding header, a decompress failure, or a verify failure.

bacommon.assetcas.encode_asset_token(token: securedata.Archive) str[source]

Encode a capability token for the X-Asset-Token header.

Returns base64-urlsafe of the token’s canonical JSON (HTTP headers don’t carry raw JSON cleanly).

bacommon.assetcas.parse_flavor_manifest_blobs(data: bytes | str) dict[str, int][source]

Extract a flavor-manifest’s data-blob map from its canonical JSON.

Parses the {"e": {logical_path: {part: {"h", "s"}}}} shape and returns every referenced data blob as a content-sha256-hex -> canonical-byte-size map, in manifest order (the order clients download). The single shared reader for this shape – resolve grant-building, /casblob scope verification, and tests all go through it. Raises ValueError on a malformed manifest.

bacommon.bacloud module

Functionality related to the bacloud tool.

Warning

This is an internal api and subject to change at any time. Do not use it in mod code.

bacommon.build module

Functionality related to game builds.

Warning

This is an internal api and subject to change at any time. Do not use it in mod code.

bacommon.clienteffect module

ClientEffect related functionality.

Warning

This is an internal api and subject to change at any time. Do not use it in mod code.

bacommon.clienteffect.V2_EFFECTS_MIN_BUILD = 22931

First engine build carrying the v2 client-effect machinery (ScreenMessageV2/PlaySoundV2 + resolve-before-run). Servers use this to emit the right form per client build.

bacommon.cloud module

Cloud related functionality.

Warning

This is an internal api and subject to change at any time. Do not use it in mod code.

bacommon.cloudfilecodec module

zstd (de)compression for cloud-file blobs.

Holds the CompressionType enum (how a stored blob is encoded) plus the codec dispatch mapping each type to a concrete zstd operation – including which pre-shared dictionary a dict-based type uses. Lives in bacommon so both server components (which compress on store) and client components (which read the compression type from an asset manifest and decompress on load) share one source of truth for the type->codec mapping and load identical dictionary bytes.

Compression is always expressed relative to a cloud-file’s canonical (uncompressed) content: a blob’s stored bytes are the canonical content run through its CompressionType. UNCOMPRESSED means the stored bytes are the canonical content.

bacommon.cloudfilecodec.CAS_ACCEPT_COMPRESSION_HEADER = 'X-Accept-Compression'

a comma-separated list of CompressionType values the client can decode (e.g. 'zb1,z,u'). Lets the server fulfill the request in any encoding the client supports – picking the smallest available – rather than assuming a fixed one. A client always includes u (it can always handle uncompressed). Absent ⇒ a legacy client that decodes per its manifest (back-compat).

Type:

Request header on a CAS /casblob GET

bacommon.cloudfilecodec.CAS_COMPRESSION_HEADER = 'X-Cas-Compression'

the single CompressionType value the served bytes are encoded in. The client decodes per THIS header (not its manifest), so whatever the node actually serves – even a re-negotiated or differently-cached encoding – is always decoded correctly. Absent ⇒ a legacy node; the client falls back to its manifest’s compression.

Type:

Response header on a CAS /casblob GET

class bacommon.cloudfilecodec.CompressionType(*values)[source]

Bases: Enum

How a blob’s stored bytes are compressed.

New members can be added as we see fit. Each member must encode enough to decompress deterministically – dictionary-based members bake in the exact dictionary and version, so a stored blob is always decodable by whatever holds that member. The enum value is a stable wire string: it travels in asset manifests for the client to read, so values must never change.

UNCOMPRESSED = 'u'

Stored bytes are the canonical content verbatim.

ZSTD = 'z'

Stored bytes are the canonical content run through plain zstd.

ZSTD_DICT_BOB_V1 = 'zb1'

Stored bytes are the canonical content run through zstd using the v1 display-mesh (.bob) dictionary (bacommon.meshzstddict.display_mesh_dict_v1()). The dict and its version are fixed by this member.

bacommon.cloudfilecodec.all_compression_types() set[CompressionType][source]

All CompressionType members this build can decode.

What a client advertises in CAS_ACCEPT_COMPRESSION_HEADER – every member is decodable here because the codecs (and any bundled dictionaries) ship with the build.

bacommon.cloudfilecodec.compress_for_type(canonical: bytes, ctype: CompressionType, *, level: int) bytes[source]

Encode canonical bytes per ctype.

The canonical (uncompressed) content identified by a cloud-file’s cloud_file_id. Inverse of decompress_for_type(). This is the single source of truth for which dictionary a dict-based type uses.

bacommon.cloudfilecodec.decompress_for_type(stored: bytes, ctype: CompressionType) bytes[source]

Decode stored bytes per ctype back to canonical content.

Inverse of compress_for_type().

bacommon.cloudfilecodec.format_compression_accept(types: Iterable[CompressionType]) str[source]

Encode a set of supported types for the accept header.

bacommon.cloudfilecodec.parse_compression_accept(value: str | None) set[CompressionType][source]

Decode the accept header into a set of known types.

Tolerant of unknown/empty entries (an unrecognized future value is skipped, not an error) so an old server reading a newer client’s header simply ignores types it doesn’t know.

bacommon.cloudfilecodec.zstd_compress(data: bytes, level: int) bytes[source]

Compress data with plain zstd at the given level.

bacommon.cloudfilecodec.zstd_compress_with_dict(data: bytes, dict_bytes: bytes, level: int) bytes[source]

Compress data with zstd using a pre-shared dictionary.

bacommon.cloudfilecodec.zstd_decompress(data: bytes) bytes[source]

Decompress plain-zstd data produced by zstd_compress().

bacommon.cloudfilecodec.zstd_decompress_with_dict(data: bytes, dict_bytes: bytes) bytes[source]

Decompress dict-zstd data using its pre-shared dictionary.

dict_bytes must be the exact dictionary used to compress data; a mismatch raises or yields garbage.

bacommon.consolechannel module

Payload types for the cloud-console SmartSocket channel kind.

One root pair per channel kind (see the hierarchy-per-contract rule in streamcall-smartsocket.md): commands go up from the console as ConsoleCommand, everything it displays comes back down as ConsoleEvent. The relay never decodes either – a new event type must never require a basn rollout.

Payloads here are pure data. In particular they carry log entries and levels, not pre-colored html; rendering belongs to whoever displays them. That is both the payload-model rule and one less hand-rolled escaping site than the polling console this replaced.

Client-visible, because the app itself can hold the far end of a console session. That makes these definitions a public wire contract once a build that speaks them ships: storage names and type-id values may never be repurposed, and removed ones stay retired. Add rather than change.

class bacommon.consolechannel.ConsoleCommand[source]

Bases: IOMultiType[ConsoleCommandTypeID]

Something the console’s user asked us to do.

classmethod get_type(type_id: ConsoleCommandTypeID) type[ConsoleCommand][source]

Return a specific subclass given a type-id.

Should be overridden by child classes. Generally, users of the class should call get_type_cached() instead of this, as it is more efficient.

classmethod get_type_id() ConsoleCommandTypeID[source]

Return the type-id for this subclass.

class bacommon.consolechannel.ConsoleCommandTypeID(*values)[source]

Bases: Enum

Type IDs for browser-to-adapter payloads.

EXEC = 'e'
class bacommon.consolechannel.ConsoleContact(*values)[source]

Bases: Enum

How we’re getting along with the target.

LOST = 'l'

It is gone for good. The session ends after this, and the console must get a fresh handle to reach whatever replaced it.

OK = 'o'

Talking to it normally.

UNREACHABLE = 'u'

Transient trouble reaching it; we keep trying.

class bacommon.consolechannel.ConsoleEvent[source]

Bases: IOMultiType[ConsoleEventTypeID]

Something for the console to display.

classmethod get_type(type_id: ConsoleEventTypeID) type[ConsoleEvent][source]

Return a specific subclass given a type-id.

Should be overridden by child classes. Generally, users of the class should call get_type_cached() instead of this, as it is more efficient.

classmethod get_type_id() ConsoleEventTypeID[source]

Return the type-id for this subclass.

class bacommon.consolechannel.ConsoleEventTypeID(*values)[source]

Bases: Enum

Type IDs for adapter-to-browser payloads.

EXEC_ACK = 'e'
GAP = 'g'
INSTANCE = 'i'
LOG_ENTRIES = 'l'
PERMISSION = 'p'
STATUS = 's'
class bacommon.consolechannel.ConsolePermission(*values)[source]

Bases: Enum

Where our permission to control the target stands.

A console session is offered to the target regardless; what this tracks is whether the person at the device has agreed to let it do anything. Until they have, no log flows and no code runs.

DENIED = 'd'

Refused, or nobody answered. Retrying immediately just puts the same question in front of the same person, so a console that sees this should stop asking rather than loop.

GRANTED = 'g'

Allowed. Log flows and commands run.

PENDING = 'p'

Asked, and waiting on someone at the device to answer.

class bacommon.consolechannel.ExecAckEvent(line_count: int, target_time: float)[source]

Bases: ConsoleEvent

We handed some code to the target.

Worth its own event because code with no output is otherwise indistinguishable from code that never arrived.

classmethod get_type_id() ConsoleEventTypeID[source]

Return the type-id for this subclass.

line_count: int
target_time: float

The target’s own clock when it ran, so the console can line the exec up against surrounding log timestamps.

class bacommon.consolechannel.ExecCommand(code: str)[source]

Bases: ConsoleCommand

Run some code on the target.

code: str
classmethod get_type_id() ConsoleCommandTypeID[source]

Return the type-id for this subclass.

class bacommon.consolechannel.GapEvent(dropped: int)[source]

Bases: ConsoleEvent

Log lines we can never show.

The target keeps a bounded archive, so entries can age out before we ask for them. Derived from the archive’s start index running ahead of what we requested, not sent by the target.

dropped: int
classmethod get_type_id() ConsoleEventTypeID[source]

Return the type-id for this subclass.

class bacommon.consolechannel.InstanceEvent(target_uuid: str, device_name: str | None = None, replaced_previous: bool = False)[source]

Bases: ConsoleEvent

Which app instance we’re now talking to.

Sent on first contact and whenever the target’s uuid changes, which is how a restarted app announces itself. A change means the log we were following is gone, so the console starts a fresh one.

device_name: str | None = None

Display name of the device, when we know one.

classmethod get_type_id() ConsoleEventTypeID[source]

Return the type-id for this subclass.

replaced_previous: bool = False

True when this replaced an earlier instance rather than being the first one we saw – the console words those differently.

target_uuid: str
class bacommon.consolechannel.LogEntriesEvent(entries: list[LogEntry], next_index: int = 0)[source]

Bases: ConsoleEvent

A slice of the target’s log.

entries: list[LogEntry]
classmethod get_type_id() ConsoleEventTypeID[source]

Return the type-id for this subclass.

next_index: int = 0

Where the target’s log stands after these entries. The console hands this back when it asks for a new handle, so a replacement session resumes instead of replaying everything the target still has cached.

class bacommon.consolechannel.PermissionEvent(state: ConsolePermission, message: str | None = None)[source]

Bases: ConsoleEvent

Whether the target has agreed to be controlled.

Sent on every channel, including re-announcing the settled state after a reconnect, so a console that joins late still knows why it is (or isn’t) seeing anything.

classmethod get_type_id() ConsoleEventTypeID[source]

Return the type-id for this subclass.

message: str | None = None

Optional human-readable detail. English is fine; this surface is admin-only.

state: ConsolePermission
class bacommon.consolechannel.StatusEvent(contact: ConsoleContact, message: str | None = None)[source]

Bases: ConsoleEvent

A change in our ability to reach the target.

contact: ConsoleContact
classmethod get_type_id() ConsoleEventTypeID[source]

Return the type-id for this subclass.

message: str | None = None

Optional human-readable detail. English is fine; this surface is admin-only.

bacommon.displayitem module

Functionality for displaying currencies, prizes, owned items, etc.

Warning

This is an internal api and subject to change at any time. Do not use it in mod code.

bacommon.locale module

Functionality for wrangling locale info.

bacommon.locale.LANGUAGE_PICKER_SEPARATOR = '·'

Separator between a language picker entry’s two halves.

This is a workaround for an engine text-layout quirk, and the exact character matters. Do not “clean this up” to an em dash.

The game client absorbs punctuation into an adjacent OS-rendered text span on purpose, so punctuation next to OS-drawn glyphs doesn’t visually mismatch it (TextGraphics::IsOSDrawableAscii, whose comment says as much). The layout loop is roughly:

if (IsOSDrawableAscii(val) && !os_span.empty()) {
    os_span.push_back(val);          // absorbed
} else if (Glyph* g = GetGlyph(val, big)) {
    ...                              // baked glyph; CLOSES the span
} else {
    os_span.push_back(val);          // no glyph -> absorbed anyway
}

An RTL endonym opens an OS span. A separator that gets absorbed joins that span, so bidi runs over “<arabic> - “ as one unit, resolves the trailing neutrals against an RTL paragraph, and draws them on the left: - العربية Arabic. A separator that instead closes the span leaves the Arabic alone in it, where it shapes correctly.

U+00B7 MIDDLE DOT closes it because it clears both gates, and each is independently required:

  1. It is above every ASCII range IsOSDrawableAscii tests (32-47, 58-64, 91-96, 123-126), so it is not absorbed. An ASCII hyphen (45) sits in the first range and IS absorbed – which is why swapping the em dash for a hyphen changed nothing.

  2. It is under kGlyphCount (1280) and g_glyph_map[183] is a real font page, so it has a baked glyph and takes the span-closing branch.

Miss either and it breaks: an em dash (U+2014) clears gate 1 but is above 1280, so it has no baked glyph, falls to the final else, and is absorbed regardless. Same for a bullet (U+2022). So the rule is NOT “use any non-ASCII character” – it is “128-1279 with a baked glyph”, or one of the ASCII characters IsOSDrawableAscii explicitly excludes (parens, quotes, brackets).

Parentheses would work too and were the original form, but they nest badly: the qualified variants already carry their own parenthetical on both sides, giving Português (Brasil) (Portuguese (Brazil)).

Verified on Android 2026-07-30. That generalizes across platforms in a way the earlier bidi-isolate attempt did not: this hinges on the baked font sheet and on platform-independent engine code, not on each platform’s text stack. Full trace, including two failed approaches, in ballistica-internal docs/followups.md.

class bacommon.locale.Locale(*values)[source]

Bases: Enum

A distinct grouping of language, cultural norms, etc.

This list of locales is considered ‘sacred’ - we assume any values (and associated long values) added here remain in use out in the wild indefinitely. If a locale is superseded by a newer or more specific one, the new locale should be added and both new and old should map to the same LocaleResolved.

ARABIC = 'arabc'
BELARUSSIAN = 'blrs'
CHINESE = 'chn'
CHINESE_SIMPLIFIED = 'chn_sim'
CHINESE_TRADITIONAL = 'chn_tr'
CROATIAN = 'croat'
CZECH = 'czch'
DANISH = 'dnsh'
DUTCH = 'dtch'
ENGLISH = 'eng'
ESPERANTO = 'esprnto'
FILIPINO = 'filp'
FRENCH = 'frnch'
GERMAN = 'grmn'
GIBBERISH = 'gibber'
GREEK = 'greek'
HINDI = 'hndi'
HUNGARIAN = 'hngr'
INDONESIAN = 'indnsn'
ITALIAN = 'italn'
JAPANESE = 'jpn'
KAZAKH = 'kazk'
KOREAN = 'kor'
MALAY = 'mlay'
PERSIAN = 'pers'
PIRATE_SPEAK = 'pirate'
POLISH = 'pol'
PORTUGUESE = 'prtg'
PORTUGUESE_BRAZIL = 'prtg_brz'
PORTUGUESE_PORTUGAL = 'prtg_pr'
ROMANIAN = 'rom'
RUSSIAN = 'rusn'
SERBIAN = 'srbn'
SLOVAK = 'slvk'
SPANISH = 'spn'
SPANISH_LATIN_AMERICA = 'spn_lat'
SPANISH_SPAIN = 'spn_spn'
SWEDISH = 'swed'
TAMIL = 'taml'
THAI = 'thai'
TURKISH = 'turk'
UKRAINIAN = 'ukrn'
VENETIAN = 'venetn'
VIETNAMESE = 'viet'
property description: str[source]

A human readable description for the locale.

Intended as instructions to humans or AI for translating. For most locales this is simply the language name, but for special ones like pirate-speak it may include instructions.

classmethod from_long_value(value: str) Locale[source]

Given a long value, return a Locale.

property long_value: str[source]

A longer more human readable alternative to value.

Like the regular enum values, these values will never change and can be used for persistent storage/etc.

property resolved: LocaleResolved[source]

Return the associated resolved locale.

class bacommon.locale.LocaleResolved(*values)[source]

Bases: Enum

A resolved Locale for use in logic.

These values should never be stored or transmitted and should always come from resolving a Locale which can be stored/transmitted. This gives us the freedom to revise this list as needed to keep our actual list of implemented resolved-locales as trim as possible.

ARABIC = 'arabc'
BELARUSSIAN = 'blrs'
CHINESE_SIMPLIFIED = 'chn_sim'
CHINESE_TRADITIONAL = 'chn_tr'
CROATIAN = 'croat'
CZECH = 'czch'
DANISH = 'dnsh'
DUTCH = 'dtch'
ENGLISH = 'eng'
ESPERANTO = 'esprnto'
FILIPINO = 'filp'
FRENCH = 'frnch'
GERMAN = 'grmn'
GIBBERISH = 'gibber'
GREEK = 'greek'
HINDI = 'hndi'
HUNGARIAN = 'hngr'
INDONESIAN = 'indnsn'
ITALIAN = 'italn'
JAPANESE = 'jpn'
KAZAKH = 'kazk'
KOREAN = 'kor'
MALAY = 'mlay'
PERSIAN = 'pers'
PIRATE_SPEAK = 'pirate'
POLISH = 'pol'
PORTUGUESE_BRAZIL = 'prtg_brz'
PORTUGUESE_PORTUGAL = 'prtg_pr'
ROMANIAN = 'rom'
RUSSIAN = 'rusn'
SERBIAN = 'srbn'
SLOVAK = 'slvk'
SPANISH_LATIN_AMERICA = 'spn_lat'
SPANISH_SPAIN = 'spn_spn'
SWEDISH = 'swed'
TAMIL = 'taml'
THAI = 'thai'
TURKISH = 'turk'
UKRAINIAN = 'ukrn'
VENETIAN = 'venetn'
VIETNAMESE = 'viet'
property decimal_mark: str[source]

The character separating a number’s whole and fraction parts.

Locale data, not translated content: a decimal mark is a fact about a language, and asking a translation model for one invites a confidently wrong answer that no reviewer would catch by reading the string. So it is curated here and applied by formatting code rather than authored into any asset package.

Deliberately covers only the mark. Digit grouping is not modelled: our formatted numbers scale their units (1.2 GB, never 1234.5 MB), so a group separator has nothing to separate, and skipping it avoids the messier per-locale rules (1,234,567 / 1 234 567 / 12,34,567). Digit shaping is likewise out: locales that could use non-ASCII digits read Western ones fine.

Arabic and Persian are '.' rather than '٫' on purpose – that mark pairs with Eastern Arabic digits, which we do not emit.

property duration_separator: str[source]

The joiner between composed duration parts (“1h 23m”).

Locale data, not translated content, same as decimal_mark: most languages join abbreviated duration parts with a space, while CJK compounds join with none (“1時間45分”). Curated here and applied by formatting code so it can never be a confidently wrong translation, and so a new locale is a type error rather than a silent space.

property endonym: str[source]

This locale’s name written in this locale’s own language.

Static per-locale data, deliberately not a translated string: a language picker shows every option at once, which no single-locale string table can do (a client holds one locale’s strings at a time). It is also what lets a user find their language whatever the ui is currently set to. Pair it with a translated language name – see the strings/locales group in BaCommonAssets – to render e.g. “Svenska (Swedish)”.

Values match what shipped in the legacy lang_names_translated blob, so players see no churn, with these deliberate deviations:

  • English was simply absent there and is supplied here.

  • Slovak’s stray trailing space is dropped.

  • Latin-American Spanish gains the accent it was missing (“Latinoamérica”).

  • Gibberish’s was ‘Abuktarika’, which sorted it to the very top of every picker – a surprising spot for a joke locale. It now starts with a G so it lands where someone hunting for it would look, keeping the mock-English style of the locale’s own output (‘Germaunish’, ‘Sveendishsprok’).

  • Persian’s wrapping bidi controls (U+2066/U+200E) are dropped. Directional isolation is a rendering concern, and both consumers already give each entry its own container (an html <option>, a popup-menu row), so the run cannot bleed into a neighbor. A caller placing one of these inline in mixed text should isolate it there.

property endonym_sort_key: str[source]

Sort key for listing locales by endonym.

Endonym order is what a language picker wants: it is the half of the label the eye scans for, and unlike ordering by translated name it does not reshuffle the whole list when the ui language changes.

Accents are folded so a leading diacritic doesn’t exile an entry from its own alphabet – raw codepoint order files “Čeština” after “Wikang Pilipino”. Non-Latin scripts still land after Latin ones, which reads as a reasonable grouping rather than an error. Deliberately not full locale-aware collation: that needs ICU, and the result here is stable and good enough for a few dozen entries.

Lives here rather than in each picker because there are two of them – the client’s settings window and the master server’s account-settings page – and they silently disagreed about ordering until 2026-07-30.

static from_tag(tag: str) LocaleResolved[source]

Return a locale for a given string tag.

Tags can be provided in BCP 47 form (‘en-US’) or POSIX locale string form (‘en_US.UTF-8’). A tag naming a language we have no locale for falls back to English (with a warning, so we can add it); a malformed tag raises ValueError.

Use from_tag_or_none() when a non-match is an ordinary outcome rather than something to complain about – negotiating a browser’s Accept-Language list, say, where most entries are expected to miss.

static from_tag_or_none(tag: str) LocaleResolved | None[source]

Return a locale for a string tag, or None if we have none.

The quiet counterpart to from_tag(): returns None instead of warning and falling back to English, and swallows the malformed-tag ValueError. This is what locale negotiation wants – walking a client’s preference list, a miss is the normal case and must be distinguishable from a genuine match on English.

Note that an unknown region within a known language is still a match (zh-XX resolves to simplified Chinese), and still warns – that one is worth adding.

property locale: Locale[source]

Return a locale that resolves to this resolved locale.

In some cases, such as when presenting locale options to the user, it makes sense to iterate over resolved locale values, as regular locales may include obsolete or redundant values. When storing locale values to disk or transmitting them, however, it is important to use plain locales. This method can be used to get back to a plain locale from a resolved one.

property tag: str[source]

An IETF BCP 47 tag for this locale.

This is often simply a language code (‘en’) but may in some cases include the country (‘pt-BR’) or script (‘zh-Hans’). Locales which are not “real” will include an ‘x’ in the middle (‘en-x-pirate’).

bacommon.locale.language_picker_label(locale: LocaleResolved, translated_name: str) str[source]

Build one entry for a language picker.

Reads "<endonym> · <name in the reader's language>", e.g. "Svenska · Swedish", collapsing to just the endonym when the two would be identical (the entry for the language you already use). The endonym lets a speaker find their own language whatever the ui is set to; the translated name lets everyone else identify a script they cannot read. Shape follows macOS’s language list, which uses a dash; see LANGUAGE_PICKER_SEPARATOR for why we cannot.

Known remaining wart, engine-side: the space before the separator still gets absorbed into an RTL endonym’s span, so those rows render slightly tight on that side (العربية· Arabic). And when both halves are non-ASCII – an RTL endonym in a CJK ui – everything lands in one span whatever the separator, and one of the two orders always reverses. That one needs real mixed-direction layout in the engine (docs/followups.md). The master server’s html picker is unaffected throughout: the browser lays out the whole string itself.

Lives beside the data rather than in either picker because there are two of them – the client’s settings window and the master server’s account-settings page – and they have already drifted once on ordering.

bacommon.loctext module

Localized-text evaluation (structured plural/select + substitutions).

The value delivered for a locale is either a plain str or a StringSelector. evaluate() resolves either against a locale and caller-supplied arguments to a final str:

  • A plain str has its {name} placeholders replaced by str(args['name']).

  • A StringSelector picks one leaf string at render time – by CLDR plural category of an integer arg (kind=PLURAL) or by the string value of an arg (kind=SELECT) – then substitutes {name} and (for plurals) # (the count) in the chosen leaf.

We use our own small structured form rather than parsing a message-format string: the runtime decisions are few and closed, the data is self-describing (it can only express what we support), and a struct is far safer to port than a hand-rolled string parser. This is the prototype of the native Lstr2 runtime – keeping it in bacommon gives pure-Python consumers (the client’s embedded Python, server-side resolution in bamaster/basn) the same evaluator, and the StringSelector IOAttrs keys are the on-the-wire schema the C++ port implements.

Scope: cardinal plural selection for non-negative integers, string select, {name} substitution, and # for the plural count. =N keys match an exact count before the category rule. Not (yet): ordinals/selectordinal, decimal operands, plural offsets, nested selectors.

exception bacommon.loctext.LocTextError[source]

Bases: Exception

A malformed localized value or a missing/bad argument.

class bacommon.loctext.PluralCategory(*values)[source]

Bases: Enum

A CLDR plural category.

FEW = 'few'
MANY = 'many'
ONE = 'one'
OTHER = 'other'
TWO = 'two'
ZERO = 'zero'
class bacommon.loctext.SelectorKind(*values)[source]

Bases: Enum

How a StringSelector chooses among its forms.

PLURAL = 'p'

Choose by the CLDR plural category of an integer argument.

SELECT = 's'

Choose by the string value of an argument (e.g. gender).

class bacommon.loctext.StringSelector(kind: SelectorKind, arg: str, forms: dict[str, str])[source]

Bases: object

A localized string whose final form is chosen at render time.

The structured alternative to a plain str value: forms maps a form key to its leaf text (which may carry {name} substitutions and, for plurals, # for the count). For PLURAL the keys are CLDR category names (one/few/…/other) or =N exact-count matches; for SELECT they are the possible string values of arg. other is the fallback. The IOAttrs keys are the on-the-wire schema shared with the native Lstr2 port.

arg: str
forms: dict[str, str]
kind: SelectorKind
bacommon.loctext.evaluate(value: str | StringSelector, locale: Locale, /, **args: object) str[source]

Resolve a localized value to a final string.

value and locale are positional-only so they cannot collide with a substitution named value or locale – without that, an authored string using {value} is unrenderable through this function, which is a trap that surfaces only once someone writes such a string.

value is a plain str ({name} substitutions only) or a StringSelector (plural/select choice, then substitution). args supplies the named arguments; plural selection uses locale’s CLDR rule (see plural_category()). Raises LocTextError on a missing/ill-typed argument or a selector with no matching form and no other.

bacommon.loctext.plural_category(locale: Locale, n: int) PluralCategory[source]

Return the CLDR cardinal plural category for an integer in a locale.

n is treated as a non-negative integer count (its absolute value is used). Uses the locale’s resolved form so obsolete aliases (e.g. the bare SPANISH) follow their modern locale’s rule. Every shipped locale is mapped explicitly; a genuinely-unknown locale falls back to the one-for-1 rule. See the module note for the integer-only scope.

bacommon.loctext.required_plural_categories(locale: Locale) list[PluralCategory][source]

The plural categories a plural message must cover for a locale.

The set the locale’s rule can produce over the integers, plus the mandatory other fallback (ICU requires it), in canonical order. Used to tell a translation model exactly which plural forms to emit for the target locale – derived from the same rules plural_category() evaluates with, so producer and client agree.

The integer sample (0..200) spans every modulo cycle the rules use; under the integer-only scope (see the module note) this is the exact set of categories the locale can select.

bacommon.loctext.substitution_names(value: str | StringSelector) set[str][source]

Return the substitution-argument names a value consumes.

For a plain string these are its {name} tokens; for a StringSelector its pivot arg plus any {name} tokens in its forms. This is the single source consumers use to derive a string’s parameter set from its value (e.g. rebuilding positional-substitution order client-side) – the producer-side round-trip validation guarantees every parameter’s token survives into every stored output, so the derivation is total.

bacommon.loggercontrol module

System for managing loggers.

class bacommon.loggercontrol.LoggerControlConfig(levels: dict[str, int]=<factory>)[source]

Bases: object

A logging level configuration that applies to all loggers.

Any loggers not explicitly contained in the configuration will be set to NOTSET.

apply(*, warn_unexpected_loggers: bool = False, warn_missing_loggers: bool = False, ignore_log_prefixes: list[str] | None = None) None[source]

Apply the config to all Python loggers.

If ‘warn_unexpected_loggers’ is True, warnings will be issues for any loggers not explicitly covered by the config. This is useful to help ensure controls for all possible loggers are present in a UI/etc.

If ‘warn_missing_loggers’ is True, warnings will be issued for any loggers present in the config that are not found at apply time. This can be useful for pruning settings for no longer used loggers.

Warnings for any log names beginning with any strings in ‘ignore_log_prefixes’ will be suppressed. This can allow ignoring loggers associated with submodules for a given package and instead presenting only a top level logger (or none at all).

apply_diff(diffconfig: LoggerControlConfig) LoggerControlConfig[source]

Apply a diff config to ourself.

Note that values that resolve to NOTSET are left intact in the output config. This is so all loggers expected by either the base or diff config to exist can be created if desired/etc.

diff(baseconfig: LoggerControlConfig) LoggerControlConfig[source]

Return a config containing only changes compared to a base config.

Note that this omits all NOTSET values that resolve to NOTSET in the base config.

This diffed config can later be used with apply_diff() against the base config to recreate the state represented by self.

classmethod from_current_loggers() Self[source]

Build a config from the current set of loggers.

get_effective_level(logname: str) int[source]

Given a log name, predict its level if this config is applied.

levels: dict[str, int]
sanity_check_effective_levels() None[source]

Checks existing loggers to make sure they line up with us.

This can be called periodically to ensure that a control-config is properly driving log levels and that nothing else is changing them behind our back.

would_make_changes() bool[source]

Return whether calling apply would change anything.

bacommon.logging module

Logging functionality.

class bacommon.logging.ClientLoggerName(*values)[source]

Bases: Enum

Logger names used on the Ballistica client.

ACCOUNT = 'ba.account'
ACCOUNT_CLIENT_V2 = 'ba.accountclientv2'
APP = 'ba.app'
ASSETS = 'ba.assets'
ASSET_MANAGER = 'ba.assetmanager'
AUDIO = 'ba.audio'
BA = 'ba'
CACHE = 'ba.cache'
CLOUD_SUBSCRIPTION = 'ba.cloudsub'
CONNECTIVITY = 'ba.connectivity'
DISCORD = 'ba.discord'
DISPLAYTIME = 'ba.displaytime'
ENV = 'ba.env'
GARBAGE_COLLECTION = 'ba.gc'
GRAPHICS = 'ba.gfx'
INPUT = 'ba.input'
LIFECYCLE = 'ba.lifecycle'
LOGIN_ADAPTER = 'ba.loginadapter'
LOG_REPORTING = 'ba.logreport'
NETWORKING = 'ba.net'
PERFORMANCE = 'ba.perf'
UI = 'ba.ui'
USER = 'ba.user'
V2TRANSPORT = 'ba.v2transport'
WORKSPACE = 'ba.workspace'
property description: str

Return a short description for the logger.

bacommon.logging.get_base_logger_control_config_client() LoggerControlConfig[source]

Return the logger-control-config used by the Ballistica client.

This should remain consistent since local logger configurations are stored relative to this.

bacommon.login module

Functionality related to cloud based assets.

class bacommon.login.LoginType(*values)[source]

Bases: Enum

Types of logins available.

DISCORD = 'discord'

Discord (OAuth2 via Discord Social SDK)

EMAIL = 'email'

Email/password

GAME_CENTER = 'game_center'

Apple’s Game Center

GPGS = 'gpgs'

Google Play Game Services

property displayname: str

A human readable name for this value.

property displaynameshort: str

A short human readable name for this value.

bacommon.logreporting module

Shared bits for triggered client log reporting.

The server hands a client a LogReportSpec (via transient cloud-vals) describing when to trip a log report and how much surrounding context to ship. The client then ships the resulting entry range incrementally, tracking its progress with a LogReportWindow.

Everything here is pure logic shared by the client reporter, the server’s ingest path, and tests; nothing engine-specific belongs in this module.

class bacommon.logreporting.LogReportSpec(trigger_level: LogLevel | None = None, trigger_phrases: list[str] = <factory>, max_before_entries: int | None = None, max_after_entries: int | None = None)[source]

Bases: object

When a client should ship log history, and how much of it.

An entry trips the report if it matches either the level trigger or any phrase trigger (the two OR together, and each is optional). A spec with neither is inactive. There is no re-arming: a client reports at most one triggered window per app run.

property active: bool

Whether this spec can ever trip (has any trigger).

match_entry(level: LogLevel, message: str) tuple[bool, str | None][source]

Test an entry against our triggers.

Returns (matched, phrase) where phrase is the trigger phrase that matched or None if the level trigger (or nothing) did. Runs on the client’s log-handling thread for every entry once reporting is enabled, so it stays a couple of comparisons.

max_after_entries: int | None = None

Max entries after the trigger to ship, or None to keep shipping new entries for the remainder of the run.

max_before_entries: int | None = None

Max entries preceding the trigger to include, or None to include everything still in the client’s log cache. Composes with the cache’s own size limit; the smaller set wins.

trigger_level: LogLevel | None = None

Trip when an entry at or above this level is logged, or None to disable level triggering (phrases may still trip).

trigger_phrases: list[str]

Trip when an entry’s message contains any of these substrings (case-sensitive; ORed together and with the level trigger). Keep these few and plain - each is tested against every log message on targeted clients.

class bacommon.logreporting.LogReportWindow(start: int, end: int | None, cursor: int)[source]

Bases: object

The entry-index range a tripped report ships, and its progress.

Indices are the log cache’s absolute entry indices (see LogArchive). cursor is the next index not yet confirmed delivered; it only ever advances after a confirmed send, so a failed send leaves the range to be re-sent (the server dedupes overlap by index).

advance(archive_start_index: int, entry_count: int) None[source]

Advance the cursor past a confirmed-delivered archive slice.

Uses the archive’s own start index rather than assuming it matches the cursor - cache eviction can hand back a slice starting later than asked for.

property complete: bool

Whether everything this window covers has been delivered.

cursor: int

Next index not yet confirmed delivered.

end: int | None

Index to stop shipping at (exclusive), or None to keep shipping for the rest of the run.

evicted_count(archive_start_index: int) int[source]

How many window entries a gather revealed as lost.

A gather handing back a slice starting past the cursor means entries the window still owed were evicted from the cache before they could ship. Returns how many, counting only entries the window actually covered (a bounded window ends at end no matter how far past it the cache start moved). Call before advance() (which moves the cursor past the gap).

classmethod from_trigger(trigger_index: int, spec: LogReportSpec) LogReportWindow[source]

Build the window for a trigger at the given entry index.

The window covers up to max_before_entries entries before the trigger, the triggering entry itself, and up to max_after_entries after it. The before side is a request, not a promise: entries already evicted from the client’s log cache simply won’t be there to gather.

gather_args() tuple[int, int | None][source]

Return (start_index, max_entries) for the next gather.

Suitable for passing to get_cached().

start: int

First index to ship.

bacommon.logreporting.trim_archive_overlap(archive: LogArchive, next_expected_index: int) int[source]

Drop leading archive entries at indices before next_expected_index.

Server-side dedup helper: a client re-sends its unacknowledged range after a failed send, so a receiver that remembers the next index it expects from an app instance can trim the overlap here. Mutates archive in place and returns the number of entries dropped.

bacommon.meshzstddict module

Zstandard dictionaries for compressing display-mesh (.bob) data.

These dictionaries are trained on the corpus of display-mesh blobs and substantially improve zstd compression of individual meshes (small files that share a lot of structure). They live in bacommon so that both server components (which compress meshes on store) and client components (which decompress them on load) load identical dictionary bytes.

Dictionaries are immutable and versioned: never mutate an existing .dict file – add a new version instead, so blobs compressed with an older dictionary remain decompressable. Whatever stores a compressed blob must record which dictionary version produced it.

bacommon.meshzstddict.display_mesh_dict_v1() bytes[source]

Return the v1 zstd dictionary for display-mesh (.bob) data.

The dictionary is read from the bundled data file on first call and cached for the lifetime of the process.

bacommon.metascan module

Scanner for # ba_meta directives in Python source.

Recognized directive shapes:

  • # ba_meta require api <N> — module-level API version requirement. When expected_api_version is supplied, modules whose value doesn’t match are skipped (and listed in ScanResults.incorrect_api_modules). When it is None the line is parsed for validity but no filtering occurs.

  • # ba_meta export <TYPE> — export the class defined on the next non-blank source line under the export-type <TYPE>.

  • # ba_meta require asset-package <ID> — module declares that it needs the named asset-package at runtime.

Other shapes are reported as malformed.

This module has no dependencies beyond the standard library so it can run anywhere — in the game runtime (wrapped by babase._meta.MetadataSubsystem), in build/tooling contexts (e.g. tools/pcommand assetpins), or in tests.

Higher-level concerns — background-thread scheduling, UI feedback, expansion of legacy export-type shortcuts, deprecation warnings — live in the consumer.

class bacommon.metascan.DirectoryScan(paths: list[str], expected_api_version: int | None = None, deprecated_export_shortcuts: dict[str, str] | None = None, *, expects_extras: bool = False)[source]

Bases: object

Scans directories for # ba_meta directives.

Pure-Python; no runtime dependencies. Construct with a list of paths (which must already be on PYTHONPATH if discovered modules will be imported by the consumer), then call run(). Results land in results.

If expected_api_version is set, modules whose # ba_meta require api value doesn’t match are skipped and listed in ScanResults.incorrect_api_modules. Pass None to scan regardless of api version (the typical tooling-side mode).

If deprecated_export_shortcuts is provided, export-type strings that appear as keys are substituted with the corresponding canonical class path and a deprecation warning is emitted with file:line context. Pass None to perform no substitution.

If expects_extras is True, run() will block after finishing the base paths until set_extras() is called from another thread. This supports the runtime pattern of kicking off the scan immediately and providing extra paths (workspace dirs, etc.) once they are known. Synchronous tooling callers should leave it at the default False; run() will then finish after the base paths.

run() None[source]

Do the thing.

set_extras(paths: list[str]) None[source]

Set extra portion.

class bacommon.metascan.ScanResults(exports: dict[str, list[str]]=<factory>, asset_packages: dict[str, list[str]]=<factory>, incorrect_api_modules: list[str] = <factory>, announce_errors_occurred: bool = False)[source]

Bases: object

Final results from a meta-scan.

announce_errors_occurred: bool = False
asset_packages: dict[str, list[str]]
exports: dict[str, list[str]]
exports_by_name(name: str) list[str][source]

Return exports matching a given name.

incorrect_api_modules: list[str]

bacommon.net module

Network related data and functionality.

Warning

This is an internal api and subject to change at any time. Do not use it in mod code.

bacommon.securedata module

Functionality related to verifying server generated data.

Warning

This is an internal api and subject to change at any time. Do not use it in mod code.

class bacommon.securedata.Archive(data: bytes, signature: bytes, cert: Cert | None = None)[source]

Bases: object

Self-contained signed payload — verifiable on its own.

Produced by Writer.write() (delegate-signed) or make_master_archive() (master-signed; cert is None). Verified via Reader.read(), which returns the payload bytes on success.

Embed directly as a field on dataclassio messages and responses (Annotated[Archive, IOAttrs('x')]); dataclassio nests the bytes-typed fields into the outer JSON without the extra base64-of-base64 round trip a bytes-blob field would incur. For the rare case that genuinely needs bytes (filesystem storage, etc.), serialize explicitly with efro.dataclassio.dataclass_to_json().

Per-field IOAttrs short keys are stable; new optional fields get soft_default rather than reusing or renaming existing ones — same as anywhere else dataclassio types travel.

cert: Cert | None = None
data: bytes
signature: bytes
class bacommon.securedata.Cert(payload: bytes, signature: bytes)[source]

Bases: object

Master-signed delegation certificate.

Asserts that the public key embedded in payload is authorized to sign on the master’s behalf for the validity window in the payload. Verifiable by anyone holding a current Reader (server-side) or the static public keys baked into the client binary (client-side, once a future client version supports the cert path).

Carries an opaque payload (canonical JSON of CertPayload) plus the master signature over those bytes. Mirrors the InsecureDirective pattern — re-encoding the dataclass on every verify would risk canonicalization drift.

decoded_payload() CertPayload[source]

Decode payload to a CertPayload.

Does not verify the signature. Callers are expected to be operating on a cert that’s already been chain-verified via Reader.check().

payload: bytes

Canonical JSON encoding of CertPayload. Signed as-is and re-decoded by recipients via decoded_payload().

signature: bytes

Signature of payload made with the master signing key.

class bacommon.securedata.CertPayload(publickey: bytes, starttime: datetime, endtime: datetime, issuer: str)[source]

Bases: object

Unsigned content of a Cert.

Master signs the canonical JSON encoding of this; recipients re-decode after signature verification to access the fields.

endtime: datetime
issuer: str

Identifier of the delegate (e.g. basn node id) for accountability and debugging. Not used for verification.

publickey: bytes

Public half of the keypair this cert authorizes.

starttime: datetime

Validity window during which signatures made by the matching private key should be honored.

exception bacommon.securedata.Invalid[source]

Bases: Exception

Raised by Reader.read() and Writer.write() when an operation cannot complete safely.

For read: the archive bytes are malformed, the master signature does not validate, the cert is expired or signed by an unknown master, or the delegate signature does not match.

For write: the writer’s cert is past its validity window and would produce a signed archive a verifier would refuse.

str(exc) carries a short reason suitable for logging.

bacommon.securedata.MIN_CLIENT_BUILD_FOR_DELEGATE_VERIFY: int = 22843

Minimum ballistica-internal client build number known to support cert-bearing verification via Reader.check(). Older clients only have the legacy check(data, signature) path and would silently fail to verify (data, signature, cert) triples — server code must therefore skip the delegate-signed path for clients reporting a build below this threshold.

Bump in lockstep with the ballistica-internal release that ships the matching client code. Tracks the build at the moment the cert path was added; ratchets upward only when a protocol change requires it.

class bacommon.securedata.Reader(starttime: datetime, endtime: datetime, publickeys: list[bytes])[source]

Bases: object

Verifies data as being signed by our master server.

check(data: bytes, signature: bytes, cert: Cert | None = None) bool[source]

Verify data, returning True if successful.

When cert is None (the default), signature is verified directly against the master public keys — used for anything bamaster signed itself.

When cert is given, the chain is verified end-to-end:

  1. The cert’s signature must validate against a master public key (proving the master authorized this delegate).

  2. The cert’s validity window must include now.

  3. signature must then validate against the delegate public key embedded in the cert.

Used for data signed by a delegate (typically a basn node) acting on the master’s behalf. Older clients that predate the delegate-signing rollout will only ever be sent cert=None triples; see bacommon/securedata.py design notes for the rollout discipline.

Uses the ballistica native _babase.verify_ed25519 when available (inside the app binary). On contexts without it (server, pytest, tools scripts) falls back to the cryptography package, which those contexts already have.

endtime: datetime
publickeys: list[bytes]
read(archive: Archive) bytes[source]

Verify archive and return its data on success.

archive is the Archive produced by Writer.write() or make_master_archive(). Routes automatically: an archive without a cert is verified directly against this reader’s master public keys; an archive with a cert is chain-verified (cert against master keys, then data against the cert’s delegate pubkey).

Raises Invalid for any failure — expired/forged cert, bad signature.

starttime: datetime
bacommon.securedata.STATIC_DATA_PUBLIC_KEYS: tuple[bytes, ...] = (b'\x98\xc0\xb3{\xea\n\t\x0f\xfb\xcbN\x1c\x03A\xd7\xd6d\x95{.\xdc\xda\x9b\xf6\xe0\x7f\x0bM\x84^\x15b',)

Public halves of long-lived Ed25519 keypairs the master server uses to sign static data that clients must verify offline — without any network round trip to fetch a Reader. Compiled into the client binary, so a MITM cannot swap them.

Multiple entries let us rotate: add the new key (as a second entry) in a client release, wait for that release to propagate to the installed base, switch the server to sign with the new key, then later drop the old key in a subsequent client release. On verify, a signature is considered valid if ANY listed key verifies it.

The matching private halves live on the master server in svals under static_data_private_key (current signing key). basn nodes fetch the current private key from the master at startup over the secure inter-node channel.

class bacommon.securedata.Writer(starttime: datetime, endtime: datetime, privatekey: bytes, cert: Cert)[source]

Bases: object

Delegated signing capability.

Issued by the master to a delegate (typically a basn node) over a secure channel. The delegate calls write() on data and distributes the resulting archive bytes; recipients chain-verify and recover the original bytes via Reader.read().

Sensitive: contains a private key. Never traverses untrusted channels and should not be persisted to disk.

The top-level starttime / endtime mirror the cert’s validity window for ergonomic refresh-decision code (see basn’s _update_secure_data_signer). They are NOT authoritative for verification — cert is.

Servers must consult MIN_CLIENT_BUILD_FOR_DELEGATE_VERIFY before sending delegate-signed archives to a client; clients below the threshold do not understand the cert path and would reject the archive.

cert: Cert

Master-signed cert authorizing the public counterpart of privatekey.

endtime: datetime
privatekey: bytes

Raw Ed25519 private key bytes.

sign(data: bytes) bytes[source]

Low-level: produce a raw signature over data.

Most callers should use write() instead — it returns a self-contained archive that includes the signature, the cert, and the data, all verifiable in a single Reader.read() call.

starttime: datetime
write(data: bytes) Archive[source]

Sign data and return an Archive.

The archive carries the signature + the writer’s cert; recipients pass it to Reader.read() to verify and recover the original bytes.

Raises Invalid if the writer’s cert has expired — an archive produced past expiry would be rejected by every verifier, so we fail fast at write time rather than at verify time.

bacommon.securedata.make_master_archive(data: bytes, master_priv_bytes: bytes) Archive[source]

Pack data + a master-key signature into an Archive.

Server-only — clients do not have access to a master private key. Verifiers (with master public keys) accept the resulting archive via Reader.read() exactly the same way they accept Writer-produced archives, except the master path carries no cert.

master_priv_bytes is the raw 32-byte Ed25519 seed. On bamaster, prefer UniversalGlobals.secure_data_archive_master which sources the key from svals.

bacommon.servermanager module

Functionality related to the server manager script.

class bacommon.servermanager.ChatMessageCommand(message: str, clients: list[int] | None)[source]

Bases: ServerCommand

Chat message from the server.

clients: list[int] | None
message: str
class bacommon.servermanager.ClientListCommand[source]

Bases: ServerCommand

Print a list of clients.

class bacommon.servermanager.KickCommand(client_id: int, ban_time: int | None)[source]

Bases: ServerCommand

Kick a client.

ban_time: int | None
client_id: int
class bacommon.servermanager.ScreenMessageCommand(message: str, color: tuple[float, float, float] | None, clients: list[int] | None)[source]

Bases: ServerCommand

Screen-message from the server.

clients: list[int] | None
color: tuple[float, float, float] | None
message: str
class bacommon.servermanager.ServerCommand[source]

Bases: object

Base class for commands that can be sent to the server.

class bacommon.servermanager.ServerConfig(party_name: str = 'FFA', party_is_public: bool = True, password: str | None = None, authenticate_clients: bool = True, admins: list[str] = <factory>, enable_default_kick_voting: bool = True, allow_punch_grab: bool = False, public_ipv4_address: str | None = None, public_ipv6_address: str | None = None, port: int = 43210, max_party_size: int = 6, session_max_players_override: int | None = None, session_type: str = 'ffa', playlist_code: int | None = None, playlist_inline: list[dict[str, ~typing.Any]] | None = None, playlist_shuffle: bool = True, auto_balance_teams: bool = True, coop_campaign: str = 'Easy', coop_level: str = 'Onslaught Training', enable_telnet: bool = False, teams_series_length: int = 7, ffa_series_length: int = 24, stats_url: str | None = None, clean_exit_minutes: float | None = None, unclean_exit_minutes: float | None = None, idle_exit_minutes: float | None = None, show_tutorial: bool = False, team_names: tuple[str, str] | None = None, team_colors: tuple[tuple[float, float, float], tuple[float, float, float]] | None = None, enable_queue: bool = True, protocol_version: int | None = None, stress_test_players: int | None = None, player_rejoin_cooldown: float = 10.0, log_levels: dict[str, str] | None = None, dont_write_bytecode: bool = False)[source]

Bases: object

Configuration for the server manager app (<appname>_server).

admins: list[str]
allow_punch_grab: bool = False
authenticate_clients: bool = True
auto_balance_teams: bool = True
clean_exit_minutes: float | None = None
coop_campaign: str = 'Easy'
coop_level: str = 'Onslaught Training'
dont_write_bytecode: bool = False
enable_default_kick_voting: bool = True
enable_queue: bool = True
enable_telnet: bool = False
ffa_series_length: int = 24
idle_exit_minutes: float | None = None
log_levels: dict[str, str] | None = None
max_party_size: int = 6
party_is_public: bool = True
party_name: str = 'FFA'
password: str | None = None
player_rejoin_cooldown: float = 10.0
playlist_code: int | None = None
playlist_inline: list[dict[str, Any]] | None = None
playlist_shuffle: bool = True
port: int = 43210
protocol_version: int | None = None
public_ipv4_address: str | None = None
public_ipv6_address: str | None = None
session_max_players_override: int | None = None
session_type: str = 'ffa'
show_tutorial: bool = False
stats_url: str | None = None
stress_test_players: int | None = None
team_colors: tuple[tuple[float, float, float], tuple[float, float, float]] | None = None
team_names: tuple[str, str] | None = None
teams_series_length: int = 7
unclean_exit_minutes: float | None = None
class bacommon.servermanager.ShutdownCommand(reason: ShutdownReason, immediate: bool)[source]

Bases: ServerCommand

Tells the server to shut down.

immediate: bool
reason: ShutdownReason
class bacommon.servermanager.ShutdownReason(*values)[source]

Bases: Enum

Reason a server is shutting down.

NONE = 'none'
RESTARTING = 'restarting'
class bacommon.servermanager.StartServerModeCommand(config: ServerConfig)[source]

Bases: ServerCommand

Tells the app to switch into ‘server’ mode.

config: ServerConfig

bacommon.strbrief module

Parse the tag vocabulary in an assets_v1 .bstr authoring brief.

A .bstr input is a freeform AI-translation brief (decision D10) carrying a small, strict inline tag vocabulary (D11) – the only machine-readable part. Three consumers depend on these tags deterministically: wrapper codegen (the typed accessor signature), generation (whether/how to produce plural forms), and validation (tag round-trip, D12). This module extracts and validates them.

The grammar is one rule:

tag     := target [ '|' spec ]
target  := ident | '@' refpath
spec    := ident [ '(' kwargs ')' ]
kwarg   := ident '=' ( int | float | true | false | ident )

Each character has exactly one job. @ marks where content comes from – a term reference is resolved and inlined at generation time (D12) rather than passed at call time, and is the only tag that is not a wrapper param. : separates namespaces inside a reference (@apref:path). | applies a rendering spec, the same filter operator template languages use, so new forms cost a registry entry rather than a new sigil:

  • {name} – runtime text substitution; a str wrapper param.

  • {count|plural} – runtime number substitution; an int wrapper param and the plural pivot (the string gains one form per CLDR category).

  • {@myterms:strings/apple} – a term reference. Always apref:path; colon-less same-package refs are not supported.

Braces escape as {{ and }}, exactly as they do at runtime (bacommon.loctext). The two layers must agree on what a brace means, or a brief and its own translated output disagree about which spans are tags.

Nested braces are rejected on purpose, so a raw ICU message pasted into a brief fails loudly rather than parsing as garbage – the brief layer uses this grammar, not ICU.

This is the brief (authoring) layer. What gets stored and evaluated at runtime is an ICU-MessageFormat-subset message (bacommon.loctext); the producer expands these tags into that form ({count|plural} -> {count, plural, one {...} other {...}}).

Lives in shared bacommon (rather than the bamaster producer, its original home) so authoring-side tooling can validate briefs before upload – assetworkspace put parses every local .bstr brief and refuses the sync on parse errors, surfacing mistakes (duplicate tags, pasted ICU, bad names, unknown specs) at edit time instead of minutes later inside a server translation run. The producer imports this same module, so the grammar cannot drift between the two.

class bacommon.strbrief.BriefSignature(tags: list[BriefTag])[source]

Bases: object

The full tag signature extracted from a brief (in order).

property params: list[BriefTag]

Wrapper params (everything but term refs), in order.

First-appearance order. Term refs are excluded – they’re resolved at generation time, not passed at call time.

property pivot: BriefTag | None

The single form-producing tag, or None.

The coexistence invariant guarantees at most one (enforced by parse_brief()).

tags: list[BriefTag]
property token_params: list[BriefTag]

Params that must survive into output as a {name} token.

Everything the runtime substitutes by name. The pivot is excluded: its number renders through the ICU # count placeholder inside each form, not through a named token.

class bacommon.strbrief.BriefSpec(name: str, args: dict[str, int | float | bool | str]=<factory>)[source]

Bases: object

A parsed rendering spec – the foo(a=1) in {x|foo(a=1)}.

args: dict[str, int | float | bool | str]
property canonical_src: str

This spec as canonical name / name(args) source.

name: str
class bacommon.strbrief.BriefTag(kind: BriefTagKind, name: str, spec: BriefSpec | None = None)[source]

Bases: object

One parsed inline tag from a brief.

property display_kind: str

The kind plus any spec args, in canonical encoded form.

What the language blob’s k carrier stores for a spec’d param: the bare param_kind when the spec carries no args (keeping pre-args blobs byte-identical), else kind(name=value,...) – the same name(args) shape spec bodies use, with args sorted by name and rendered canonically so identical briefs can never produce differing blob bytes. Decoded at render time by parse_display_kind().

property form_producing: bool

Whether this tag makes the string gain plural forms.

kind: BriefTagKind
name: str

For TEXT/VALUE, the wrapper arg name; for TERM, the reference path.

property param_kind: str

The call-signature category for wrapper codegen.

spec: BriefSpec | None = None

The rendering spec, or None for a plain text sub / term ref.

class bacommon.strbrief.BriefTagKind(*values)[source]

Bases: Enum

The three things a tag can be.

TERM = 'term'

A {@path} term reference, inlined at generation time.

TEXT = 'text'

A plain {name} – text substituted at render time.

VALUE = 'value'

A {name|spec} – a typed value rendered by its spec.

bacommon.strbrief.SPECS: dict[str, SpecDef] = {'data_size': SpecDef(param_kind='bytes', form_producing=False, args={'compact': <class 'bool'>}, arg_choices={}, validate_args=None, supported=True), 'duration': SpecDef(param_kind='millis', form_producing=False, args={'maxparts': <class 'int'>, 'decimals': <class 'int'>, 'dir': <class 'str'>, 'clamp': <class 'bool'>}, arg_choices={'dir': ('past', 'future')}, validate_args=<function _validate_duration_args>, supported=True), 'ordinal': SpecDef(param_kind='count', form_producing=True, args={}, arg_choices={}, validate_args=None, supported=False), 'plural': SpecDef(param_kind='count', form_producing=True, args={}, arg_choices={}, validate_args=None, supported=True)}

The spec registry – the whole extension point. A new rendering form is an entry here plus its handling in the producer/evaluator; the grammar above never changes. ordinal is reserved: CLDR ordinal categories are a different form set from cardinals, so it is a pivot like plural, not a formatter.

class bacommon.strbrief.SpecDef(param_kind: str, form_producing: bool = False, args: dict[str, type] = <factory>, arg_choices: dict[str, tuple[str, ...]] = <factory>, validate_args: ~typing.Callable[[dict[str, int | float | bool | str]], str | None] | None = None, supported: bool = True)[source]

Bases: object

What one registered rendering spec means.

param_kind is the coarse call-signature category consumed by wrapper codegen. form_producing marks a spec that multiplies the string into per-plural-category forms rather than merely rendering a value into it – the property the one-pivot rule is checked against. args maps each accepted keyword to its type (an int literal is accepted where float is declared); arg_choices optionally restricts a string-typed arg to an enumerated value set, and validate_args optionally checks the whole parsed arg map (returning an error message or None) – both enforced at parse so a bad brief fails at author time, not at display. supported is False for a name that is reserved but not implemented; it parses, then fails with a tailored message.

arg_choices: dict[str, tuple[str, ...]]
args: dict[str, type]
form_producing: bool = False
param_kind: str
supported: bool = True
validate_args: Callable[[dict[str, int | float | bool | str]], str | None] | None = None
bacommon.strbrief.parse_brief(text: str) BriefSignature[source]

Extract + validate the tag signature from a brief input.

Raises CleanError on raw ICU pasted into the brief, an empty/malformed tag, an invalid arg name, an unknown or unsupported spec, a bad spec argument, a duplicate arg name, or more than one form-producing spec (the coexistence invariant – a string with two independent counts should be split into two entries).

bacommon.strbrief.parse_display_kind(expr: str) tuple[str, dict[str, int | float | bool | str]][source]

Split a k-carrier kind expression into (kind, args).

The inverse of BriefTag.display_kind: a bare kind ('bytes') yields empty args; 'bytes(compact=true)' yields the decoded arg map. Arg values are typed by literal shape (true/false -> bool, digits -> int/float, else identifier string) – the declared spec-arg types that governed authoring are not consulted here, since the display side dispatches on the kind and reads only the args it knows. Raises CleanError on a malformed expression; display-time callers surface that through their usual fail-visible path.

bacommon.strbrief.validate_round_trip(sig: BriefSignature, segments: Iterable[str], *, require_count: bool) None[source]

Assert a brief’s runtime tags survived correctly into generated text.

The post-generation half of the D12 contract:

  • every {name} token param must appear verbatim in each segment (the model must not drop or rename a runtime substitution);

  • no {name} token other than those may appear (the model must not invent a substitution that would fail at render);

  • with require_count, each segment must contain the # count placeholder (every plural form renders the number);

  • no term reference {@...} may survive (those are resolved/inlined at generation time, never shipped as a token).

Escaped braces ({{/}}) are literal text and are skipped throughout, matching the runtime evaluator.

segments is the per-locale output text(s) to check: a single output string for a plain/text-sub entry, or the per-plural-form texts for a pivot entry (so each form is verified independently). Raises CleanError on any violation.

bacommon.text module

Text related bits.

class bacommon.text.SpecialChar(*values)[source]

Bases: Enum

Custom unicode characters the engine can display.

Keep this in sync with babase._generated.enums.SpecialChar.

BACK = '\ue00b'
BOTTOM_BUTTON = '\ue008'
BOXING_GLOVE = '\ue067'
CLOSE = '\ue017'
CROWN = '\ue043'
DELETE = '\ue009'
DICE_BUTTON1 = '\ue022'
DICE_BUTTON2 = '\ue023'
DICE_BUTTON3 = '\ue024'
DICE_BUTTON4 = '\ue025'
DOWN_ARROW = '\ue004'
DPAD_CENTER_BUTTON = '\ue010'
DRAGON = '\ue048'
EYE_BALL = '\ue045'
FAST_FORWARD_BUTTON = '\ue00f'
FEDORA = '\ue041'
FIREBALL = '\ue04f'
FLAG_ALGERIA = '\ue054'
FLAG_ARGENTINA = '\ue05f'
FLAG_AUSTRALIA = '\ue058'
FLAG_BRAZIL = '\ue035'
FLAG_CANADA = '\ue039'
FLAG_CHILE = '\ue061'
FLAG_CHINA = '\ue037'
FLAG_CZECH_REPUBLIC = '\ue057'
FLAG_EGYPT = '\ue052'
FLAG_FRANCE = '\ue03c'
FLAG_GERMANY = '\ue034'
FLAG_INDIA = '\ue03a'
FLAG_INDONESIA = '\ue03d'
FLAG_IRAN = '\ue05d'
FLAG_ITALY = '\ue03e'
FLAG_JAPAN = '\ue03b'
FLAG_KUWAIT = '\ue053'
FLAG_MALAYSIA = '\ue056'
FLAG_MEXICO = '\ue033'
FLAG_NETHERLANDS = '\ue040'
FLAG_PHILIPPINES = '\ue060'
FLAG_POLAND = '\ue05e'
FLAG_QATAR = '\ue051'
FLAG_RUSSIA = '\ue036'
FLAG_SAUDI_ARABIA = '\ue055'
FLAG_SINGAPORE = '\ue059'
FLAG_SOUTH_KOREA = '\ue03f'
FLAG_UNITED_ARAB_EMIRATES = '\ue050'
FLAG_UNITED_KINGDOM = '\ue038'
FLAG_UNITED_STATES = '\ue032'
HAL = '\ue042'
HEART = '\ue047'
HELMET = '\ue049'
LEFT_ARROW = '\ue001'
LEFT_BUTTON = '\ue005'
LOCAL_ACCOUNT = '\ue030'
LOGO_FLAT = '\ue00c'
MIKIROG = '\ue062'
MOON = '\ue04d'
MUSHROOM = '\ue04a'
NINJA_STAR = '\ue04b'
OUYA_BUTTON_A = '\ue01c'
OUYA_BUTTON_O = '\ue019'
OUYA_BUTTON_U = '\ue01a'
OUYA_BUTTON_Y = '\ue01b'
PALM_TREE = '\ue066'
PARTY_ICON = '\ue027'
PAUSE_BUTTON = '\ue016'
PLAY_BUTTON = '\ue015'
PLAY_PAUSE_BUTTON = '\ue00e'
PLAY_STATION_CIRCLE_BUTTON = '\ue012'
PLAY_STATION_CROSS_BUTTON = '\ue011'
PLAY_STATION_SQUARE_BUTTON = '\ue014'
PLAY_STATION_TRIANGLE_BUTTON = '\ue013'
POTATO = '\ue065'
REWIND_BUTTON = '\ue00d'
RIGHT_ARROW = '\ue002'
RIGHT_BUTTON = '\ue007'
SANTA_HAT = '\ue064'
SHIFT = '\ue00a'
SKULL = '\ue046'
SPIDER = '\ue04e'
TEST_ACCOUNT = '\ue028'
TICKET = '\ue01f'
TICKET_BACKING = '\ue029'
TOKEN = '\ue01d'
TOP_BUTTON = '\ue006'
TROPHY0A = '\ue02d'
TROPHY0B = '\ue02e'
TROPHY1 = '\ue02a'
TROPHY2 = '\ue02b'
TROPHY3 = '\ue02c'
TROPHY4 = '\ue02f'
UP_ARROW = '\ue003'
VIKING_HELMET = '\ue04c'
YIN_YANG = '\ue044'

bacommon.transfer module

Functionality related to transferring files/data.

Warning

This is an internal api and subject to change at any time. Do not use it in mod code.