bacommon package

Functionality and data shared by all Ballistica components.

This includes clients, various servers, tools, etc.

Subpackages

Submodules

bacommon.analytics module

Analytics support.

Warning

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

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.

Warning

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

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.assetpackage module

Client-facing types related to asset packages.

class bacommon.assetpackage.ApverNum

An asset-package-version’s numeric id: a permanent int minted by the master server when the version is created, never reused, and a full alternate to the version’s string id (a-0.babuiltinassets.260927). Numeric ids are what machinery stores and sends; string ids are for people (display, authoring). A distinct type (a plain int on the wire) so one can’t be mixed up with other ints, such as asset indices.

alias of int

class bacommon.assetpackage.AssetPackageDisplayInfo(name: str, owner: str)[source]

Bases: object

Names an asset package for people (as in an error message).

Sent alongside resolve failures that are about one particular package (AUTH_REQUIRED, ACCESS_DENIED) so the client can say which package it is in its own (translated) words, e.g. “asset package ‘foo’ by efro”.

name: str

The package’s name (foo in a-0.foo.260911).

owner: str

Its owner’s display name (account tag), or their account id if no tag could be found.

class bacommon.assetpackage.AssetPackageResolveError(*values)[source]

Bases: Enum

Why an asset-package resolve failed (structured for branching).

The server sends this back when a resolve fails, so the client can react precisely (e.g. prompt for sign-in on AUTH_REQUIRED) rather than parsing the human-readable error string. On the client it arrives as babase.AssetResolveError.code.

New values may be added over time; code branching on these should handle unrecognized ones (older clients see any value they don’t know as INTERNAL).

ACCESS_DENIED = 'access'

Caller is authenticated but lacks access to this (non-public) version (not the owner / not on the package’s dev team).

AUTH_REQUIRED = 'auth'

Caller is unauthenticated and the version is non-public; signing in with an account that has access may resolve it.

CLIENT_TOO_OLD = 'tooold'

The client build is too old to address current asset-package manifests (which use clean source-named logical paths); the user must update. Clients predating the build-number field also land here.

CONTENT = 'content'

The package’s own source content failed to build – a problem the package author can fix (e.g. a malformed sound or texture file). The human-readable error names the offending source file(s); clients should surface it verbatim. Old clients see this as INTERNAL.

INTERNAL = 'internal'

An internal/assemble error occurred server-side.

INVALID = 'invalid'

A requested dimension value was invalid (texture profile/quality, language, etc.).

NOT_FOUND = 'notfound'

The requested asset-package-version id is unknown / invalid.

bacommon.automationchannel module

Payload types for the automation SmartSocket channel kind.

Warning

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

One root pair per channel kind (see the hierarchy-per-contract rule in streamcall-smartsocket.md): a driver sends AutomationCommand up, the device answers with AutomationEvent. The relay never decodes either – a new payload type must never require a basn rollout.

The channel is a sibling of the cloud console’s, deliberately: both carry code to a device and results back. What differs is who may – a console session is authorized by account ownership and ships in every build, while this one is authorized by a device-minted key (so a signed-out test device can be driven) and exists only in developer builds. That difference is why the capability is separate rather than an option on the console’s.

Client-visible, so these definitions are a public wire contract once a build speaking them ships: storage names and type-id values may never be repurposed, and removed ones stay retired. Add rather than change.

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.characterbuilder module

Character builders: the authoring form of a character.

A .bchar file in an assets workspace holds one CharacterBuilder as json. It says everything needed for a character to appear in the store and in games – its name, its icon, its default colors, and how it looks and is proportioned in play – by pointing at other assets in the same workspace.

This is an authoring format: written by people (or the workspace editor), stored once, and read by the server, which builds the compact definitions clients actually receive from it. So its keys are plain words, and nothing here is sent to game clients as is.

Every asset is named by its logical path: its source file’s path from the workspace root, minus the extension (zoe/head.obj is zoe/head). Logical paths are unique across all kinds of asset in a workspace, so a path alone is never ambiguous. Assets in other workspaces or packages cannot be referenced; copy what a character needs into its own workspace.

Files inside published package versions are kept forever, so this schema only ever grows: new optional fields, or a new CharacterBuilderData kind beside the existing ones.

class bacommon.characterbuilder.BasicSpazBuilder(name: str, icon_texture: str, icon_mask_texture: str, color: tuple[float, float, float], highlight: tuple[float, float, float], color_texture: str, color_mask_texture: str, head_mesh: str, torso_mesh: str, upper_arm_mesh: str, upper_leg_mesh: str, lower_leg_mesh: str, toes_mesh: str, forearm_mesh: str | None = None, hand_mesh: str | None = None, pelvis_mesh: str | None = None, left_upper_arm_mesh: str | None = None, left_forearm_mesh: str | None = None, left_hand_mesh: str | None = None, left_upper_leg_mesh: str | None = None, left_lower_leg_mesh: str | None = None, left_toes_mesh: str | None = None, highlight_name: str = '', highlight_editing: bool = True, highlight2: tuple[float, float, float] = (1.0, 1.0, 1.0), highlight2_name: str = '', highlight2_editing: bool = False, jump_sounds: list[str] = <factory>, attack_sounds: list[str] = <factory>, impact_sounds: list[str] = <factory>, death_sounds: list[str] = <factory>, pickup_sounds: list[str] = <factory>, fall_sounds: list[str] = <factory>, torso_radius: float = 0.15, shoulder_offset: tuple[float, float, float] = (0.0, 0.0, 0.0), thigh_radius: float = 0.04, ankle_radius: float = 0.07, step_separation: float = 0.08, idle_arm_stiffness: float = 1.0, arm_swing: float = 0.6, idle_sway: float = 0.05, eye_style_left: ~bacommon.characterbuilder.BuilderEyeStyle = BuilderEyeStyle.REGULAR, eye_style_right: ~bacommon.characterbuilder.BuilderEyeStyle = BuilderEyeStyle.REGULAR, eye_scale: float = 1.0, eye_offset: tuple[float, float, float] = (0.065, -0.036, 0.205), eye_color: tuple[float, float, float] = (0.5, 0.5, 1.2), eyeball_color: tuple[float, float, float] = (0.46, 0.38, 0.36), eyelid_color: tuple[float, float, float] = (0.5, 0.3, 0.2), eyelid_angle: float = 0.0, reflection_scale: float = 0.1, flippers: bool = False, attachments: list[~bacommon.characterbuilder.BuilderAttachment] = <factory>)[source]

Bases: CharacterBuilderData

A classic spaz: per-body-part meshes sharing one texture.

Has one name, one icon, a color and up to two highlights. Every part is drawn from a single color texture, tinted through a single mask texture: the mask’s red channel takes the character’s color, its green channel the highlight and its blue channel the second highlight. Arm and leg meshes are authored for the character’s right side and mirrored for the left; a left_ version of any of them gives the left side a mesh of its own.

Numbers left out take the standard spaz’s values, so a builder need only state what makes its character different.

ankle_radius: float = 0.07
arm_swing: float = 0.6

How far the arms swing while walking.

attachments: list[BuilderAttachment]

Extra pieces hung off the character’s body parts, such as hair, antennas and hats.

attack_sounds: list[str]
color: tuple[float, float, float]

The character’s own colors. It is shown in these wherever nobody has picked others (the store, a new profile).

color_mask_texture: str
color_texture: str
death_sounds: list[str]
eye_color: tuple[float, float, float] = (0.5, 0.5, 1.2)

Iris color.

eye_offset: tuple[float, float, float] = (0.065, -0.036, 0.205)

Where one eye sits relative to the head’s center; the other mirrors it.

