bacommontools package

Tools functionality shared by all Ballistica components.

Submodules

bacommontools.bacloud module

A tool for interacting with ballistica’s cloud services. This facilitates workflows such as creating asset-packages, etc.

class bacommontools.bacloud.App[source]

Bases: object

Context for a run of the tool.

run() → int[source]

Run the tool.

run_interactive_command(cwd: str, args: list[str]) → None[source]

Run a single user command to completion.

class bacommontools.bacloud.StateData(login_token: str | None = None)[source]

Bases: object

Persistent state data stored to disk.

login_token: str | None = None
bacommontools.bacloud.get_tz_offset_seconds() → float[source]

Return the offset between utc and local time in seconds.

bacommontools.bacloud.run_bacloud_main() → None[source]

Do the thing.

bacommontools.bacloudsession module

bacloud’s live session to a basn node.

Warning

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

One SmartSocket session replaces the request-per-HTTPS-handshake conversation bacloud used to have. There is no mint step and no HTTPS request at all: the client dials /bacloudsession on the node it already resolved, and the handshake itself creates the channel. Every command after that rides the session – including each end_command continuation and every streamed command’s output.

What the session buys, measured rather than assumed (2026-08-15): streamed output pushed as produced instead of on a 0.25-5s poll cadence, one connection instead of two for a streamed command, one recovery implementation instead of several (reconnects are invisible here; deaths are not), and a far end that finds out when we quit. Notably not per-command latency – that measured the same as the request-per-connection path it replaced, because urllib3 pooling had already amortized the TLS handshake across a run.

Sequential by contract. Send a request, read until its response, repeat. No correlation ids and no multiplexing: bulk transfers were never on this channel – uploads and downloads go direct to storage on their own connections – so nothing here needs to overlap.

Threading. bacloud is synchronous and stays that way. The session runs an asyncio loop on its own thread and BacloudSession.request() is an ordinary blocking call, so nothing above it has to know a socket is involved.

Credentials and resume. Our bearer rides the WS upgrade’s Authorization header on every attach, exactly as it rode every HTTPS request before. The node answers a freshly created channel with a SessionHandleResponse carrying a resume token, which we hold and present as X-WS-Token if we ever have to reconnect. It needs no refreshing – it is minted to outlive the session itself.

That response also tells us where to reconnect, and we must use it rather than re-dialing the host we opened with. In prod that host is a regional endpoint which routes each connection to some node, and a session lives in exactly one node’s process – so re-dialing it would usually reach a node with nothing to resume. (This is the one place bacloud is node-bound; streamcall is not, because its state is bamaster’s.)

