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.StateData(login_token: str | None = None)[source]¶
Bases:
objectPersistent state data stored to disk.
bacommontools.bacloudsession module¶
bacloud’s live session to a basn node.
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”).
- class bacommontools.bacloudsession.BacloudSession(ws_url: str, bearer: str | None)[source]¶
Bases:
objectA live conversation with the bacloud server.
Constructed via
open(), which returnsNonerather than raising when a session can’t be had – not because that is survivable (it is not; the caller turns it into a hard error) but so the message the user sees is written where the surrounding context is, rather than here.- end() None[source]¶
Tell the far end we’re done, briefly and best-effort.
endrather thandetach: a process that is exiting is not coming back, and a relay that is told so releases the channel (and its node-side task) now instead of holding the slot through the whole linger window.We only ask;
_end_when_requesteddoes the saying, on the session thread. That split is load-bearing in two ways.It is the only place the goodbye can work at all: once
endpoint.run()has returned, the transport is already gone andendpoint.end()has nothing left to send – so a goodbye issued from out here was silently a no-op in exactly the case it was written for.And scheduling a coroutine in from out here is not merely useless but unsafe.
run_coroutine_threadsafecreates a task, and creating one whileasyncio.runis tearing the loop down races the C_asyncioaccelerator’s task bookkeeping and segfaults the interpreter. That window is not exotic: the node closes the channel when a command finishes, so a run whose last command just completed arrives here with the loop already unwinding. (Reported from a build 2026-08-19; the_endedflag could not guard it, since it is set only afterasyncio.runhas fully returned.)call_soon_threadsafeis the one loop method documented as thread-safe, creates no task, and raises rather than crashing if the loop is already closed.
- classmethod open(server: str, bearer: str | None) BacloudSession | None[source]¶
Open a session to
server, or return None.serveris the host bacloud already resolved – the same one its requests went to before – so this adds no lookup and no hop.
- request(request: StandardRequestData) StandardResponseData[source]¶
Send one request and block for its response.
Blocks without a deadline of its own on purpose: the session’s own liveness machinery is the authority on whether the far end is still there, and a second timeout here could only disagree with it. A dead session raises rather than hanging.
The one gap in that, stated so it isn’t rediscovered: liveness watches the leg, not the outstanding request. A response dropped on a connection that then stays healthy is only retransmitted by a resume hello, which never fires because the leg looks fine – so this would block forever. Real transports cannot produce that (TCP delivers or the connection breaks); only the relay’s chaos hook can, by silencing frames on a live connection. If that ever stops being true, the fix is a request-scoped deadline here, not a shorter liveness window.
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:
objectStats from a display-mesh compile.
- class bacommontools.meshcompile.BobData(mesh_format: int, vertices: list[tuple[float, float, float, int, int, int, int, int]], indices: list[int])[source]¶
Bases:
objectParsed contents of a
.bobfile.
- class bacommontools.meshcompile.CobCompileResult(vertex_count_in: int, vertex_count_out: int, tri_count_in: int, tri_count_out: int)[source]¶
Bases:
objectStats from a collision-mesh compile.
- class bacommontools.meshcompile.CobData(file_id: int, positions: list[float], indices: list[int], normals: list[float] | None)[source]¶
Bases:
objectParsed contents of a
.cobfile.
- bacommontools.meshcompile.compile_collision_mesh(src: str | Path, dst: str | Path) CobCompileResult[source]¶
Compile a wavefront
.objfile to a binary.cobfile.Reads a constrained subset of the obj format:
vrecords andfrecords (v,v/t,v//n, andv/t/ncorner 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
.objfile to a binary.bobfile.Reads the obj subset our exporters produce:
v/vt/vnrecords plusfrecords with fullv/t/ncorners. 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.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_keyfrompconfig/localconfig.jsonand 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>