schema/README.md at main · mas-bandwidth/schema · GitHub
//blob/show" data-turbo-transient="true" />
Skip to content
Type / to search
Sign in<br>Sign upAppearance settings
You signed in with another tab or window. Reload to refresh your session.<br>You signed out in another tab or window. Reload to refresh your session.<br>You switched accounts on another tab or window. Reload to refresh your session.
Dismiss alert
{{ message }}
Uh oh!
There was an error while loading. Please reload this page.
mas-bandwidth
schema
Public
Notifications<br>You must be signed in to change notification settings
Fork
Star
FilesExpand file tree
main
/README.md
Copy path
Blame<br>More file actions
Blame<br>More file actions
Latest commit
History<br>History<br>History
146 lines (120 loc) · 6.75 KB
main
/README.md
Copy path
Top
File metadata and controls<br>Preview
Code
Blame
146 lines (120 loc) · 6.75 KB
Raw<br>Copy raw file<br>Download raw file
OutlineEdit and raw actions
schema
Write down your data types once and generate code to read and write them in four languages automatically.
const MaxHealth = 1000
enum ShipType { Fighter, Corvette, Bomber }
message ShipState {<br>ship_type ShipType<br>position Vec3<br>orientation Quat<br>health int32 [min = 0, max = MaxHealth]<br>at_rest bool<br>if !at_rest {<br>linear_velocity Vec3<br>angular_velocity Vec3
This declaration compiles to C++, C#, Go and Rust code that<br>reads and writes your data types and agree on every bit. Now your native plugin, your Unity client<br>and your Go backend all speak the same language.
Why it exists
Multiplayer games serialize the same data in several languages at once — an<br>engine client here, a dedicated server there, tools and services around them.<br>Every way of solving that costs something:
Hand-written serializers drift. Someone widens a field on one side; the<br>other side keeps reading the old width, and an afternoon goes to a bug that<br>is one bit wide. Separate read and write paths drift too — even within a<br>single language, the reader and the writer are two expressions of one format<br>that nothing forces to agree.
A unified read/write function fixes the drift and costs you elsewhere.<br>Templating one function over a read stream and a write stream is a good C++<br>answer, and it is still slower than the hand-written pair — and it is not<br>available at all in Go, or in most of the languages a game actually has to<br>ship in.
Solving it with heavier template machinery costs compile time , in headers<br>every translation unit includes.
General-purpose formats fix the drift by paying for it on the wire, in<br>bytes and allocations you cannot afford at 60 Hz.
Generating the code takes the fourth path. One declaration produces the reader<br>and the writer, in every language, so they cannot disagree — and because the<br>format is decided at compile time, what comes out is the straight-line code you<br>would have hand-written, not an interpreter walking a schema at runtime.
The agreement is proven rather than promised: every build compiles the corpus<br>in all four languages and byte-compares the results against pinned goldens. If<br>two languages ever differ by one bit, CI says so before you do.
Features
One declaration, four languages — C++, C#, Go and Rust, bit-identical on<br>the wire, reader and writer generated together so they cannot drift.
Bit-packed, not byte-packed — [min = 0, max = 1000] costs 10 bits, not<br>4 bytes. Bounds are part of the type, and the wire cost follows from them.
Branches that cost nothing — if !at_rest { … } omits whole field groups<br>from the wire, back-referencing a bool already sent.
Compressed floats — [min, max, resolution] sends a step index, not a<br>float. A 0–1 throttle at 0.01 costs 7 bits.
Fixed point is a type in the language — fixed(48, 16) is declared like<br>any other field, and the compiler owns both the storage and the wire for it.<br>This is the one you cannot get by being careful with a format that lacks it:<br>floating point is not bit-identical across compilers and architectures, so<br>a simulation that must agree everywhere — lockstep, rollback, deterministic<br>replay, server/client prediction — cannot be built on floats. A fixed value<br>is an integer, and integers add the same on every machine. It is also its own<br>quantization: storage and wire share one integer domain, so there is no<br>separate quantize step to keep in sync.
128-bit integers , ranged like any other, in every target language.
Zero allocation, no runtime reflection — straight-line code reading and<br>writing your own buffers.
Reads validate, always — out-of-range values are refused, not clamped or<br>trusted. Generated readers are built to face hostile packets.
A second, evolution-tolerant wire for config, assets and settings, with<br>reflection and relocatable storage — see WIRES.md.
schema pack compiles directories of JSON into one binary container,<br>validating every value against the schema as it goes.
A canonical source format — every command formats in place, so the<br>protocol id hashes one true form.
Generated...