Canonical design: efrohome:docs/global_design/ streamcall-smartsocket.md (“Consumer #2”).

bacommontools.meshcompile module

Compilers for Ballistica’s binary mesh formats.

Covers display meshes (.bob) and collision meshes (.cob).

This module is intentionally stdlib-only and side-effect free so it can run anywhere it gets efrosynced to (game repo asset builds now, master-server cloud-build recipes later).

class bacommontools.meshcompile.BobCompileResult(corner_count: int, vertex_count: int, tri_count: int, index_size: int)[source]

Bases: object

Stats from a display-mesh compile.

corner_count: int
index_size: int
tri_count: int
vertex_count: int
property vertex_reuse: float

Verts per triangle; lower is better (0.5 = perfect grid reuse).

Values near 3.0 mean almost no corner sharing (hard edges / UV seams splitting most vertices) - an art/export property no index reordering can fix.

class bacommontools.meshcompile.BobData(mesh_format: int, vertices: list[tuple[float, float, float, int, int, int, int, int]], indices: list[int])[source]

Bases: object

Parsed contents of a .bob file.

indices: list[int]
mesh_format: int
vertices: list[tuple[float, float, float, int, int, int, int, int]]
class bacommontools.meshcompile.CobCompileResult(vertex_count_in: int, vertex_count_out: int, tri_count_in: int, tri_count_out: int)[source]

Bases: object

Stats from a collision-mesh compile.

tri_count_in: int
tri_count_out: int
property tris_dropped: int

How many degenerate triangles were dropped.

vertex_count_in: int
vertex_count_out: int
property vertices_welded: int

How many exact-duplicate vertices were merged away.

class bacommontools.meshcompile.CobData(file_id: int, positions: list[float], indices: list[int], normals: list[float] | None)[source]

Bases: object

Parsed contents of a .cob file.

file_id: int
indices: list[int]
normals: list[float] | None
positions: list[float]
bacommontools.meshcompile.compile_collision_mesh(src: str | Path, dst: str | Path) → CobCompileResult[source]

Compile a wavefront .obj file to a binary .cob file.

Reads a constrained subset of the obj format: v records and f records (v, v/t, v//n, and v/t/n corner forms are all accepted; texture-coordinate and normal references are ignored). Faces with more than 3 corners are fan-triangulated.

Output is deterministic for a given input, which matters for content-addressed asset storage.

Beyond straight conversion this applies a few optimizations:

  • Exact-duplicate vertex positions are welded (compared at float32 precision, matching what gets written).

  • Degenerate triangles (two or more corners sharing a vertex) are dropped.

  • Triangles are sorted along a Morton curve of their centroids and vertices are then ordered by first use, so triangles that are near each other in space are also near each other in memory. ODE/OPCODE reads these arrays in place during collision queries; spatially-local queries thus touch fewer cache lines. (Tree shape is unaffected; OPCODE splits on geometry, not input order.)

  • Unreferenced vertices are pruned (they would otherwise inflate both memory use and ODE’s model-space AABB).

bacommontools.meshcompile.compile_mesh(src: str | Path, dst: str | Path) → BobCompileResult[source]

Compile a wavefront .obj file to a binary .bob file.

Reads the obj subset our exporters produce: v/vt/vn records plus f records with full v/t/n corners. Faces with more than 3 corners are fan-triangulated. The obj V texture coordinate is flipped (1 - v) per GL convention. UVs and normal components meaningfully outside their encodable ranges ([0, 1] / [-1, 1]) are errors; values within a 0.05 tolerance are treated as authoring noise and clamped silently by the quantization.

Output is deterministic for a given input, which matters for content-addressed asset storage.

Optimizations applied:

  • Corners are quantized to the final vertex encoding and welded (exact-match on all attributes), so identical corners share one vertex.

  • Degenerate triangles (two or more corners welding to the same vertex) are dropped.

  • Triangle order is optimized for the GPU post-transform vertex cache (Forsyth’s linear-speed algorithm), then vertices are renumbered by first use for fetch locality. This also prunes unreferenced vertices.

  • Index width is chosen per mesh: u16 when vertices fit, u32 otherwise (the engine supports both; this removes the old make_bob 21845-face limit).

bacommontools.meshcompile.read_collision_mesh(path: str | Path) → CobData[source]

Read a binary .cob file (current or legacy format).

bacommontools.meshcompile.read_mesh(path: str | Path) → BobData[source]

Read a binary .bob file.

bacommontools.pcommands module

Pcommands for bacommontools.

bacommontools.pcommands.bacurl() → None[source]

Run curl with the Ballistica API key injected.

Usage: bacurl [curl-args…] <url>

Reads ballistica_api_key from pconfig/localconfig.json and passes it as a Bearer token in the Authorization header. All arguments are forwarded to curl. The -s (silent) flag is added automatically. HTTP errors exit non-zero (–fail-with-body) so piped JSON parsing fails loudly instead of KeyError-ing on error payloads; the error body still prints.

Examples:

bacurl https://dev.ballistica.net/api/v1/admin/stats/catalog
bacurl -X POST -H 'Content-Type: application/json' \
    -d '{"dry_run":true}' \
    https://dev.ballistica.net/api/v1/admin/stats/flush
bacommontools.pcommands.compile_collision_mesh() → None[source]

Compile a collision mesh from .obj to our binary .cob format.

Usage: compile_collision_mesh <src.obj> <dst.cob>

bacommontools.pcommands.compile_mesh() → None[source]

Compile a display mesh from .obj to our binary .bob format.

Usage: compile_mesh <src.obj> <dst.bob>

bacommontools.pcommands.require_ballistica_api_key() → None[source]

Verify a Ballistica API key is available; error if not.