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:
EnumType ID for each of our subclasses.
- CLASSIC = 'c'¶
- class bacommon.analytics.ClassicAnalyticsEvent(eventtype: EventType, extra: str | None = None)[source]¶
Bases:
AnalyticsEventAnalytics event related to classic.
- class EventType(*values)[source]¶
Bases:
EnumTypes 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'¶
- 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:
EnumProcessor 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:
EnumA 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:
EnumOverall 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:
EnumA unique Ballistica build variation within a single platform.
Each distinct permutation of an app has a unique combination of
AppPlatformandAppVariant. 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:
EnumProcess 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:
ExceptionA 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_sizecheck 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
datathen atomically write it intodest_root.Verify, write to a temp file in the destination directory,
fsync, thenos.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. RaisesCasDownloadErroron 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 theX-Asset-Tokenheader and anX-Accept-Compressionheader advertising every encoding this build can decode, so the node fulfills the request in whichever supported encoding is smallest/available.filehashandsizeare the blob’s canonical (uncompressed) identity. The encoding of the received bytes is taken solely from the node’sX-Cas-Compressionresponse 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 beforecas_write()sha256-verifies them againstfilehashand 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. RaisesCasDownloadErroron 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-Tokenheader.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,/casblobscope verification, and tests all go through it. RaisesValueErroron 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
CompressionTypevalues 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 includesu(it can always handle uncompressed). Absent ⇒ a legacy client that decodes per its manifest (back-compat).- Type:
Request header on a CAS
/casblobGET
- bacommon.cloudfilecodec.CAS_COMPRESSION_HEADER = 'X-Cas-Compression'¶
the single
CompressionTypevalue 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
/casblobGET
- class bacommon.cloudfilecodec.CompressionType(*values)[source]¶
Bases:
EnumHow 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
CompressionTypemembers 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
canonicalbytes perctype.The canonical (uncompressed) content identified by a cloud-file’s
cloud_file_id. Inverse ofdecompress_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
storedbytes perctypeback 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
datawith plain zstd at the given level.
- bacommon.cloudfilecodec.zstd_compress_with_dict(data: bytes, dict_bytes: bytes, level: int) bytes[source]¶
Compress
datawith zstd using a pre-shared dictionary.
- bacommon.cloudfilecodec.zstd_decompress(data: bytes) bytes[source]¶
Decompress plain-zstd
dataproduced byzstd_compress().
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:
EnumType IDs for browser-to-adapter payloads.
- EXEC = 'e'¶
- class bacommon.consolechannel.ConsoleContact(*values)[source]¶
Bases:
EnumHow 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:
EnumType 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:
EnumWhere 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:
ConsoleEventWe 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.
- class bacommon.consolechannel.ExecCommand(code: str)[source]¶
Bases:
ConsoleCommandRun some code on the target.
- classmethod get_type_id() ConsoleCommandTypeID[source]¶
Return the type-id for this subclass.
- class bacommon.consolechannel.GapEvent(dropped: int)[source]¶
Bases:
ConsoleEventLog 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.
- 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:
ConsoleEventWhich 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.
- classmethod get_type_id() ConsoleEventTypeID[source]¶
Return the type-id for this subclass.
- class bacommon.consolechannel.LogEntriesEvent(entries: list[LogEntry], next_index: int = 0)[source]¶
Bases:
ConsoleEventA slice of the target’s log.
- classmethod get_type_id() ConsoleEventTypeID[source]¶
Return the type-id for this subclass.
- class bacommon.consolechannel.PermissionEvent(state: ConsolePermission, message: str | None = None)[source]¶
Bases:
ConsoleEventWhether 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:
ConsoleEventA change in our ability to reach the target.
- contact: ConsoleContact¶
- classmethod get_type_id() ConsoleEventTypeID[source]¶
Return the type-id for this subclass.
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:
It is above every ASCII range
IsOSDrawableAsciitests (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.It is under
kGlyphCount(1280) andg_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 charactersIsOSDrawableAsciiexplicitly 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:
EnumA 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.
- 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:
EnumA resolved
Localefor use in logic.These values should never be stored or transmitted and should always come from resolving a
Localewhich 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, never1234.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/localesgroup in BaCommonAssets – to render e.g. “Svenska (Swedish)”.Values match what shipped in the legacy
lang_names_translatedblob, 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’sAccept-Languagelist, 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(): returnsNoneinstead of warning and falling back to English, and swallows the malformed-tagValueError. 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-XXresolves 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.
- 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; seeLANGUAGE_PICKER_SEPARATORfor 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
strhas its{name}placeholders replaced bystr(args['name']).A
StringSelectorpicks 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:
ExceptionA malformed localized value or a missing/bad argument.
- class bacommon.loctext.PluralCategory(*values)[source]¶
Bases:
EnumA CLDR plural category.
- FEW = 'few'¶
- MANY = 'many'¶
- ONE = 'one'¶
- OTHER = 'other'¶
- TWO = 'two'¶
- ZERO = 'zero'¶
- class bacommon.loctext.SelectorKind(*values)[source]¶
Bases:
EnumHow a
StringSelectorchooses 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:
objectA localized string whose final form is chosen at render time.
The structured alternative to a plain
strvalue:formsmaps a form key to its leaf text (which may carry{name}substitutions and, for plurals,#for the count). ForPLURALthe keys are CLDR category names (one/few/…/other) or=Nexact-count matches; forSELECTthey are the possible string values ofarg.otheris the fallback. TheIOAttrskeys are the on-the-wire schema shared with the nativeLstr2port.- kind: SelectorKind¶
- bacommon.loctext.evaluate(value: str | StringSelector, locale: Locale, /, **args: object) str[source]¶
Resolve a localized
valueto a final string.valueandlocaleare positional-only so they cannot collide with a substitution namedvalueorlocale– 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.valueis a plainstr({name}substitutions only) or aStringSelector(plural/select choice, then substitution).argssupplies the named arguments; plural selection useslocale’s CLDR rule (seeplural_category()). RaisesLocTextErroron a missing/ill-typed argument or a selector with no matching form and noother.
- bacommon.loctext.plural_category(locale: Locale, n: int) PluralCategory[source]¶
Return the CLDR cardinal plural category for an integer in a locale.
nis treated as a non-negative integer count (its absolute value is used). Uses the locale’s resolved form so obsolete aliases (e.g. the bareSPANISH) follow their modern locale’s rule. Every shipped locale is mapped explicitly; a genuinely-unknown locale falls back to theone-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
pluralmessage must cover for a locale.The set the locale’s rule can produce over the integers, plus the mandatory
otherfallback (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 rulesplural_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 aStringSelectorits pivotargplus 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:
objectA 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.
- get_effective_level(logname: str) int[source]¶
Given a log name, predict its level if this config is applied.
bacommon.logging module¶
Logging functionality.
- class bacommon.logging.ClientLoggerName(*values)[source]¶
Bases:
EnumLogger 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'¶
- 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.
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:
objectWhen 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.
- match_entry(level: LogLevel, message: str) tuple[bool, str | None][source]¶
Test an entry against our triggers.
Returns
(matched, phrase)wherephraseis 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.
- class bacommon.logreporting.LogReportWindow(start: int, end: int | None, cursor: int)[source]¶
Bases:
objectThe entry-index range a tripped report ships, and its progress.
Indices are the log cache’s absolute entry indices (see
LogArchive).cursoris 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.
- 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
endno matter how far past it the cache start moved). Call beforeadvance()(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_entriesentries before the trigger, the triggering entry itself, and up tomax_after_entriesafter 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.
- 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
archivein 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.metascan module¶
Scanner for # ba_meta directives in Python source.
Recognized directive shapes:
# ba_meta require api <N>— module-level API version requirement. Whenexpected_api_versionis supplied, modules whose value doesn’t match are skipped (and listed inScanResults.incorrect_api_modules). When it isNonethe 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:
objectScans directories for
# ba_metadirectives.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 inresults.If
expected_api_versionis set, modules whose# ba_meta require apivalue doesn’t match are skipped and listed inScanResults.incorrect_api_modules. PassNoneto scan regardless of api version (the typical tooling-side mode).If
deprecated_export_shortcutsis 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. PassNoneto perform no substitution.If
expects_extrasis True,run()will block after finishing the base paths untilset_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 defaultFalse;run()will then finish after the base paths.
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:
objectSelf-contained signed payload — verifiable on its own.
Produced by
Writer.write()(delegate-signed) ormake_master_archive()(master-signed;certisNone). Verified viaReader.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 withefro.dataclassio.dataclass_to_json().Per-field IOAttrs short keys are stable; new optional fields get
soft_defaultrather than reusing or renaming existing ones — same as anywhere else dataclassio types travel.
- class bacommon.securedata.Cert(payload: bytes, signature: bytes)[source]¶
Bases:
objectMaster-signed delegation certificate.
Asserts that the public key embedded in
payloadis authorized to sign on the master’s behalf for the validity window in the payload. Verifiable by anyone holding a currentReader(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 ofCertPayload) plus the master signature over those bytes. Mirrors theInsecureDirectivepattern — re-encoding the dataclass on every verify would risk canonicalization drift.- decoded_payload() CertPayload[source]¶
Decode
payloadto aCertPayload.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 viadecoded_payload().
- class bacommon.securedata.CertPayload(publickey: bytes, starttime: datetime, endtime: datetime, issuer: str)[source]¶
Bases:
objectUnsigned content of a
Cert.Master signs the canonical JSON encoding of this; recipients re-decode after signature verification to access the fields.
- exception bacommon.securedata.Invalid[source]¶
Bases:
ExceptionRaised by
Reader.read()andWriter.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 legacycheck(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:
objectVerifies 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
certisNone(the default),signatureis verified directly against the master public keys — used for anything bamaster signed itself.When
certis given, the chain is verified end-to-end:The cert’s signature must validate against a master public key (proving the master authorized this delegate).
The cert’s validity window must include now.
signaturemust 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=Nonetriples; seebacommon/securedata.pydesign notes for the rollout discipline.Uses the ballistica native
_babase.verify_ed25519when available (inside the app binary). On contexts without it (server, pytest, tools scripts) falls back to thecryptographypackage, which those contexts already have.
- read(archive: Archive) bytes[source]¶
Verify
archiveand return its data on success.archiveis theArchiveproduced byWriter.write()ormake_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
Invalidfor any failure — expired/forged cert, bad signature.
- 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:
objectDelegated 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 viaReader.read().Sensitive: contains a private key. Never traverses untrusted channels and should not be persisted to disk.
The top-level
starttime/endtimemirror the cert’s validity window for ergonomic refresh-decision code (see basn’s_update_secure_data_signer). They are NOT authoritative for verification —certis.Servers must consult
MIN_CLIENT_BUILD_FOR_DELEGATE_VERIFYbefore 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.
- 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 singleReader.read()call.
- write(data: bytes) Archive[source]¶
Sign
dataand return anArchive.The archive carries the signature + the writer’s cert; recipients pass it to
Reader.read()to verify and recover the original bytes.Raises
Invalidif 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_bytesis the raw 32-byte Ed25519 seed. On bamaster, preferUniversalGlobals.secure_data_archive_masterwhich 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:
ServerCommandChat message from the server.
- class bacommon.servermanager.ClientListCommand[source]¶
Bases:
ServerCommandPrint a list of clients.
- class bacommon.servermanager.KickCommand(client_id: int, ban_time: int | None)[source]¶
Bases:
ServerCommandKick a client.
- class bacommon.servermanager.ScreenMessageCommand(message: str, color: tuple[float, float, float] | None, clients: list[int] | None)[source]¶
Bases:
ServerCommandScreen-message from the server.
- class bacommon.servermanager.ServerCommand[source]¶
Bases:
objectBase 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:
objectConfiguration for the server manager app (<appname>_server).
- class bacommon.servermanager.ShutdownCommand(reason: ShutdownReason, immediate: bool)[source]¶
Bases:
ServerCommandTells the server to shut down.
- reason: ShutdownReason¶
- class bacommon.servermanager.ShutdownReason(*values)[source]¶
Bases:
EnumReason a server is shutting down.
- NONE = 'none'¶
- RESTARTING = 'restarting'¶
- class bacommon.servermanager.StartServerModeCommand(config: ServerConfig)[source]¶
Bases:
ServerCommandTells 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; astrwrapper param.{count|plural}– runtime number substitution; anintwrapper param and the plural pivot (the string gains one form per CLDR category).{@myterms:strings/apple}– a term reference. Alwaysapref: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:
objectThe 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()).
- class bacommon.strbrief.BriefSpec(name: str, args: dict[str, int | float | bool | str]=<factory>)[source]¶
Bases:
objectA parsed rendering spec – the
foo(a=1)in{x|foo(a=1)}.
- class bacommon.strbrief.BriefTag(kind: BriefTagKind, name: str, spec: BriefSpec | None = None)[source]¶
Bases:
objectOne 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
kcarrier stores for a spec’d param: the bareparam_kindwhen the spec carries no args (keeping pre-args blobs byte-identical), elsekind(name=value,...)– the samename(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 byparse_display_kind().
- kind: BriefTagKind¶
- class bacommon.strbrief.BriefTagKind(*values)[source]¶
Bases:
EnumThe 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.
ordinalis reserved: CLDR ordinal categories are a different form set from cardinals, so it is a pivot likeplural, 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:
objectWhat one registered rendering spec means.
param_kindis the coarse call-signature category consumed by wrapper codegen.form_producingmarks 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.argsmaps each accepted keyword to its type (anintliteral is accepted wherefloatis declared);arg_choicesoptionally restricts a string-typed arg to an enumerated value set, andvalidate_argsoptionally 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.supportedis False for a name that is reserved but not implemented; it parses, then fails with a tailored message.
- bacommon.strbrief.parse_brief(text: str) BriefSignature[source]¶
Extract + validate the tag signature from a brief
input.Raises
CleanErroron 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. RaisesCleanErroron 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.segmentsis 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). RaisesCleanErroron any violation.
bacommon.text module¶
Text related bits.
- class bacommon.text.SpecialChar(*values)[source]¶
Bases:
EnumCustom 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'¶
- DISCORD_LOGO = '\ue026'¶
- DOWN_ARROW = '\ue004'¶
- DPAD_CENTER_BUTTON = '\ue010'¶
- DRAGON = '\ue048'¶
- EXPLODINARY_LOGO = '\ue031'¶
- 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'¶
- GAME_CENTER_LOGO = '\ue021'¶
- GOOGLE_PLAY_GAMES_LOGO = '\ue020'¶
- HAL = '\ue042'¶
- HEART = '\ue047'¶
- HELMET = '\ue049'¶
- LEFT_ARROW = '\ue001'¶
- LEFT_BUTTON = '\ue005'¶
- LOCAL_ACCOUNT = '\ue030'¶
- LOGO = '\ue01e'¶
- LOGO_FLAT = '\ue00c'¶
- MIKIROG = '\ue062'¶
- MOON = '\ue04d'¶
- MUSHROOM = '\ue04a'¶
- NINJA_STAR = '\ue04b'¶
- NVIDIA_LOGO = '\ue05c'¶
- OCULUS_LOGO = '\ue05a'¶
- 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'¶
- STEAM_LOGO = '\ue05b'¶
- 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'¶
- V2_LOGO = '\ue063'¶
- 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.