eye_scale: float = 1.0
eye_style_left: BuilderEyeStyle = 'regular'
eye_style_right: BuilderEyeStyle = 'regular'
eyeball_color: tuple[float, float, float] = (0.46, 0.38, 0.36)
eyelid_angle: float = 0.0

Resting eyelid tilt in degrees (positive reads as angry).

eyelid_color: tuple[float, float, float] = (0.5, 0.3, 0.2)
fall_sounds: list[str]
flippers: bool = False

For a character whose arms are flippers or fins, meaning one long upper-arm mesh that is the whole limb. Its arms are drawn shorter while it wears boxing gloves, so they don’t poke through the gloves. This changes only that; to leave out the forearms and hands such a character doesn’t have, leave forearm_mesh and hand_mesh unset.

forearm_mesh: str | None = None

Optional. Left out, nothing is drawn for that part (an upper-arm mesh may be the whole arm; a big round body may hide its pelvis).

classmethod get_type_id() → CharacterBuilderDataTypeID[source]

Return the type-id for this subclass.

hand_mesh: str | None = None
head_mesh: str
highlight: tuple[float, float, float]
highlight2: tuple[float, float, float] = (1.0, 1.0, 1.0)

A second highlight, taken by the masks’ blue channel. White (the default) leaves what that channel covers untinted, so a character that doesn’t use one needn’t say anything.

highlight2_editing: bool = False
highlight2_name: str = ''

As highlight_name and highlight_editing, for the second highlight (plain name: ‘Highlight 2’). Not editable unless a character says so: most don’t have one.

highlight_editing: bool = True

Whether someone using the character may pick its highlight themselves. Off for one whose highlight is part of who it is.

highlight_name: str = ''

What the highlight is called where someone picks it (a profile editor): the logical path of a string in the same workspace, as name is, for a character whose highlight is something in particular (‘Hair Color’). Empty: plain ‘Highlight’.

icon_mask_texture: str
icon_texture: str

The picture that stands for the character in menus, and its tint mask (red takes the color, green the highlight).

idle_arm_stiffness: float = 1.0

How stiffly the lower arms are held while idle. 1 is fully rigid; lower values give relaxed dangling arms.

idle_sway: float = 0.05

How far the body sways while standing.

impact_sounds: list[str]
jump_sounds: list[str]
left_forearm_mesh: str | None = None
left_hand_mesh: str | None = None
left_lower_leg_mesh: str | None = None
left_toes_mesh: str | None = None
left_upper_arm_mesh: str | None = None

Optional left-side versions of the arm and leg meshes, for a character whose two sides differ (one clawed hand, a peg leg). Each replaces the mesh of the same name for the character’s left side only; left out, the left side draws that mesh as usual. A left mesh is still authored as a right-side part and drawn mirrored, exactly as the shared one is. (A side can’t be given nothing this way: leaving one out means ‘same as the right’.)

left_upper_leg_mesh: str | None = None
lower_leg_mesh: str
name: str

The character’s name, as the logical path of a string (a .bstr file) in the same workspace, so it can be translated.

pelvis_mesh: str | None = None
pickup_sounds: list[str]
reflection_scale: float = 0.1

How strongly the body mirrors its surroundings. 0 is matte; higher is glossier.

shoulder_offset: tuple[float, float, float] = (0.0, 0.0, 0.0)
step_separation: float = 0.08

Sideways distance between the feet while walking.

thigh_radius: float = 0.04
toes_mesh: str
torso_mesh: str
torso_radius: float = 0.15
upper_arm_mesh: str
upper_leg_mesh: str
class bacommon.characterbuilder.BuilderAttachSide(*values)[source]

Bases: Enum

Which of a paired body part’s two sides an attachment is on.

Like the limb meshes themselves, a limb (or wing) attachment is authored for the character’s right side and drawn mirrored on the left.

BOTH = 'both'

Both sides. The only choice for the head, torso and pelvis.

LEFT = 'left'
RIGHT = 'right'
class bacommon.characterbuilder.BuilderAttachTarget(*values)[source]

Bases: Enum

A body part an attachment can hang off.

FOREARM = 'forearm'
HAND = 'hand'
HEAD = 'head'
LOWER_LEG = 'lower_leg'
PELVIS = 'pelvis'
TOES = 'toes'
TORSO = 'torso'
UPPER_ARM = 'upper_arm'
UPPER_LEG = 'upper_leg'
WING = 'wing'

Not a body part but a place wings go; a pair of spots at the character’s back that swing and flap as wings do. Whatever is attached here is the wing, and a character has wings exactly when something is. Its forward (+z) points out along the wing from the middle of the torso. Sided like a limb, and like a limb it takes static attachments only.

class bacommon.characterbuilder.BuilderAttachment(target: ~bacommon.characterbuilder.BuilderAttachTarget, kind: ~bacommon.characterbuilder.BuilderAttachmentKind, segments: list[~bacommon.characterbuilder.BuilderAttachmentSegment], side: ~bacommon.characterbuilder.BuilderAttachSide = BuilderAttachSide.BOTH, transform: ~bacommon.characterbuilder.BuilderTransform = <factory>, stiffness: float = 0.5, damping: float = 0.5, drag: float = 0.5, curl: float = 0.0, length: float = 0.5, radius: float = 0.0, curl_change: float = 0.0, length_change: float = 0.0, radius_change: float = 0.0, stiffness_change: float = 0.0, damping_change: float = 0.0)[source]

Bases: object

One piece hung off one of a character’s body parts.

The tuning numbers below run 0 to 1 (or -1 to 1 where noted) and what they adjust depends on the kind; a static attachment ignores them all.

curl: float = 0.0

Resting bend at every joint of a chain, -1 to 1. Positive bends toward the attachment’s own up; 1 is a half turn per joint.

curl_change: float = 0.0

How much the same-named number above changes from one segment of a chain to the next, -1 to 1 each. A chain can run stiff at the root and floppy at the tip, say.

damping: float = 0.5

How quickly its motion settles.

damping_change: float = 0.0
drag: float = 0.5

How much it trails behind a moving character.

kind: BuilderAttachmentKind
length: float = 0.5

Segment length. The default 0.5 is the kind’s own length; 0 is half of it and 1 is one and a half times it. Art is unaffected.

length_change: float = 0.0
radius: float = 0.0

Segment thickness. The default 0 is the kind’s own; 1 is three times it. Art is unaffected.

radius_change: float = 0.0
segments: list[BuilderAttachmentSegment]

Art for each of the kind’s segments, root first.

side: BuilderAttachSide = 'both'
stiffness: float = 0.5

How firmly it springs back to its resting pose.

stiffness_change: float = 0.0
target: BuilderAttachTarget

The body part it hangs off.

transform: BuilderTransform

Where it is anchored on the body part and which way it rests. A static attachment is simply drawn there. Scale is not used; resize a segment’s own transform instead.

class bacommon.characterbuilder.BuilderAttachmentKind(*values)[source]

Bases: Enum

How an attachment moves.

Each kind is a ready-made physical rig; an attachment picks one, places it, tunes it and supplies art for its segments. All of them are for looks only and never affect play.

The LEGACY_ kinds reproduce one classic hair rig and twirl oddly when disturbed; prefer the ANTENNA kinds for anything new.

ANTENNA = 'antenna'

A stiff single capsule that mostly holds its pose, for antennas, ears, horns and the like. One segment.

ANTENNA_2 = 'antenna_2'

A chain of two such capsules. Two segments.

ANTENNA_3 = 'antenna_3'

A chain of three. Three segments.

ANTENNA_4 = 'antenna_4'

A chain of four. Four segments.

LEGACY_PONYTAIL_2 = 'legacy_ponytail_2'

A stiffer root capsule plus a floppy tip. Two segments.

LEGACY_TUFT_LARGE = 'legacy_tuft_large'

Single softly-jointed capsule. One segment.

