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

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.

bacommontools.streamws module

WebSocket-based stream consumer for bacloud (Phase 2).

A stream-mode bacloud kickoff lands at a basn node, which injects a StreamWS into the response pointing at its own /streamcall/<call_id> WebSocket endpoint. We open that WS, print StreamOutput frames live as they arrive, and return the terminal StreamFinal so the caller can splice it back into bacloud’s existing response-handling flow.

On a non-terminal close (network blip, abnormal close, expired token) we reconnect — refreshing the token via POST /streamcall/<call_id>/refresh-token first if the close code says the token is expired (4001). Reconnects use exponential backoff up to a configurable wall-clock budget (default 60s, override via BACLOUD_RECONNECT_BUDGET_SECONDS); past the budget we surface CleanError. Token-bad / call-id-mismatch / no-token closes (4002/4003/4004) are fatal — no retry.

v0 reconnect doesn’t ask basn to replay the cursor: a reconnecting client may miss frames that landed during the disconnect window. In practice the stream still completes (the basn-side subscription keeps polling regardless of WS attachments), and bacloud renders the terminal StreamFinal correctly. Cursor-aware resume is Phase 3 territory.

Test-only env vars:

  • BACLOUD_TEST_FORCE_DROP_AFTER_SECONDS=N — force the WS closed N seconds after open; the reconnect path then runs as it would on a real drop.

  • BACLOUD_TEST_BREAK_RECONNECT=1 — point the reconnect URL at a guaranteed-unreachable host (127.0.0.1:1); reconnects fail until the budget expires.

bacommontools.streamws.consume_via_ws(response: ResponseData, *, bearer: str | None, host: str) ResponseData[source]

Drain a stream over WebSocket and return a terminal-only response.

The returned ResponseData carries the terminal StreamFinal in stream_frames so bacloud’s existing stream_frames loop falls through to the usual terminal handling (message/error/end_command).

host is the bacloud client’s resolved kickoff hostname (the basn the kickoff went to); used to construct the WS URL when the producer didn’t pin one.

Caller must check response.stream_ws is not None first. Raises CleanError on unrecoverable WS failure (token-bad, reconnect-budget exhausted, etc.).