LEGACY_TUFT_MEDIUM = 'legacy_tuft_medium'

The same with a medium-sized capsule. One segment.

LEGACY_TUFT_SMALL = 'legacy_tuft_small'

The same with a small capsule. One segment.

STATIC = 'static'

No movement at all, just art fixed to the body part, for hats, horns, badges and the like. One segment. The only kind limbs and wings can take.

class bacommon.characterbuilder.BuilderAttachmentSegment(mesh: str, texture: str | None = None, mask_texture: str | None = None, transform: BuilderTransform = <factory>)[source]

Bases: object

The art for one segment of an attachment.

Drawn with the character’s own texture, tint mask, color and highlight, like every other piece of it, unless it names textures of its own.

mask_texture: str | None = None

A tint mask of its own, read as the character’s color_mask_texture is. Left out, that one is used. For art with a texture of its own that should take no tint at all, name an all-black mask here.

mesh: str

The segment’s mesh, by logical path.

texture: str | None = None

A color texture of its own, by logical path. Left out, the character’s color_texture is used, which suits art whose picture is part of the character’s one texture.

transform: BuilderTransform

How the mesh is drawn on its segment (for a static attachment, on the body part itself).

class bacommon.characterbuilder.BuilderEyeStyle(*values)[source]

Bases: Enum

How one of a character’s eyes is drawn.

LIDLESS = 'lidless'

Bare eyeball; the lid appears only during blinks.

NONE = 'none'

No eye drawn at all (painted-on or eyeless heads, eyepatches).

REGULAR = 'regular'

Eyeball plus a resting/blinking eyelid.

class bacommon.characterbuilder.BuilderTransform(translate: tuple[float, float, float] = (0.0, 0.0, 0.0), rotate: tuple[float, float, float] = (0.0, 0.0, 0.0), scale: tuple[float, float, float] = (1.0, 1.0, 1.0))[source]

Bases: object

A placement: move, turn and resize, in that order of effect.

Applied to a thing in the space of whatever it is attached to. The thing is resized about its own origin, then turned, then moved; so scale never changes how far translate moves it, and rotate never swings it around some distant point. Left out entirely, a transform changes nothing.

rotate is heading, pitch and roll in degrees, applied in that order and described the way an aircraft’s are, the thing’s ‘nose’ being its own forward (+z) axis:

  • heading turns it about the up axis of what it is attached to. Positive turns right, so something pointing out of a character’s nose swings toward the character’s right ear.

  • pitch then raises its nose. Positive aims it up, so toward the top of a head.

  • roll then spins it about its own nose without changing where that points. Positive is clockwise seen from behind.

rotate: tuple[float, float, float] = (0.0, 0.0, 0.0)
scale: tuple[float, float, float] = (1.0, 1.0, 1.0)
translate: tuple[float, float, float] = (0.0, 0.0, 0.0)
bacommon.characterbuilder.CHARACTER_BUILDER_EXTENSION = '.bchar'

The file extension of a character builder in a workspace.

class bacommon.characterbuilder.CharacterBuilder(data: CharacterBuilderData)[source]

Bases: object

One character; a .bchar file’s contents.

Deliberately just a wrapper: everything that defines the character – even its name, icon and colors – belongs to its CharacterBuilderData kind, so a later kind is free to define any of those differently (several highlights, none, …). Fields land here only if they turn out to be common to every kind.

data: CharacterBuilderData

The character itself.

class bacommon.characterbuilder.CharacterBuilderData[source]

Bases: IOMultiType[CharacterBuilderDataTypeID]

A complete character of some kind; see the kinds below.

A kind owns everything about its characters: name, icon, colors and in-game form alike.

classmethod get_type(type_id: CharacterBuilderDataTypeID) → type[CharacterBuilderData][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() → CharacterBuilderDataTypeID[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.characterbuilder.CharacterBuilderDataTypeID(*values)[source]

Bases: Enum

Type ID for each kind of character a builder can describe.

BASIC_SPAZ = 'basic_spaz'

bacommon.characterranges module

The allowed span of each numeric value in a character.

The one table of these spans. The game clamps every value it reads to its span here (its constants are generated from this module), the cloud checks delivered characters against it, and authoring tools show and build characters clamped to it, so what an author sees is what plays.

Physique spans are exactly what the legacy style presets covered. They are simulation state (every machine in a game derives the same rigid bodies from them), so widening one changes gameplay. The look spans are looser than legacy to give characters visual headroom.

Changing a span here changes it everywhere at once, the next time each side is built. An older game build keeps the spans it shipped with, so a widened span only takes effect on builds made after the change.

bacommon.characterranges.ATTACHMENT_RANGES: dict[str, Range] = {'curl': (-1.0, 1.0), 'curl_change': (-1.0, 1.0), 'damping': (0.0, 1.0), 'damping_change': (-1.0, 1.0), 'drag': (0.0, 1.0), 'length': (0.0, 1.0), 'length_change': (-1.0, 1.0), 'position': (-0.5, 0.5), 'radius': (0.0, 1.0), 'radius_change': (-1.0, 1.0), 'stiffness': (0.0, 1.0), 'stiffness_change': (-1.0, 1.0)}

Spans for an attachment’s values. position is where a simulated attachment sits on its body part (the classic hair rig peaks at 0.3); the rest are its tuning dials, whose whole useful span this is, so none can be overdriven.

bacommon.characterranges.BASIC_SPAZ_RANGES: dict[str, Range] = {'ankle_radius': (0.045, 0.07), 'arm_swing': (0.3, 0.6), 'color': (0.0, 1.0), 'eye_color': (0.0, 2.0), 'eye_offset': (-0.5, 0.5), 'eye_scale': (0.1, 2.0), 'eyeball_color': (0.0, 1.0), 'eyelid_angle': (-30.0, 30.0), 'eyelid_color': (0.0, 1.0), 'highlight': (0.0, 1.0), 'idle_arm_stiffness': (0.2, 1.0), 'idle_sway': (0.02, 0.05), 'reflection_scale': (0.0, 2.0), 'shoulder_offset': (-0.05, 0.03), 'step_separation': (0.03, 0.08), 'team_coloring_strength': (0.0, 1.0), 'thigh_radius': (0.04, 0.06), 'torso_radius': (0.11, 0.3)}

Spans for a basic spaz’s own values, keyed by what the value is. highlight covers every highlight color and team_coloring_strength every team-coloring strength.

type bacommon.characterranges.Range = tuple[float, float]

An inclusive (low, high) span. For a value with several components (a color, an offset) it applies to each one.

bacommon.characterranges.clamp(value: float, span: Range) → float[source]

Return a value held within a span.

bacommon.characterrig module

The fixed numbers of a character’s rig.

How a character is put together physically: the bodies it is simulated as, where its joints anchor on them, and how its art is placed on them. These are the game’s own values. The game’s code is generated from this module, so anything else that reads it (tools that show a character outside the game, say) is looking at exactly what plays.

Nearly everything here is simulation state: every machine in a game builds the same bodies from it, so changing a number changes gameplay for every character at once. Treat edits accordingly.

bacommon.characterrig.ATTACHMENT_RIGS: dict[str, tuple[AttachmentSegmentRig, ...]] = {'antenna': (AttachmentSegmentRig(geom_radius=0.04, geom_length=0.13, mass_radius=0.07, mass_length=0.13, density=0.01, parent_anchor=(0.0, 0.0, 0.0), child_anchor=(0.0, 0.0, -0.065), linear_stiffness=0.8, linear_damping=0.0005, angular_stiffness=1e-05, angular_damping=1e-08, collides=True),), 'antenna_2': (AttachmentSegmentRig(geom_radius=0.04, geom_length=0.13, mass_radius=0.07, mass_length=0.13, density=0.01, parent_anchor=(0.0, 0.0, 0.0), child_anchor=(0.0, 0.0, -0.065), linear_stiffness=0.8, linear_damping=0.0005, angular_stiffness=1e-05, angular_damping=1e-08, collides=True), AttachmentSegmentRig(geom_radius=0.04, geom_length=0.13, mass_radius=0.07, mass_length=0.13, density=0.01, parent_anchor=(0.0, 0.0, 0.065), child_anchor=(0.0, 0.0, -0.065), linear_stiffness=0.8, linear_damping=0.0005, angular_stiffness=1e-05, angular_damping=1e-08, collides=True)), 'antenna_3': (AttachmentSegmentRig(geom_radius=0.04, geom_length=0.13, mass_radius=0.07, mass_length=0.13, density=0.01, parent_anchor=(0.0, 0.0, 0.0), child_anchor=(0.0, 0.0, -0.065), linear_stiffness=0.8, linear_damping=0.0005, angular_stiffness=1e-05, angular_damping=1e-08, collides=True), AttachmentSegmentRig(geom_radius=0.04, geom_length=0.13, mass_radius=0.07, mass_length=0.13, density=0.01, parent_anchor=(0.0, 0.0, 0.065), child_anchor=(0.0, 0.0, -0.065), linear_stiffness=0.8, linear_damping=0.0005, angular_stiffness=1e-05, angular_damping=1e-08, collides=True), AttachmentSegmentRig(geom_radius=0.04, geom_length=0.13, mass_radius=0.07, mass_length=0.13, density=0.01, parent_anchor=(0.0, 0.0, 0.065), child_anchor=(0.0, 0.0, -0.065), linear_stiffness=0.8, linear_damping=0.0005, angular_stiffness=1e-05, angular_damping=1e-08, collides=True)), 'antenna_4': (AttachmentSegmentRig(geom_radius=0.04, geom_length=0.13, mass_radius=0.07, mass_length=0.13, density=0.01, parent_anchor=(0.0, 0.0, 0.0), child_anchor=(0.0, 0.0, -0.065), linear_stiffness=0.8, linear_damping=0.0005, angular_stiffness=1e-05, angular_damping=1e-08, collides=True), AttachmentSegmentRig(geom_radius=0.04, geom_length=0.13, mass_radius=0.07, mass_length=0.13, density=0.01, parent_anchor=(0.0, 0.0, 0.065), child_anchor=(0.0, 0.0, -0.065), linear_stiffness=0.8, linear_damping=0.0005, angular_stiffness=1e-05, angular_damping=1e-08, collides=True), AttachmentSegmentRig(geom_radius=0.04, geom_length=0.13, mass_radius=0.07, mass_length=0.13, density=0.01, parent_anchor=(0.0, 0.0, 0.065), child_anchor=(0.0, 0.0, -0.065), linear_stiffness=0.8, linear_damping=0.0005, angular_stiffness=1e-05, angular_damping=1e-08, collides=True), AttachmentSegmentRig(geom_radius=0.04, geom_length=0.13, mass_radius=0.07, mass_length=0.13, density=0.01, parent_anchor=(0.0, 0.0, 0.065), child_anchor=(0.0, 0.0, -0.065), linear_stiffness=0.8, linear_damping=0.0005, angular_stiffness=1e-05, angular_damping=1e-08, collides=True)), 'legacy_ponytail_2': (AttachmentSegmentRig(geom_radius=0.09, geom_length=0.1, mass_radius=0.0, mass_length=0.0, density=0.01, parent_anchor=(0.0, 0.0, 0.0), child_anchor=(0.0, -0.01, 0.1), linear_stiffness=1.0, linear_damping=0.03, angular_stiffness=0.0015, angular_damping=3e-06, collides=True), AttachmentSegmentRig(geom_radius=0.09, geom_length=0.13, mass_radius=0.0, mass_length=0.0, density=0.01, parent_anchor=(0.0, 0.01, -0.1), child_anchor=(0.0, -0.01, 0.12), linear_stiffness=0.4, linear_damping=0.02, angular_stiffness=0.00025, angular_damping=1e-06, collides=False)), 'legacy_tuft_large': (AttachmentSegmentRig(geom_radius=0.07, geom_length=0.13, mass_radius=0.0, mass_length=0.0, density=0.01, parent_anchor=(0.0, 0.0, 0.0), child_anchor=(0.0, -0.08, -0.12), linear_stiffness=0.2, linear_damping=0.01, angular_stiffness=0.00025, angular_damping=1e-06, collides=True),), 'legacy_tuft_medium': (AttachmentSegmentRig(geom_radius=0.055, geom_length=0.13, mass_radius=0.07, mass_length=0.13, density=0.01, parent_anchor=(0.0, 0.0, 0.0), child_anchor=(0.0, -0.08, -0.12), linear_stiffness=0.2, linear_damping=0.01, angular_stiffness=0.00025, angular_damping=1e-06, collides=True),), 'legacy_tuft_small': (AttachmentSegmentRig(geom_radius=0.04, geom_length=0.13, mass_radius=0.07, mass_length=0.13, density=0.01, parent_anchor=(0.0, 0.0, 0.0), child_anchor=(0.0, -0.08, -0.12), linear_stiffness=0.2, linear_damping=0.01, angular_stiffness=0.00025, angular_damping=1e-06, collides=True),)}

The segments of each kind of simulated attachment, first segment first. Keyed by kind, in the game’s own order of kinds, which must not change (a static attachment has no rig and is not here).

class bacommon.characterrig.AttachmentSegmentRig(geom_radius: float, geom_length: float, mass_radius: float, mass_length: float, density: float, parent_anchor: Vec3, child_anchor: Vec3, linear_stiffness: float, linear_damping: float, angular_stiffness: float, angular_damping: float, collides: bool = True)[source]

Bases: object

The rig of one simulated segment of an attachment.

A segment is a capsule lying along its own z axis, hung by a spring joint from the segment before it (or, for the first, from the spot on the body part where its attachment sits).

angular_damping: float

How strongly that turning is damped.

angular_stiffness: float

How strongly its joint’s spring turns it to its rest angle.

child_anchor: Vec3

Where its joint anchors on itself, in its own space.

collides: bool = True

Whether it bumps into things (a ponytail’s tip does not).

density: float

How dense that capsule is.

geom_length: float

Length of the capsule that collides, not counting its caps.

geom_radius: float

Radius of the capsule that collides.

linear_damping: float

How strongly that pull is damped.

linear_stiffness: float

How strongly its joint’s spring pulls it into place.

mass_length: float

Length of that capsule, likewise.

mass_radius: float

Radius of the capsule its mass is worked out from, or 0 to use the one that collides.

parent_anchor: Vec3

Where its joint anchors on the segment before it, in that segment’s space. Unused for a first segment.

bacommon.characterrig.SPAZ_RIG = SpazRig(head_radius=0.23, pelvis_size=(0.35, 0.16, 0.1), roller_radius=0.3, upper_arm_radius=0.06, upper_arm_length=0.16, lower_arm_radius=0.06, lower_arm_length=0.13, upper_leg_length=0.12, lower_leg_span=0.26, toes_radius=0.075, stand_head_height=2.25, stand_torso_height=1.8, stand_pelvis_height=1.66, stand_roller_height=1.6, pelvis_joint_drop=0.05, pelvis_joint_forward=0.05, shoulder_on_torso=(-0.15, 0.14, 0.0), shoulder_on_upper_arm=(0.02, 0.0, -0.1), elbow_on_upper_arm=(0.0, 0.0, 0.07), elbow_on_lower_arm=(0.0, 0.0, -0.08), hip_on_pelvis=(-0.1, -0.01, 0.0), hip_on_upper_leg=(0.0, 0.0, -0.05), knee_on_upper_leg=(0.0, 0.0, 0.05), knee_on_lower_leg=(0.0, 0.0, -0.05), ankle_on_lower_leg=(0.0, 0.05, 0.05), ankle_on_toes=(0.0, -0.04, 0.0), forearm_stretch_pivot=0.1, hand_offset=0.04, hand_turn=10.0, eye_mesh_scale=0.09, eyeball_pitch=-10.0)

The spaz rig’s numbers.

class bacommon.characterrig.SpazRig(head_radius: float = 0.23, pelvis_size: Vec3 = (0.35, 0.16, 0.1), roller_radius: float = 0.3, upper_arm_radius: float = 0.06, upper_arm_length: float = 0.16, lower_arm_radius: float = 0.06, lower_arm_length: float = 0.13, upper_leg_length: float = 0.12, lower_leg_span: float = 0.26, toes_radius: float = 0.075, stand_head_height: float = 2.25, stand_torso_height: float = 1.8, stand_pelvis_height: float = 1.66, stand_roller_height: float = 1.6, pelvis_joint_drop: float = 0.05, pelvis_joint_forward: float = 0.05, shoulder_on_torso: Vec3 = (-0.15, 0.14, 0.0), shoulder_on_upper_arm: Vec3 = (0.02, 0.0, -0.1), elbow_on_upper_arm: Vec3 = (0.0, 0.0, 0.07), elbow_on_lower_arm: Vec3 = (0.0, 0.0, -0.08), hip_on_pelvis: Vec3 = (-0.1, -0.01, 0.0), hip_on_upper_leg: Vec3 = (0.0, 0.0, -0.05), knee_on_upper_leg: Vec3 = (0.0, 0.0, 0.05), knee_on_lower_leg: Vec3 = (0.0, 0.0, -0.05), ankle_on_lower_leg: Vec3 = (0.0, 0.05, 0.05), ankle_on_toes: Vec3 = (0.0, -0.04, 0.0), forearm_stretch_pivot: float = 0.1, hand_offset: float = 0.04, hand_turn: float = 10.0, eye_mesh_scale: float = 0.09, eyeball_pitch: float = -10.0)[source]

Bases: object

The fixed numbers of a spaz’s own rig (see SPAZ_RIG).

Lengths are in the game’s world units and angles in degrees. A capsule lies along its own z axis and its length leaves out its two round caps. Anything given for one side is the character’s right; the left mirrors it in x.

ankle_on_lower_leg: Vec3 = (0.0, 0.05, 0.05)

An ankle’s place on its lower leg.

ankle_on_toes: Vec3 = (0.0, -0.04, 0.0)

An ankle’s place on its foot.

elbow_on_lower_arm: Vec3 = (0.0, 0.0, -0.08)

An elbow’s place on its lower arm.

elbow_on_upper_arm: Vec3 = (0.0, 0.0, 0.07)

An elbow’s place on its upper arm.

eye_mesh_scale: float = 0.09

The size the shared eye art (ball, iris and lid) is drawn at.

eyeball_pitch: float = -10.0

An eyeball’s resting turn about its x axis, so it looks slightly down.

forearm_stretch_pivot: float = 0.1

The point this far along a forearm is the one its art shrinks about when flippers draw at half length (under boxing gloves).

hand_offset: float = 0.04

A hand’s art sits this far along the lower arm.

hand_turn: float = 10.0

How far an empty hand’s art is turned about the lower arm’s y axis.

head_radius: float = 0.23

Radius of the head’s ball.

hip_on_pelvis: Vec3 = (-0.1, -0.01, 0.0)

A hip’s place on the pelvis.

hip_on_upper_leg: Vec3 = (0.0, 0.0, -0.05)

A hip’s place on its upper leg.

knee_on_lower_leg: Vec3 = (0.0, 0.0, -0.05)

A knee’s place on its lower leg.

knee_on_upper_leg: Vec3 = (0.0, 0.0, 0.05)

A knee’s place on its upper leg.

lower_arm_length: float = 0.13

Length of a lower arm’s capsule.

lower_arm_radius: float = 0.06

Radius of a lower arm’s capsule.

lower_leg_span: float = 0.26

A lower leg’s capsule is this long less twice its radius (the character’s ankle radius), so its whole span stays the same whatever its thickness.

pelvis_joint_drop: float = 0.05

The joint between pelvis and torso sits this far below the torso’s center.

pelvis_joint_forward: float = 0.05

On the torso, that joint also sits this far forward, like the curve of a spine.

pelvis_size: Vec3 = (0.35, 0.16, 0.1)

Width, height and depth of the pelvis’s box.

roller_radius: float = 0.3

Radius of the ball a spaz rolls around on, at its full (standing) size.

shoulder_on_torso: Vec3 = (-0.15, 0.14, 0.0)

A shoulder’s place on the torso, before the character’s own shoulder offset.

shoulder_on_upper_arm: Vec3 = (0.02, 0.0, -0.1)

A shoulder’s place on its upper arm.

stand_head_height: float = 2.25

Starting height of the head’s center.

stand_pelvis_height: float = 1.66

Starting height of the pelvis’s center.

stand_roller_height: float = 1.6

Starting height of the roller ball’s center.

stand_torso_height: float = 1.8

Starting height of the torso’s center.

toes_radius: float = 0.075

Radius of a foot’s ball.

upper_arm_length: float = 0.16

Length of an upper arm’s capsule.

upper_arm_radius: float = 0.06

Radius of an upper arm’s capsule.

upper_leg_length: float = 0.12

Length of an upper leg’s capsule (its radius is the character’s thigh radius).

type bacommon.characterrig.Vec3 = tuple[float, float, float]

Three numbers: a point or offset as (x, y, z). Y is up, z is forward, and a character’s own right is -x.

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.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.

Warning

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

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.consolechannel module

Payload types for the cloud-console SmartSocket channel kind.

Warning

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

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.

bacommon.depiction module

Depictions: self-sufficient things for clients to draw.

A depiction describes something to show – a character’s icon, a name, a character in 3D, an image – that a client draws natively wherever it is hosted: a doc-ui page (see bacommon.docui.v2.Depiction), a page’s viewer pane, and later widgets and in-game nodes. The host decides where and how big; the depiction decides what is drawn there.

Depictions are self-sufficient: each carries whatever package references it needs, and never adds packages to whatever contains it. Art that isn’t local yet shows as a standin and upgrades in place when it arrives; the container never waits on it.

A client that doesn’t recognize a depiction’s kind draws a placeholder (an outlined box with a question mark) and warns once, since producers should only send kinds a client understands. Any finer-grained degradation – a name drawing plain text when its styling is too new for the client, say – is each kind’s own business.

class bacommon.depiction.BlankDepiction(color_override: tuple[float, float, float] | None = None, team_coloring: bool = False)[source]

Bases: Depiction

Nothing at all: an empty doc-ui viewer pane, say.

Whatever its host draws around it (a viewer pane’s background and frame) is what shows. A doc-ui page wanting an empty pane beside its rows shows one of these, since a page with no viewer at all gets no pane (see bacommon.docui.v2.Page.viewer). Clients before bacommon.docui.v2.PAGE_DRIVEN_VIEWER_BUILD don’t know it (see bacommon.docui.v2.blank_viewer()).

color_override: tuple[float, float, float] | None = None

See Depiction. (Ignored; there’s nothing to color.)

classmethod get_type_id() → DepictionTypeID[source]

Return the type-id for this subclass.

team_coloring: bool = False

See Depiction. (Ignored, as above.)

class bacommon.depiction.CharacterIconDepiction(icon: str, color_override: tuple[float, float, float] | None = None, team_coloring: bool = False)[source]

Bases: Depiction

A character’s icon, drawn square.

icon is an opaque, cloud-composed icon definition (a character’s icon component on its own; the schema is server-private, see bamaster baserver/character.py), tinted by its own colors. A standard-character standin shows while the art isn’t local.

A color override replaces the icon’s main color, the way a team’s color replaces a character’s own on its spaz; highlights stay the icon’s own.

color_override: tuple[float, float, float] | None = None

See Depiction.

classmethod get_type_id() → DepictionTypeID[source]

Return the type-id for this subclass.

icon: str
team_coloring: bool = False

See Depiction.

class bacommon.depiction.CharacterViewerDepiction(spaz: str, camera_shake: float = 1.0, camera_gyro: float = 1.0, color_override: tuple[float, float, float] | None = None, team_coloring: bool = False)[source]

Bases: Depiction

A character standing in a little scene of its own, live in 3D.

spaz is an opaque, cloud-composed spaz definition (a character’s in-game form on its own; see CharacterIconDepiction); it alone decides how the character looks, colors included. A host keeps the viewer running across updates, so a changed definition (an editor’s draft, say) updates the picture in place rather than starting it over.

camera_gyro: float = 1.0

How far turning the device (on devices with a gyro) swings the camera around the character, relative to the usual: 2.0 swings twice as far, 0 (or less) not at all, and 10 is the most there is (more counts as 10). (Not at all regardless while the viewer has camera gyro disabled.)

camera_shake: float = 1.0

How much the camera’s hand-held wobble moves, relative to the usual: 2.0 wobbles twice as much, 0 (or less) holds it still, and 10 is the most there is (more counts as 10). (Still regardless while the viewer has camera shake disabled.)

color_override: tuple[float, float, float] | None = None

See Depiction. (Viewers ignore it; the definition alone decides the character’s colors.)

classmethod get_type_id() → DepictionTypeID[source]

Return the type-id for this subclass.

spaz: str
team_coloring: bool = False

See Depiction.

class bacommon.depiction.Depiction[source]

Bases: IOMultiType[DepictionTypeID]

Something for a client to draw; see the module docs.

Every kind carries an optional color_override: one rgb color imposed on the depiction, baked in. It is the static form of the color override a host can impose live (a display node’s color_override, say), and a host’s wins where both are set. Each kind decides what it applies to (a name’s text and whatever its capsule routes it to, a character icon’s main color); kinds with nothing to apply it to ignore it. A player’s icon in a teams game carries its team’s color this way, so it shows in that color wherever it is drawn.

Every kind likewise carries team_coloring: the depiction’s main color is a team’s and should stay its clearly dominant one. Each kind decides what that means, toning its other colors down (a character icon’s highlights; the parts of a name its override isn’t routed to); kinds with nothing to tone ignore it. The main color is the color override if there is one, else whatever the kind takes as its own. Hosts can also turn it on live (a display node’s team_coloring).

(Both are declared on each kind rather than here, as this base holds no fields; they are co and tco on the wire for all.)

classmethod get_type(type_id: DepictionTypeID) → type[Depiction][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() → DepictionTypeID[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).

classmethod get_unknown_type_fallback() → Depiction[source]

Return a fallback object in cases of unrecognized types.

This can allow newer data to remain readable in older environments. Use caution with this option, however, as it effectively modifies data.

class bacommon.depiction.DepictionTypeID(*values)[source]

Bases: Enum

Type ID for each of our subclasses.

BLANK = 'bl'
CHARACTER_ICON = 'ci'
CHARACTER_VIEWER = 'cv'
IMAGE = 'i'
MESH_VIEWER = 'mv'
NAME = 'n'
UNKNOWN = 'u'
class bacommon.depiction.ImageDepiction(texture: TextureSpec | int, aspect: float = 1.0, scale: float = 1.0, color: tuple[float, float, float, float] | None = None, mask_texture: TextureSpec | int | None = None, tint_texture: TextureSpec | int | None = None, tint_color: tuple[float, float, float] | None = None, tint2_color: tuple[float, float, float] | None = None, tint3_color: tuple[float, float, float] | None = None, packages: list[ApverNum] | None = None, domain_digest: str | None = None, color_override: tuple[float, float, float] | None = None, team_coloring: bool = False)[source]

Bases: Depiction

A texture, fetched on demand, optionally masked and tinted.

For art that must be self-sufficient – anything whose package the page (or other container) should not have to resolve first – such as the main toolbar’s chests. Page art that resolves with its page is better served by the bacommon.docui.v2.Image decoration.

Texture slots take a full spec or, in the compact wire form, an integer index into the index domain laid out over packages (as character components do); producers send the compact form, with domain_digest guarding the indices.

aspect: float = 1.0

Width over height. Stated here rather than taken from the loaded texture so the standin shown until the art arrives has the same shape, and nothing shifts when it does.

color: tuple[float, float, float, float] | None = None
color_override: tuple[float, float, float] | None = None

See Depiction. (Images ignore it for now.)

domain_digest: str | None = None

Producer’s (prefix) digest of that domain, checked by the client before resolving indices (see bacommon.assetspec.wire_digest()).

classmethod get_type_id() → DepictionTypeID[source]

Return the type-id for this subclass.

mask_texture: TextureSpec | int | None = None
packages: list[ApverNum] | None = None

Packages laid out (in order) as the index domain integer texture slots index into; None when every slot is a full spec. (A plain None default rather than a list factory: a factory default breaks Sphinx’s signature rendering of this class.)

scale: float = 1.0

Draws the image this much bigger or smaller about its box’s center, for art with glows or other extras that should spill past where it is placed. Only the drawing changes: the box (and so layout and what counts as a press) stays put. Clients clamp this to 0-2; ones before this field draw at 1.

team_coloring: bool = False

See Depiction.

texture: TextureSpec | int
tint2_color: tuple[float, float, float] | None = None
tint3_color: tuple[float, float, float] | None = None

Tint through the tint texture’s blue channel (as tint_color is red and tint2_color green). Clients before this field ignore it.

tint_color: tuple[float, float, float] | None = None
tint_texture: TextureSpec | int | None = None
class bacommon.depiction.MeshViewerCamera[source]

Bases: IOMultiType[MeshViewerCameraTypeID]

A MeshViewerDepiction’s camera, placed by hand.

Either held in one place (MeshViewerStillCamera) or moving (MeshViewerPingPongCamera). A client that doesn’t know a kind frames things automatically, as with no camera at all.

classmethod get_default_type_id() → MeshViewerCameraTypeID[source]

Return a type-id to be assumed when none is present.

By default, dataclassio errors when deserializing multitype data that contains no type-id value. Overriding this to return a type-id changes that behavior: data with no type-id present will be deserialized as the returned type, and instances of that type will be serialized without a type-id value. This both saves a bit of space and allows ‘upgrading’ an existing regular dataclass to a multitype - simply designate the original dataclass type as the default and old serialized data will remain loadable (and data for the default type will remain loadable by old code).

Be aware of the following, however:

  • Once serialized data exists anywhere without type-id values, the default type-id must never be changed or removed; doing so would cause that existing data to be silently reinterpreted as some other type (or to error).

  • A missing type-id normally acts as a sanity check when deserializing; defining a default effectively disables that check, meaning malformed data may deserialize successfully as the default type instead of erroring.

classmethod get_type(type_id: MeshViewerCameraTypeID) → type[MeshViewerCamera][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() → MeshViewerCameraTypeID[source]

Return the type-id for this subclass.

classmethod get_unknown_type_fallback() → MeshViewerCamera[source]

Return a fallback object in cases of unrecognized types.

This can allow newer data to remain readable in older environments. Use caution with this option, however, as it effectively modifies data.

class bacommon.depiction.MeshViewerCameraPose(position: tuple[float, float, float], target: tuple[float, float, float], fov_y: float)[source]

Bases: object

Where a MeshViewerCamera is at some moment.

In the depiction’s own space: the same one its entries’ position values place them in.

fov_y: float

Its vertical field of view, in degrees (between 0 and 180).

position: tuple[float, float, float]

Where it looks from.

target: tuple[float, float, float]

What it looks at.

class bacommon.depiction.MeshViewerCameraTypeID(*values)[source]

Bases: Enum

Type ID for each kind of MeshViewerCamera.

PING_PONG = 'pp'
STILL = 's'
UNKNOWN = 'u'
class bacommon.depiction.MeshViewerDepiction(entries: list[MeshViewerEntry], spin: bool = True, draggable: bool = True, bounds_scale: float = 1.0, view_yaw: float = 0.0, view_pitch: float | None = None, camera: MeshViewerCamera | None = None, camera_shake: float | None = None, camera_gyro: float = 1.0, background_color: tuple[float, float, float] | None = None, packages: list[ApverNum] | None = None, domain_digest: str | None = None, color_override: tuple[float, float, float] | None = None, team_coloring: bool = False)[source]

Bases: Depiction

A few meshes, live in 3D and slowly turning.

Its entries are placed relative to one another; the client frames them all to fill the picture (by the bounds meshes carry), turns them slowly, and lets them be swung around by dragging, much as the asset browser’s mesh preview does. On devices with a gyro, turning the device swings the view around what the camera looks at (see camera_gyro). A host keeps the viewer running across updates, so changed entries reframe in place.

background_color: tuple[float, float, float] | None = None

What shows behind the meshes; None for the host’s usual viewer background (a doc-ui viewer pane’s own blank color, say).

bounds_scale: float = 1.0

How big an area it frames, relative to the bounds of everything in it (about the same center): 2.0 frames twice as much, so its meshes look half as big. Meant for shrinking (1 and up); below 1 crops in (and well below, the camera ends up in among them).

camera: MeshViewerCamera | None = None

A camera placed by hand, for a set shot (held still or moving; see MeshViewerCamera). When given, nothing is framed automatically (bounds_scale and view_pitch don’t apply) and dragging up and down does nothing. The turntable still turns everything about the origin (with spin, view_yaw, and side to side drags).

camera_gyro: float = 1.0

How far turning the device (on devices with a gyro) swings the camera around what it looks at, relative to the usual: 2.0 swings twice as far, 0 (or less) not at all, and 10 is the most there is (more counts as 10). The usual is a generous swing for framed views and a subtler one (a third as far) under a camera placed by hand. (Not at all regardless while the viewer has camera gyro disabled.)

camera_shake: float | None = None

How much the camera’s hand-held wobble moves, as CharacterViewerDepiction.camera_shake does: 1.0 is the character viewer’s usual, 0 (or less) holds it still, and 10 is the most there is. None for the default: 1.0 with a camera placed by hand (a set shot), else 0.

color_override: tuple[float, float, float] | None = None

See Depiction. (Ignored; entries carry their own colors.)

domain_digest: str | None = None

Producer’s (prefix) digest of that domain (see ImageDepiction).

draggable: bool = True

Whether dragging on it swings it around. Off, with spin off too, it’s a completely fixed view.

entries: list[MeshViewerEntry]
classmethod get_type_id() → DepictionTypeID[source]

Return the type-id for this subclass.

packages: list[ApverNum] | None = None

Packages laid out (in order) as the index domain integer slots index into; None when every slot is a full spec (see ImageDepiction).

spin: bool = True

Whether it turns on its own. (When it doesn’t, it always shows exactly its view_yaw to start with.)

team_coloring: bool = False

See Depiction. (Ignored, as above.)

view_pitch: float | None = None

Degrees the camera starts looking down at what it frames (0 head on, up to just short of 90 straight down; negative looks up); None for the usual, about 29.

view_yaw: float = 0.0

Degrees the turntable starts turned (about the vertical axis); with spin off, the side that faces the camera.

class bacommon.depiction.MeshViewerEntry(mesh: MeshSpec | int, texture: TextureSpec | int | None = None, color: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0), transparent: bool = False, reflection: CubeMapTextureSpec | int | None = None, reflection_scale: tuple[float, float, float] = (1.0, 1.0, 1.0), tint_texture: TextureSpec | int | None = None, tint_color: tuple[float, float, float] = (1.0, 1.0, 1.0), tint2_color: tuple[float, float, float] = (1.0, 1.0, 1.0), tint3_color: tuple[float, float, float] = (1.0, 1.0, 1.0), position: tuple[float, float, float] = (0.0, 0.0, 0.0), rotation: tuple[float, float, float] = (0.0, 0.0, 0.0), scale: tuple[float, float, float] = (1.0, 1.0, 1.0))[source]

Bases: object

One mesh shown by a MeshViewerDepiction, and where.

color: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0)

Multiplied into its texture (or its whole color, untextured): rgba. Alpha only shows when transparent.

mesh: MeshSpec | int

The mesh, as a full spec or an index into the depiction’s packages domain.

position: tuple[float, float, float] = (0.0, 0.0, 0.0)

Where it sits, relative to the others.

reflection: CubeMapTextureSpec | int | None = None

A cube map it reflects (a full spec or an index); None for none.

reflection_scale: tuple[float, float, float] = (1.0, 1.0, 1.0)

Multiplied into what it reflects (rgb).

rotation: tuple[float, float, float] = (0.0, 0.0, 0.0)

How it’s turned, in degrees about x, then y, then z.

scale: tuple[float, float, float] = (1.0, 1.0, 1.0)

How it’s stretched along each axis.

texture: TextureSpec | int | None = None

Its texture, likewise; with none it draws in color alone.

tint2_color: tuple[float, float, float] = (1.0, 1.0, 1.0)

Tint through the tint texture’s green channel.

tint3_color: tuple[float, float, float] = (1.0, 1.0, 1.0)

Tint through the tint texture’s blue channel.

tint_color: tuple[float, float, float] = (1.0, 1.0, 1.0)

Tint through the tint texture’s red channel.

tint_texture: TextureSpec | int | None = None

A texture whose red, green and blue channels say where tint_color, tint2_color and tint3_color apply (a character’s color mask, say); None for no tinting.

transparent: bool = False

Whether it’s blended (by color’s alpha and its texture’s) rather than solid. Blended entries draw after every solid one. Blending and tinting don’t combine: a blended entry’s tint is ignored (with a warning).

class bacommon.depiction.MeshViewerPingPongCamera(a: MeshViewerCameraPose, b: MeshViewerCameraPose, travel_time: float, loop: bool = True)[source]

Bases: MeshViewerCamera

A camera moving back and forth between two poses.

It starts at a, sways over to b, and sways back, over and over: one smooth sine wave, fastest midway and easing into each turnaround (position, target and field of view all move together, in straight lines). Time only passes while the viewer is shown, and an update leaving the camera as it was carries on without starting over.

a: MeshViewerCameraPose

Where it starts.

b: MeshViewerCameraPose

Where it goes.

classmethod get_type_id() → MeshViewerCameraTypeID[source]

Return the type-id for this subclass.

loop: bool = True

Whether it keeps going back and forth. Off, it eases over to b just once (with the same timing) and stays there.

travel_time: float

Seconds each way.

class bacommon.depiction.MeshViewerStillCamera(position: tuple[float, float, float], target: tuple[float, float, float], fov_y: float)[source]

Bases: MeshViewerCamera

A camera held in one place.

In the depiction’s own space (see MeshViewerCameraPose, whose fields these are).

fov_y: float

Its vertical field of view, in degrees (between 0 and 180).

classmethod get_type_id() → MeshViewerCameraTypeID[source]

Return the type-id for this subclass.

position: tuple[float, float, float]

Where it looks from.

target: tuple[float, float, float]

What it looks at.

class bacommon.depiction.MeshViewerUnknownCamera[source]

Bases: MeshViewerCamera

A camera of a kind this client doesn’t know.

Treated as no camera at all: things are framed automatically.

classmethod get_type_id() → MeshViewerCameraTypeID[source]

Return the type-id for this subclass.

class bacommon.depiction.NameDepiction(name: str, color_override: tuple[float, float, float] | None = None, team_coloring: bool = False)[source]

Bases: Depiction

A name: an in-game name, an account name, a character’s name.

name is an opaque, cloud-composed name definition (the same form a character’s name component takes; the schema is server-private, see bamaster baserver/character.py). Its basic form is colored text; richer forms (a capsule with an icon, as account names are shown) fall back to it on clients that don’t understand them. Its shape is its measured text (or capsule), fitted into the box by the alignment.

color_override: tuple[float, float, float] | None = None

See Depiction.

classmethod get_type_id() → DepictionTypeID[source]

Return the type-id for this subclass.

name: str
team_coloring: bool = False

See Depiction.

class bacommon.depiction.UnknownDepiction[source]

Bases: Depiction

A depiction of a kind this client doesn’t know.

Drawn as a placeholder (an outlined box with a question mark), with a one-time warning.

classmethod get_type_id() → DepictionTypeID[source]

Return the type-id for this subclass.

bacommon.displayitem module

The producer-side display-item: what to show, and in what style.

Warning

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

A display-item is a request – show this thing in this style, in these bounds – which is what it has always been. It turns into plain doc-ui decorations (DisplayItem.decorations), so the client needs no knowledge of item types to draw one: new types need no client update. The same decorations draw into a doc-ui page or, via the doc-ui prep code, into any plain container widget.

The depiction lives here rather than in either host so there is exactly one of it, serving both the client and the master server. What each host must supply is gathered into DepictionAssets.

bacommon.hostui module

Wire types for host-defined ui (host doc-ui and party notices).

Warning

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

A game host can give connected clients buttons in their in-game menu and party window, serve the doc-ui pages those buttons open, and post a party notice. Everything here rides the per-connection json message channel between host and client (BA_JMESSAGE_HOST_UI), never the scene stream: it is per client, not per activity, and never recorded into replays. Design: docs/initiatives/host-docui.md.

These shapes go between game builds, so once a build speaking them ships, storage names and type-id values may never be repurposed. Add rather than change.

bacommon.legacydisplayitem 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.

Warning

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

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.

bacommon.meshzstddict module

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

Warning

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

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.

Warning

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

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 <NUM> — module declares that it needs the asset-package-version with that numeric id 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.

bacommon.nativeenums module

Enums shared with the native layer.

The engine’s C++ versions of these are generated from this module, so nothing but enum may be imported here, and every class must be an Enum with ascending int values. Values are spelled out because some are stored or sent places; never renumber existing ones. These are also available as babase.InputType and so on.

class bacommon.nativeenums.InputType(*values)[source]

Bases: Enum

Types of input a controller can send to the game.

BOMB_PRESS = 8
BOMB_RELEASE = 9
DOWN_PRESS = 25
DOWN_RELEASE = 26
FLY_PRESS = 13
FLY_RELEASE = 14
HOLD_POSITION_PRESS = 17
HOLD_POSITION_RELEASE = 18
JUMP_PRESS = 4
JUMP_RELEASE = 5
LEFT_PRESS = 19
LEFT_RELEASE = 20
LEFT_RIGHT = 3
PICK_UP_PRESS = 10
PICK_UP_RELEASE = 11
PUNCH_PRESS = 6
PUNCH_RELEASE = 7
RIGHT_PRESS = 21
RIGHT_RELEASE = 22
RUN = 12
START_PRESS = 15
START_RELEASE = 16
UP_DOWN = 2
UP_PRESS = 23
UP_RELEASE = 24
class bacommon.nativeenums.Permission(*values)[source]

Bases: Enum

Permissions that can be requested from the OS.

STORAGE = 0
class bacommon.nativeenums.QuitType(*values)[source]

Bases: Enum

Types of quit behavior that can be requested from the app.

‘soft’ may hide/reset the app but keep the process running, depending

on the platform (generally a thing on mobile).

‘back’ is a variant of ‘soft’ which may give ‘back-button-pressed’

behavior depending on the platform. (returning to some previous activity instead of dumping to the home screen, etc.)

‘hard’ leads to the process exiting. This generally should be avoided

on platforms such as mobile where apps are expected to keep running until killed by the OS.

BACK = 1
HARD = 2
SOFT = 0
class bacommon.nativeenums.SoundPriority(*values)[source]

Bases: Enum

How much a sound matters when more sounds want to play than can at once. When everything is busy, a new sound takes the place of the oldest playing sound of the lowest priority at or below its own (sounds that loop are never cut off this way).

Sounds are ‘default’ unless their asset says otherwise.

DEFAULT = 2
HIGH = 3
HIGHER = 4
LOW = 1
LOWER = 0
class bacommon.nativeenums.UIScale(*values)[source]

Bases: Enum

The overall scale the UI is being rendered for. Note that this is independent of pixel resolution. For example, a phone and a desktop PC might render the game at similar pixel resolutions but the size they display content at will vary significantly.

‘large’ is used for devices such as desktop PCs where fine details can

be clearly seen. UI elements are generally smaller on the screen and more content can be seen at once.

‘medium’ is used for devices such as tablets, TVs, or VR headsets.

This mode strikes a balance between clean readability and amount of content visible.

‘small’ is used primarily for phones or other small devices where

content needs to be presented as large and clear in order to remain readable from an average distance.

LARGE = 2
MEDIUM = 1
SMALL = 0

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.packetzstddict module

Zstandard dictionaries for compressing scene game packets.

Warning

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

The game compresses each outgoing scene packet on its own (they average under a hundred bytes), which zstd only does well with a dictionary trained on what packets look like. The dictionary here is trained on captures of hosted games (test_game_run --packet-dump on a host and a client across several maps, mini-games and player counts; recipe in docs/design/packet-compression.md) and is handed to the C++ layer at app start.

Both ends of a connection must hold the identical dictionary, so dictionaries are immutable and versioned: never mutate an existing .zstddict file – add a new version instead – and bump the scene protocol version when the game switches to it (the protocol version is what tells a peer which dictionary is in play).

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.

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, legacy_spaz_physics: 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
legacy_spaz_physics: bool = False
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.parse_tag(body: str) → BriefTag[source]

Parse and validate one tag body (the text between its braces).

The single-tag unit of parse_brief(), for templates that use the same tag grammar outside a brief – the spec’d tokens ({t|duration}) a literal language-string template may carry. Raises CleanError exactly as parse_brief() would for that tag.

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.

Each value is the character itself, ready to drop into a string. These utilize the private-use block of unicode characters (U+E000-U+F8FF) and are specific to the game; exporting or rendering them elsewhere will be meaningless.

This is the one list of these characters; the engine’s own table is generated from it. It is also available as babase.SpecialChar and bauiv1.SpecialChar.

ANNIVERSARY_15_BOMB = '\ue06a'
ANNIVERSARY_15_CAKE = '\ue068'
ANNIVERSARY_15_SPAZ = '\ue069'
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'
POPUP_ICON = '\ue018'
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'
property min_engine_build: int[source]

The first engine build able to draw this character.

Older builds have no glyph for it and show a blank, so anything offering characters to a particular client (a store, an icon picker) should leave out ones newer than that client’s build.

When adding a character, give it its own case here returning the build number that first ships its glyph. Everything that predates this property returns 0 (we don’t know when those were added, and every client we talk to can draw them).

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.