This is the abridged developer documentation for BlinkBlox # BlinkBlox > Describe your remotes in a schema. Get server and client modules that pack them into buffers, check everything a client sends, and hold up when a client is hostile. ## One schema, two modules [Section titled “One schema, two modules”](#one-schema-two-modules) You describe your events and functions, and what they carry, in a `.blink` file. The compiler turns that file into a server module and a client module. Both are plain Luau with no runtime dependency. Calls are batched into one buffer per frame, and each type is packed as tightly as its declaration allows. * Schema Net.blink ```blink option DefaultRate = 20 struct Hit { Target: Instance(Humanoid), Damage: u8(1..100), Critical: boolean, } event Damage { From: Client, Type: Reliable, Call: SingleSync, Rate: 10, Data: Hit } event Position { From: Server, Type: OrderedUnreliable, Call: SingleSync, Data: (Id: u16, Where: vector) } function GetInventory { Yield: Coroutine, Data: u32, Return: string(0..32)[..64] } ``` * Server Server.server.luau ```luau local Net = require(ReplicatedStorage.Blink.Server) -- The event is dispatched only after its rate limit has passed and every field has been checked: -- Damage is 1..100 and Target is a Humanoid. Net.Damage.On(function(Player, Hit) Combat.Apply(Player, Hit.Target, Hit.Damage, Hit.Critical) end) Net.GetInventory.On(function(Player, Page) return Inventory.List(Player, Page) end) RunService.Heartbeat:Connect(function() for Id, Unit in Units do Net.Position.FireAll(Id, Unit.Position) end end) ``` * Client Client.client.luau ```luau local Net = require(ReplicatedStorage.Blink.Client) Net.Damage.Fire({ Target = Humanoid, Damage = 25, Critical = false }) -- A packet that arrives after a newer one is dropped, so a unit never jumps back. Net.Position.On(function(Id, Where) Units[Id]:MoveTo(Where) end) local Items = Net.GetInventory.Invoke(1) ``` ## What it does for you [Section titled “What it does for you”](#what-it-does-for-you) Safe to point at the internet Packets are bounded before they are parsed. Each player gets a byte budget, and each event can get its own rate limit. Every length is checked before the read it pays for. One malformed event is dropped without taking the rest of the batch with it. [How the server protects itself](/BlinkBlox/guides/securing-the-server/). Small on the wire Booleans and optional flags share a bitfield, and `boolean[]` packs eight elements to a byte. A length is sent relative to its range, and a rotation fits in 7 bytes. Unreliable events that cannot fit are refused at compile time. [Bandwidth and sizes](/BlinkBlox/guides/bandwidth/). Checked at compile time A schema that cannot work never builds. That covers map keys that can never be looked up, cyclic imports, unbounded decode loops and debug remotes left in a release build. Each gets a diagnostic with a code and a source span. [Diagnostics](/BlinkBlox/reference/diagnostics/). Tooling included The CLI has watch mode and build profiles. A Studio plugin gives you an editor with live diagnostics and autocomplete, and TypeScript definitions are available for roblox-ts. [Studio plugin](/BlinkBlox/getting-started/studio-plugin/). ## Measured [Section titled “Measured”](#measured) A thousand events a frame from client to server, run in Studio on 0.29.0. The numbers are median frame rates, with bandwidth in brackets. | Payload | Roblox remotes | BlinkBlox | zap | ByteNet | | ------------- | -------------- | ------------------------- | ------------------- | ------------------- | | 1000 booleans | 15 FPS | **57 FPS** (3.19 Kbps) | 37 FPS (8.53 Kbps) | 22 FPS (8.33 Kbps) | | 100 entities | 16 FPS | **60 FPS**\* (41.57 Kbps) | 42 FPS (41.86 Kbps) | 24 FPS (41.71 Kbps) | \* Studio caps the frame rate at 60. [How these were measured](/BlinkBlox/guides/benchmarks/). ## Where it fits [Section titled “Where it fits”](#where-it-fits) BlinkBlox is a maintained fork of [Blink](https://github.com/1Axen/blink). Upstream froze this line of the compiler in 2026 and began a rewrite, and it declared an unbounded parse of a hostile client buffer out of scope. This fork fixes that and continues from there, without giving up the TypeScript output, the Studio plugin or the documentation. [Zap](https://github.com/red-blox/zap) is a neighbouring compiler that focuses on bandwidth. It leaves rate limiting to the game, and one malformed event discards the rest of that player’s batch. | | BlinkBlox | Blink 0.18 | Zap | | ------------------------------------------ | --------- | ---------- | --- | | Per-player, per-event rate limits | Yes | No | No | | A malformed event drops only itself | Yes | No | No | | Shared bitfield for booleans and optionals | Yes | No | Yes | | TypeScript output | Yes | Yes | Yes | [Install BlinkBlox](/BlinkBlox/getting-started/installation/)Rokit, pesde, release binaries or the Studio plugin. [Quick start](/BlinkBlox/getting-started/quick-start/)From an empty file to a working event in five minutes. [Coming from Blink?](/BlinkBlox/guides/migrating-from-blink/)What was kept, what changed, and what to recompile. [Changelog](/BlinkBlox/changelog/)Every release since the fork. # Changelog > Every release of BlinkBlox since the fork from upstream Blink v0.18.8. # Changelog Every release of BlinkBlox since it forked from upstream Blink at `v0.18.8`. The GitHub release notes tell the longer story of each one, with the measurements and the reports that led to it. A line marked **Recompile both modules** means a client and a server must be generated by the same release to talk to each other; the schema signature added in 0.23.0 makes a mismatch refuse at startup instead of misreading packets. ## 0.33.0 — 2026-09-24 A limit on the calls a server runs at once, and tests for the seams between events. **The wire format does not change.** ### Added * `Concurrency: N` on a function caps how many of one player’s calls the server runs at once. `Rate` limits how many calls *start* each second. A listener that waits (a DataStore save, an HTTP request, an invocation back to the client) holds each call open while it waits, so calls arriving at an allowed rate still piled up into suspended threads. A client sending its own requests is not bound by the 32 outstanding calls an honest client module keeps to. * A call past the limit is answered with a failure at once. * It is reported through `SetRateLimitHandler` and a warning, once a second per player. * The place is given back when the call is answered: returned, thrown, or failed to serialise. * A call queued for a listener that has not connected counts as running. * The counts go when the player leaves. * A fractional value is refused (`E2003`), and `Concurrency` on a `From: Server` function warns (`W3020`). ### Tests * `test/Composition.luau` fires random sequences of client events into one packet, using a schema of its own built on isolated remotes (`test/Isolated.luau`). It checks four things: * the listeners receive exactly the sequence that was fired; * the packet is the events’ standalone packets laid end to end; * a fire the sender refuses leaves the packet byte for byte unchanged; * a packet cut short delivers every event that ended before the cut, then reports once, naming the event it was cut inside. It catches the 0.24 phantom event and the 0.29 closed-up holes when either is put back. * `test/EventIndices.luau` sends every one of the 256 index bytes, alone, on both channels of every test schema’s server. A declared index must be taken as its declaration, and any other index must be reported exactly once as unknown. It catches the 0.26 channel that never refused when that bug is put back. * `test/Concurrency.luau` covers the limit, and checks that a failing call gives its place back. ## 0.32.0 — 2026-09-24 The generated modules pass their own `--!strict` line. **The wire format does not change.** ### Fixed * Every generated module declares `--!strict`, and none had been type-checked. The test schemas’ output carried about 190 strict errors, which a game’s editor showed in a file its owner cannot edit. They came from a few repeated lines: * a `pcall` of a writer that returns nothing, destructured into two locals; * a polled event’s iterator ending in a bare `return`; * an `Instance` compared with nil where its type does not allow nil; * an exported enum’s `Read` returning a value that `pcall` had widened to `string`. * An empty `FutureLibrary` or `PromiseLibrary` emitted `require()`, which fails to load. An empty path now counts as no path. * A Future or Promise library is required only by a module that invokes through it. The side that answers such a call never uses it, so the require was an unused import there. ### Language * A type named after a built-in Luau type (`number`, `string`, `boolean`, `buffer`, `thread`, `any`, `unknown`, `never`) is refused, `E3005`. It was exported as `export type number = number`, which Luau does not allow. * A top-level type named after a Roblox type the module uses is refused, `E3005`. That covers `Player`, `Instance`, `RemoteEvent`, `UnreliableRemoteEvent`, `CFrame`, `Vector3`, `Color3`, `DateTime`, `BrickColor`, and any class the schema names in `Instance(...)`. `type Player = Instance(Player)` exported a type that referred to itself and shadowed every `Player` parameter in the module. A type inside a `scope` is exported with the scope’s name in front, so it is not affected. ### Tests * The type gate analyses `test/Golden`, the output of every test schema, as a third contour. * `test/HalfFloats.luau` reads all 65536 f16 bit patterns and checks each against an IEEE decoder written independently. It also writes every value midway between two neighbours and checks it lands on one of them. Putting back the 0.29 carry bug or the 0.31 signed-zero bug makes it fail. ## 0.31.0 — 2026-09-24 Property-based tests for every serialiser, and what they found. **The wire format does not change**: modules from 0.27.0 onward still talk to each other. ### Fixed * **A 257th declaration on a channel was sent as the first.** Each channel numbers its declarations with one byte, and nothing refused the 257th, so the receiver decoded it as a different event. It is now a compile error, `E3030`. Imports count toward the limit, and declarations a profile leaves out do not. * `f16` lost the sign of zero: -0 arrived as +0. It is now written as `0x8000`, which an older reader still decodes as 0. * Under `WriteValidations`, a fixed-length array longer than its length was cut short on send instead of refused. Without `WriteValidations` it is still cut short, as an exact-length string is. * A function or event whose data is an empty type pack, `Data: ()`, read an undeclared global in its reader: nil at runtime, but a type error in the `--!strict` generated module. ### Tests * Every exported type of the test schema is drawn at random, 100 times, into two builds: one with `WriteValidations` on and one with the defaults. Draws lean on the edges: a range’s own bounds, lengths of 255 and 256, f16 subnormals, NaN and -0. * Each draw must be written into the size the compiler’s analysis allows, read back as itself, and re-written to the same bytes. The encoding must also be impossible to decode one byte short. * Corrupted bytes must either fail to decode, or decode to a value the schema admits and the sender would write. * A value the receiver would not accept, such as a length past its bound or a float that needs rounding, may be refused. What is written anyway must be read to its last byte, and under `WriteValidations` must arrive as what was sent. * The tests catch each of three earlier serialiser bugs when it is put back into the compiler: the 0.28.0 length prefix that wrapped, the 0.29.0 f16 carry, and the 0.29.0 holes in optional arrays. `BLINKBLOX_SEED` replays or varies the draws. ## 0.30.0 — 2026-09-24 Tooling for the programs that read the compiler’s output: editor tasks, CI steps and coding assistants. **The wire format does not change.** ### Added * `--check` runs the whole compile and writes nothing: no modules, no output directories, no prompt. The diagnostics and exit code are those of a real compile. It combines with `--watch`. * `--json` prints the result as one JSON document on standard output and nothing else: every diagnostic with its code, name, file, line, column and byte offset, its labels and its notes, plus the paths written. A failure that is not a diagnostic, such as a missing schema file, is reported in the document too. It cannot be combined with `--watch`. ### Fixed * A diagnostic named the schema by its bare file name, and one inside an import by the string the `import` wrote, relative to whichever file wrote it. Both now name the file by its path, normalised. * The lexer’s one diagnostic, an unexpected character, named the file `input.blink` whatever it was called. * A schema without `ServerOutput` or `ClientOutput` was reported with the compiler’s own file and line in front of the message. ### Documentation * The syntax highlighting knows `OrderedUnreliable`, `quat`, attributes such as `@profile`, and decimal numbers. * The promise of an MCP server is withdrawn. `--check --json` gives an assistant what such a server would have. ## 0.29.0 — 2026-09-24 A runtime audit, made possible once 0.28.0 had split the runtime into files small enough to read whole. **The wire format does not change.** ### Fixed * A refused or failed invocation reply desynchronised everything batched after it. The caller read the success flag inside the payload’s block, so the cursor had already moved past a payload that was never sent, and a pcall around the read hid the error. * An invocation is now identified by its id *and* its function. Ids go round the whole u8 before one is reused, and each call’s timer is cancelled when the call settles. Previously an earlier call’s timer could fail a later one, a late reply could resume the next call, and on the server a client could answer one function’s call with another function’s value type. * A failed server fire left its Instance in the player’s batch, shifting every instance after it. * A failed exported `Write` lost the queued batch. * A queued event lost every value after a trailing nil. * A listener that disconnected itself cost the next listener that event. * A `Sync` listener’s error was reported as the sender’s decode failure. * `OrderedUnreliable` accepted a stale packet across the wrap, starved a player left out of the server’s sends (the server now counts per player), and never freed what it kept. * An optional array’s holes closed up. * f16 NaN decoded as -65600, and a subnormal lost its carry. * A float range refused its own bounds once narrowed on receipt. * A type-pack element named `Length` shadowed the string writer’s local. * An enum with more than 256 values wrapped. ### Hardened * A polled event’s queue is capped on the server. * A client can no longer put a line in the output, or a call into the game’s decode handler, for every packet it sends. Both are rate-bounded. ### Language * Repeated flags, values and variants are refused. * The TypeScript tag is quoted. * A trailing comma is accepted in every list. ## 0.28.0 — 2026-09-24 The fork is renamed **BlinkBlox**. This changes what the tools print and what the release artifacts are called, not what a game depends on. The remotes, `_G._BLINK`, the plugin’s `Blink` output folder, `ServerStorage.BLINK_CONFIGURATION_FILES` and the `.blink` extension all keep their names, so builds from either side of the rename still talk to each other. ### Fixed * **A length prefix wrapped on send.** With a lower bound of 0 and `WriteValidations` off (the default), a 300-byte value in `string(0..64)` went out with a length of 44 and all 300 bytes behind it. The receiver then decoded the rest of the packet from the wrong offset. The same held for buffers, arrays, unbounded lengths and a map’s count. The upper bound is now checked on send, whatever the options say. * `Predict` on a reliable `Many` event dropped the event when no listener was bound, where the network would have queued it. * A field, flag or tag named after a Luau keyword produced a module that did not load. * The Studio plugin’s editor never showed a warning, because the same parse set it and cleared it. It also printed every warning to Output on every keystroke. * A map’s size diagnostic had nothing to underline. * The plugin’s file search treated the query as a Lua pattern. * Exact-bound errors read “to equal to”. ### Changed * Luau files are capped at 500 lines with no exceptions. The parser, the generator, the prefabs and the plugin editor were split to fit. ## 0.27.0 — 2026-09-24 Ideas taken from reading ByteNet-Max, Warp and satset. **Recompile both modules** (`WIRE_VERSION` 2). ### Added * **An inbound byte budget per player.** Set it with `option InboundBytesPerSecond` and `option InboundBurst`. Each server connection charges a per-player token bucket before it decodes anything. A packet costs its size, and never less than 128 bytes. The burst is a whole second, because after a hitch Roblox delivers the backlog at once, and a refused reliable packet takes every event in it along. Refusals go to the rate-limit handler with `Event` nil. * **`boolean[]` is packed eight to a byte.** * **`CFrame`** encodes a rotation in 7 bytes instead of 12. It is opt-in, because it is lossy. ### Fixed * Color3 wrapped HDR channels: 2.0 arrived as 254/255. Channels are now clamped. * The docs had described CFrame’s two components backwards since upstream. ### Performance * Each flush threw away the buffer it had just grown. Keeping it measured 35 to 52 percent faster on the flush path. ## 0.26.0 — 2026-09-22 **Recompile both modules.** The rate-limit handler now takes `(Player, Event, Refused)`. ### Fixed * A channel the server receives nothing on (every event `From: Server`, or no unreliable client event) never got the unknown-index guard. A client’s junk packet was read one byte at a time up to `MaxEventsPerPacket`, and nothing was reported. The channel now fails at the first byte, once, through `SetDecodeErrorHandler` with the event `nil`. * The rate-limit handler was spawned on every refusal. It now shares the warning’s schedule of once a second per player per event, and receives `Refused`, the number of refusals that call stands for. ## 0.25.0 — 2026-09-19 **Recompile both modules** if a schema has an open float range. ### Added * **`@profile("dev" | "debug" | "test" | "release")`** keeps a declaration out of every build that did not ask for it. The default is `release`. Choose a profile with `--profile` on the CLI, or with a `Profile` attribute in the Studio plugin. New codes: E3025, E3026, E3027, E3028. ### Fixed * An open side of a float range was filled with the exact-integer limit (2^24 for `f32`), so `f32(0..)` refused 2e7. Open sides are now unbounded. * A vector range has bounds of its own, `0..inf`. A negative lower bound on the magnitude is an error. * The vector magnitude check is now tested. Lune has no global `Vector3`, so no test had ever run it. * `lune run init -- file.blink` no longer treats `--` as the config path. ## 0.24.2 — 2026-09-18 A hardening patch. Generated output changes; the wire format and the schema signature do not. ### Hardened * The server’s queue for an event with no listener stops at 256 and drops the rest silently. An invocation past the cap is answered with a failure. * Ranges refuse NaN. `x < Min` passes NaN, so the checks are now written `not (x >= Min)`. ### Added * **`SetDecodeErrorHandler`** tells the game which player sent a packet that failed to decode, and which event the packet claimed to be (upstream #102). ### Fixed * Invoking a player who had already left fails immediately instead of waiting out the timeout. ## 0.24.1 — 2026-09-17 Toolchain only (lefthook 2.1.14). The compiler is unchanged. ## 0.24.0 — 2026-09-17 **No wire-format or signature change.** ### Fixed * A write that threw left its bytes behind: * a reliable fire delivered a phantom event; * an unreliable fire left its scratch buffer installed and dropped the pending reliable batch; * an invocation stranded its slot. Upstream has these open as #91 and #107. * A reply that could not be serialised answered nobody, and the caller waited out the full timeout. It now fails the call. * Two files importing each other recursed until the path outgrew the filesystem. This is now E3015, “Cyclic import”. * A type naming itself is reported as E3022, not as “Unknown reference”. * An unknown `Casing` is caught at parse time (E2003). * Component errors now have their own code (E3023), and so do duplicate options (E3024). * `Poll: true` warns (W3017). * The Studio plugin’s version had drifted five minor releases behind the compiler’s. * `--version` works, and `--quiet` silences the banner. ## 0.23.0 — 2026-09-11 The devforum release. **Recompile both modules.** ### Added * **A schema signature** on both modules. A client and a server built from different schemas refuse each other at startup instead of decoding one event as another. * **`From` on functions**, so the server can invoke a client. * **`option InvocationTimeout`** (10 seconds by default). Invocations had no timeout, and a lost call leaked a slot. * **Named type-pack elements**: `Data: (chefId: f64, dish: string)`. * A thread pool for Async dispatch, kept because it measured faster. ### Hardened * The client’s decode loop is guarded the way the server’s already was. * Map keys that can never be looked up (tables) are refused at compile time. ### Changed * `SyncValidation` says that it discards the rest of the packet, which it always did silently. * A second `.On` on a `Single` event warns, because the first listener’s disconnect stops working. ### Studio plugin * The editor no longer rebuilds one frame per line on every keystroke (39 ms at line 800, now constant). * It no longer paints the document twice. * It no longer crashes past 2000 lines. * It no longer deletes other contents of the output folder. * It remembers the output location. ## 0.22.0 — 2026-09-11 Compatible with 0.21.0 on the wire. ### Hardened * A decoded length is checked before the read and the allocation it authorises. * Every variable-length read is bounded by the bytes left in the packet. * Decode loops are bounded by what the packet paid for: * `unknown` checks the value it takes; * an element that costs nothing to decode is refused where no bound was written (E3021); * a count is checked before `table.create`. ## 0.21.0 — 2026-09-10 A wire-format release. **Recompile both modules.** ### Added * Booleans and optional flags share a bitfield. Seventeen booleans went from 17 bytes to 3. * Lengths are encoded relative to their minimum. `u8[300..400]` spends one byte on its length instead of two. * **`Type: OrderedUnreliable`** discards an unreliable packet that arrives after a newer one. ## 0.20.0 — 2026-09-10 A security release. ### Added * **Per-player, per-event rate limits.** Use `Rate` and `Burst` on each event, or `option DefaultRate` for all of them, and `option RequireRates` to make a missing rate an error. `SetRateLimitHandler` receives refusals. Nobody is kicked automatically. * **Unreliable size analysis.** An unreliable event that can never fit is refused (E3018). One that might not fit is warned about (W3019). A runtime check catches the rest. The limit is set with `option MaxUnreliableSize`, 900 by default. ### Hardened * Remote arguments are type-checked before they are read. * Decoding stops when the player leaves. * Both remotes are class-checked when the module is required. * Invocation slots are recycled from a 32-slot bitset. * Types carrying an `Instance` or `unknown` cannot be exported. ### Fixed * Numeric options never lexed. * Only the first seventeen `option` statements were read. * `Color3(1..2)` silently dropped its range, and `unknown?` parsed. * The diagnostics renderer crashed on a blank line. ### Upgrading * Schemas that used to compile may now be refused. * The concurrent invocation ceiling is 32, down from 256. ## 0.19.0 — 2026-09-10 The first release of the fork, continuing from upstream v0.18.8. ### Hardened * Inbound packets are bounded: * `MaxPacketSize` (8192), `MaxEventsPerPacket` (64) and `MaxInstancesPerPacket` (256); * an unknown event id stops the parse; * a truncated packet is contained. * A player who leaves mid-call no longer lingers in the replication map. ### Fixed * Instance classes containing a digit parse. * Edit-mode stubs keep the shape of the real API. * The CLI works unattended. * `TypesOutput` resolves against the output path. ### Added * **`option Predict`** delivers an event to local listeners without a remote. Full release notes, with the measurements behind each change, are on [GitHub Releases](https://github.com/XopoIII/BlinkBlox/releases). # Command line > Every flag of the blinkblox compiler, how it finds your schema, what it prints, and how watch mode behaves. ```sh blinkblox [CONFIG] [OPTIONS] ``` `CONFIG` is the schema to compile. The compiler reads it, follows its imports, and writes the server and client modules to the paths the schema names in [`ServerOutput` and `ClientOutput`](/BlinkBlox/language/options/#serveroutput-clientoutput-typesoutput). Both options are required. ## Compiling [Section titled “Compiling”](#compiling) ```sh blinkblox net ``` The schema’s path is looked up in this order, and the first file that exists is used: 1. the path exactly as given – `net` 2. with `.txt` appended – `net.txt` 3. with `.blink` appended – `net.blink` So `blinkblox net` and `blinkblox net.blink` compile the same file. A path into another directory works the same way: `blinkblox schemas/net`. **Output paths are relative to the schema, not to where you run the command.** `option ServerOutput = "src/server/Net.luau"` in `schemas/net.blink` writes `schemas/src/server/Net.luau`. Absolute paths are used as they are. Whatever extension the path has, the module is written as `.luau`: `"out/Server.lua"` and `"out/Server"` both write `out/Server.luau`. What gets written: * the server module and the client module, always; * a shared types module, when [`TypesOutput`](/BlinkBlox/language/options/#serveroutput-clientoutput-typesoutput) is set; * a `.d.ts` declaration next to each module, when [`Typescript`](/BlinkBlox/language/options/#typescript) is set. A module named `init` gets `index.d.ts`. On success it prints its progress and stops: ```text BlinkBlox 0.33.0 Reading source from net.blink... Parsing source into AST... Generating output files... Network files generated! ``` ### A missing output directory [Section titled “A missing output directory”](#a-missing-output-directory) If an output path points into a directory that does not exist, the compiler asks whether to create it. Answering no stops the compile. Two cases skip the question and create the directory: * `--yes` is passed. * **No terminal is attached** – the compiler runs from a script, a git hook, a build task or CI. There is nobody to answer, and naming the path in the schema is taken as the answer. A build therefore never hangs waiting on a prompt nobody can see, with or without `--yes`. ## Flags [Section titled “Flags”](#flags) Flags may come before or after the schema, in any order. The schema is the first argument that is not a flag – `blinkblox --yes net` and `blinkblox net --yes` are the same command. | Flag | What it does | | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `-h`, `--help` | Prints the usage and the list of flags. Running `blinkblox` with no schema does the same. | | `-v`, `--version` | Prints `BlinkBlox` and the version, then exits without compiling. | | `-w`, `--watch` | Compiles, then keeps watching the schema and everything it imports, recompiling on every change. See [watch mode](#watch-mode). | | `-q`, `--quiet` | Prints nothing on success: no banner, no progress. Errors and warnings are still printed, `--version` still prints the version, and watch mode still says what it is watching. | | `-c`, `--compact` | Prints each diagnostic as a single line instead of the full rendering. See [diagnostics](#diagnostics). | | `-y`, `--yes` | Accepts every prompt. The only prompt is [creating a missing output directory](#a-missing-output-directory). | | `--check` | Parses and generates everything, and writes nothing – no modules, no output directories. Reports the same diagnostics and exits with the same code as a real compile. See [checking without writing](#checking-without-writing). | | `--json` | Prints the result as one JSON document on standard output instead of rendering diagnostics, and prints nothing else. See [JSON output](#json-output). | | `-p`, `--profile` | Compiles under a [profile](/BlinkBlox/language/profiles/): `dev`, `debug`, `test` or `release`. Takes a value: `--profile dev`. Without it the build is `release`. | Flags are matched whole, so short flags do not combine: write `-q -y`, not `-qy`. An argument the compiler does not recognise as a flag is taken for the schema’s path, so a mistyped flag in front of the schema shows up as a missing file named after it. `--profile` is the only flag that takes a value, and its value is never mistaken for the schema: `blinkblox --profile dev net` compiles `net` under `dev`. A missing or misspelt profile stops the compiler before it reads anything: ```text Expected a profile after "--profile": dev, debug, test or release ``` ## Diagnostics [Section titled “Diagnostics”](#diagnostics) When a schema is wrong the compiler prints a diagnostic, writes nothing, and exits. Each diagnostic has a code, a message, the offending line with the span underlined, and often a note on how to fix it. For this schema: net.blink ```blink option ServerOutput = "src/server/Net.luau" option ClientOutput = "src/shared/Net.luau" option RequireRates = true event Chat { From: Client, Type: Reliable, Call: SingleSync, Data: string(1..200) } ``` the compiler prints: ```text BlinkBlox 0.33.0 Reading source from net.blink... Parsing source into AST... [E3020] Error: Inbound "Chat" has no rate limit ╭─[net.blink:1:11] │ 005 │ event Chat { ┆ ──┬── ┆ │ ┆ ╰── Add a Rate, in events per second │ = note: option RequireRates is set, so every inbound event and function must declare one. │ ────╯ ``` The file is named by the path it was compiled from, normalised: `blinkblox schemas/net` reports `schemas/net.blink`, and an error inside an import names the imported file’s own path, not the string the `import` wrote. The bracket after the file name is the range of lines in the file – line 1 to line 11 here – not the position of the error. The line number on the left of the source is the one to look at. With `--compact`, the same diagnostic is one line, in the form `[code] [Lfirst:Llast] [file] Error: message`: ```text [E3020] [L005:L005] [net.blink] Error: Inbound "Chat" has no rate limit ``` It carries no column, labels or notes. A program that needs those should use [`--json`](#json-output) instead. Warnings (codes starting with `W`) use the same layout in yellow. They do not stop the compile: the modules are written, and the warning is printed alongside. Diagnostics go to standard error; the banner and progress lines go to standard output. Every code, and what to do about it, is listed in the [diagnostics reference](/BlinkBlox/reference/diagnostics/). ## Exit codes [Section titled “Exit codes”](#exit-codes) | Code | When | | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `0` | The modules were written (or, with `--check`, would have been), or `--help` or `--version` was asked for. Warnings do not change it. | | `1` | Anything else: a diagnostic error in the schema, an unknown `--profile`, a schema file that does not exist, a schema without `ServerOutput` or `ClientOutput`, a refused directory prompt, or `--json` combined with `--watch`. | Only diagnostics are rendered as above. A missing schema file or a refused prompt is reported as a plain error message followed by a stack trace. Under [`--json`](#json-output) every one of these is reported in the document instead. ## Checking without writing [Section titled “Checking without writing”](#checking-without-writing) ```sh blinkblox net --check ``` `--check` runs the whole compile – the parse, every analysis, and the generator – and stops short of the disk: no module is written and no output directory is created, so it never prompts. The diagnostics and the exit code are the ones a real compile would give, and on success it ends with `Schema checked, nothing written.` instead of `Network files generated!`. Use it wherever the modules on disk should not change: a pre-commit hook, a CI step that only has to say whether the schema is valid, or an editor task run on save. It combines with `--watch`, which then reports each change without writing anything. ## JSON output [Section titled “JSON output”](#json-output) ```sh blinkblox net --check --json ``` `--json` prints one JSON document on standard output and nothing else: no banner, no progress, and no rendered diagnostics on standard error. It is meant for programs – an editor task, a CI step, a coding assistant – that need to know exactly where a diagnostic is without parsing the rendered form. The exit code is unchanged: `1` when `success` is `false`, `0` otherwise. For the schema in [Diagnostics](#diagnostics) above, it prints (formatted here; the real output is one line): ```json { "version": 1, "success": false, "diagnostics": [ { "severity": "error", "code": "E3020", "name": "AnalyzeMissingRateLimit", "message": "Inbound \"Chat\" has no rate limit", "file": "net.blink", "range": { "start": { "line": 5, "column": 7, "offset": 122 }, "end": { "line": 5, "column": 11, "offset": 126 } }, "labels": [ { "primary": true, "message": "Add a Rate, in events per second", "range": { "start": { "line": 5, "column": 7, "offset": 122 }, "end": { "line": 5, "column": 11, "offset": 126 } } } ], "notes": [ "option RequireRates is set, so every inbound event and function must declare one." ] } ], "outputs": [] } ``` | Field | Meaning | | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `version` | The format’s version, `1`. It changes when a field is removed or changes meaning; a new field does not change it. | | `success` | `false` if the compile failed, for any reason. Warnings do not make it `false`. | | `diagnostics` | Every error and warning, in the order they were raised. A compile stops at its first error, so there is at most one error, after any warnings. | | `outputs` | The paths written, normalised. Empty under `--check`, and empty when the compile failed. | Each diagnostic has: | Field | Meaning | | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `severity` | `"error"` or `"warning"`. | | `code` | The code as the rendered form prints it – `"E3020"`, `"W3019"` – listed in the [diagnostics reference](/BlinkBlox/reference/diagnostics/). `null` for a failure that is not a diagnostic. | | `name` | The code’s name in the compiler, such as `"AnalyzeMissingRateLimit"`. `null` when `code` is. | | `message` | The headline, without colour codes. | | `file` | The file it is in, named as in the [rendered form](#diagnostics). | | `range` | Where the primary label points, or `null` when there is none. | | `labels` | Every underlined span: the primary one and any secondary ones, each with its `message` and `range`. | | `notes` | The `= note:` lines, which say what to do about it. | A range’s `start` is inclusive and its `end` exclusive. `line` and `column` count from 1, as the rendered form’s line numbers do, and `offset` is the byte offset into the file, counting from 0. A column counts bytes, so a tab is one column. A failure that is not a diagnostic – a schema file that does not exist, a schema without `ServerOutput` – is reported the same way, with `code`, `name` and `range` set to `null` and the error in `message`. Standard output is a JSON document on every run, including the ones that fail before the schema is read. `--json` cannot be combined with `--watch`: watch mode never finishes, so its document would never be complete. The compiler refuses the pair and exits with `1`. ## Watch mode [Section titled “Watch mode”](#watch-mode) ```sh blinkblox net --watch ``` Watch mode compiles the schema once, then keeps running and recompiles whenever the schema or any file it imports changes – imports of imports included. It prints what it is watching when it starts, and again whenever an edit changes how many imports there are: ```text BlinkBlox 0.33.0 BlinkBlox is watching for changes: Entry: net.blink Imports: 2 ``` While it runs: * **It checks for changes once a second**, by comparing each file’s modification time. Saving a file with no change still counts as a change. * **A successful compile prints nothing.** Silence means the modules on disk are current. * **A failed compile prints its diagnostic and keeps watching.** Fix the schema and save; the next compile replaces the error with silence. The modules from the last good compile stay on disk until then. * **Missing output directories are created without asking**, as if `--yes` were passed. * **`--profile` applies to every recompile.** `--compact` does not; watch mode always renders diagnostics in full. * It runs until you stop it with Ctrl+C. An import of a missing file stops recompiles silently If you save an `import` of a file that does not exist yet, watch mode stops recompiling and prints nothing about it. Recompiles resume once the file exists. If saves stop having an effect, check your imports, or run the compiler once without `--watch` to see the error. At startup the same thing is reported instead: watch mode prints `There was an error while trying to start the watcher thread`, names the missing file, and exits – with code `0`, so a script cannot tell it from a clean stop. Watch mode finds imports by reading `import "..."` lines as text. An import that is commented out, or marked with a [profile](/BlinkBlox/language/profiles/) the build leaves out, is still watched. That costs nothing, but a missing file named there has the effect described above. # Installation > Install the BlinkBlox compiler with Rokit, pesde or a release archive, and the Studio plugin from the Creator Store. BlinkBlox comes in two parts. The **compiler** is a command-line program, `blinkblox`, that turns a `.blink` schema into Luau modules on disk. The **Studio plugin**, BlinkBlox Editor, does the same inside Roblox Studio with no external tooling. Both accept the same schema language and produce the same modules, so pick whichever fits how you work. You can use both. ## The compiler [Section titled “The compiler”](#the-compiler) * Rokit [Rokit](https://github.com/rojo-rbx/rokit) is the recommended installer. It pins the version in your project’s `rokit.toml`, so everyone who clones the project gets the same compiler. From your project directory: ```sh rokit add XopoIII/BlinkBlox blinkblox ``` This downloads the build for your platform and records it in `rokit.toml`. `blinkblox` is now on your `PATH` inside the project. To pin an exact version, name it: ```sh rokit add XopoIII/BlinkBlox@0.33.0 blinkblox ``` To move to the newest release later: ```sh rokit update XopoIII/BlinkBlox ``` Add `--global` to `rokit add` to install it for every project on the machine instead. * pesde The compiler is published to the [pesde](https://pesde.dev) registry as `xopoiii/blinkblox`. It is a binary package for the `lune` environment: pesde runs the compiler’s bundled Luau source under [Lune](https://lune-org.github.io/docs) rather than downloading a native executable. In a project with a `pesde.toml`, add it as a dev dependency and install: ```sh pesde add xopoiii/blinkblox --dev --target lune pesde install ``` pesde exposes a package’s binary under its alias, so the command is `blinkblox`, exactly as with the other installers. Not the upstream package `1axen/blink` on the same registry is upstream Blink, the project this one forked from at 0.18.8. It is a different compiler with a different runtime; see [Migrating from Blink](/BlinkBlox/guides/migrating-from-blink/) before swapping one for the other. * GitHub Releases Every [release](https://github.com/XopoIII/BlinkBlox/releases) carries a prebuilt executable for each supported platform, packed in an archive with the executable alone inside: | Platform | Archive | | -------------------- | --------------------------------- | | Windows, x86-64 | `blinkblox-windows-x86_64.tar.gz` | | macOS, Apple silicon | `blinkblox-macos-aarch64.tar.xz` | | macOS, Intel | `blinkblox-macos-x86_64.tar.xz` | | Linux, x86-64 | `blinkblox-linux-x86_64.tar.xz` | | Linux, ARM64 | `blinkblox-linux-aarch64.tar.xz` | Unpack it and put the executable (`blinkblox`, or `blinkblox.exe` on Windows) somewhere on your `PATH`: ```sh tar -xJf blinkblox-linux-x86_64.tar.xz ``` Windows 10 and later ship `tar`, so `tar -xzf blinkblox-windows-x86_64.tar.gz` works there too. Check that it runs: ```sh blinkblox --version ``` It prints `BlinkBlox` followed by the version number. The [command-line page](/BlinkBlox/getting-started/cli/) covers every flag. ## The Studio plugin [Section titled “The Studio plugin”](#the-studio-plugin) The plugin, **BlinkBlox Editor**, gives you an editor with syntax highlighting, autocomplete and live diagnostics, and generates the modules straight into your place. * Creator Store Install it from the [Creator Store](https://create.roblox.com/store/asset/132820603641668/BlinkBlox-Editor). Studio keeps it up to date from there. * GitHub Releases Every release also carries the plugin as `blinkblox-plugin.rbxm`. Download it and put it in Studio’s local plugins folder – in Studio, **Plugins > Plugins Folder** opens it – then restart Studio. A plugin installed this way does not update itself; replace the file on each release. The plugin and the compiler are released together from the same source and stamped with the same version. Keep them on the same release: the generated modules carry that version, and it is the first thing to compare when two builds disagree. [Quick start](/BlinkBlox/getting-started/quick-start/)Write a schema, compile it, and fire your first event. # Quick start > Write a schema with an event each way and a function, compile it, and call the generated modules from a server and a client script. This walk-through builds a small chat: the client asks the server to post a message, the server tells every client about it, and the client can ask the server how many coins it has. It uses the command-line compiler; in the [Studio plugin](/BlinkBlox/getting-started/studio-plugin/) the schema is the same and generating is a button. It assumes a Rojo-style project where `src/server` syncs to `ServerScriptService`, `src/client` to `StarterPlayerScripts` and `src/shared` to `ReplicatedStorage.Shared`. Adjust the paths to your own layout. 1. **Write the schema.** Create `net.blink` at the root of your project: net.blink ```blink -- Where the generated modules go, relative to this file. option ServerOutput = "src/server/Net.luau" option ClientOutput = "src/shared/Net.luau" struct Message { Author: string(1..20), Text: string(1..200), } -- The server tells clients that someone spoke. event MessagePosted { From: Server, Type: Reliable, Call: SingleSync, Data: Message } -- A client asks to say something: two a second, up to five at once. event SendMessage { From: Client, Type: Reliable, Call: SingleSync, Rate: 2, Burst: 5, Data: string(1..200) } -- A client asks how many coins it has, at most once a second. function GetCoins { Yield: Coroutine, Rate: 1, Return: u32 } ``` Each declaration says who sends it (`From`), how (`Type`, `Call`) and what it carries (`Data`). The ranges are part of the contract: `string(1..200)` means the server refuses an empty string or one longer than 200 bytes *before* your code sees it. `Rate` and `Burst` put a per-player token bucket in front of `SendMessage` and `GetCoins`, so a client that spams them is refused rather than served. [Events](/BlinkBlox/language/events/) and [functions](/BlinkBlox/language/functions/) explain every field. 2. **Compile it.** From the same directory: ```sh blinkblox net ``` ```text BlinkBlox 0.33.0 Reading source from net.blink... Parsing source into AST... Generating output files... Network files generated! ``` You now have `src/server/Net.luau` and `src/shared/Net.luau`. Both are plain Luau with no dependencies. Do not edit them by hand; change the schema and compile again, or run `blinkblox net --watch` to recompile on every save. If the schema has a mistake, the compiler points at it and writes nothing. The [command-line page](/BlinkBlox/getting-started/cli/#diagnostics) shows what that looks like. 3. **Use it on the server.** Requiring the server module creates the remotes, so require it early – from a script that runs when the server starts, not lazily from the first handler that needs it. src/server/Chat.server.luau ```luau local Net = require(script.Parent.Net) local Coins: { [Player]: number } = {} -- Text has already been checked against string(1..200) and against the rate limit. Net.SendMessage.On(function(Player: Player, Text: string) Net.MessagePosted.FireAll({ Author = Player.Name, Text = Text, }) end) -- Whatever the listener returns is the caller's answer. Net.GetCoins.On(function(Player: Player) return Coins[Player] or 0 end) ``` The server fires to one player with `Fire(Player, ...)`, to everyone with `FireAll(...)`, to a list with `FireList(Players, ...)` and to everyone but one with `FireExcept(Player, ...)`. Its listeners always receive the sending `Player` first. 4. **Use it on the client.** src/client/Chat.client.luau ```luau local ReplicatedStorage = game:GetService("ReplicatedStorage") local Net = require(ReplicatedStorage.Shared.Net) Net.MessagePosted.On(function(Message) print(`{Message.Author}: {Message.Text}`) end) Net.SendMessage.Fire("Hello!") -- Invoke yields until the server answers, and raises if the call fails. local Ok, Coins = pcall(Net.GetCoins.Invoke) if Ok then print(`I have {Coins} coins`) end ``` `Invoke` raises when the call cannot be answered: the server refused it under the rate limit, its listener threw, or no answer came within the [invocation timeout](/BlinkBlox/language/options/#invocationtimeout). Wrap it in `pcall` wherever a failure is possible, which on a network is everywhere. ## What you just got [Section titled “What you just got”](#what-you-just-got) * **Batching.** Calls made during a frame are written into buffers and sent together once per frame, on `Heartbeat`, instead of one remote call each. * **Types end to end.** Both modules export Luau types for your declarations (`Net.Message` here), and every `Fire`, `On` and `Invoke` is typed from the schema. * **A server that checks its input.** Before `SendMessage`’s listener runs, the server has checked the packet’s size, the string’s length and the player’s rate. A client that sends garbage loses its own packet and nobody else’s. * **A shared signature.** The two modules carry a signature of the schema they were built from. A client module built from a different schema than the server’s raises when it is required, instead of decoding one event as another – so always ship both modules from the same compile. `On` for an event returns a function that disconnects the listener. `SendMessage` is `SingleSync`, which keeps one listener; a second `On` replaces the first and warns. Use `ManySync` or `ManyAsync` when several scripts need to listen. [The schema language](/BlinkBlox/language/types/)Types, events, functions, scopes and imports, one page each. [Securing the server](/BlinkBlox/guides/securing-the-server/)Rate limits, inbound budgets and what to do about a client that misbehaves. # Studio plugin > Write, check and generate BlinkBlox schemas inside Roblox Studio with the BlinkBlox Editor plugin. The Studio plugin, **BlinkBlox Editor**, keeps your schemas inside the place, edits them with highlighting, autocomplete and live diagnostics, and generates the modules straight into the Explorer. It runs the same parser and generator as the [command line](/BlinkBlox/getting-started/cli/), from the same release, so a schema produces the same modules either way. [Installation](/BlinkBlox/getting-started/installation/#the-studio-plugin) covers getting it. The pictures on this page are drawn from the plugin’s own interface files – the layout, colours and text are the plugin’s, and the code in them is coloured by the plugin’s own highlighter. ## Opening the plugin [Section titled “Opening the plugin”](#opening-the-plugin) The plugin adds a toolbar named **BlinkBlox Suite** to the **Plugins** tab, with one button, **Editor**. Clicking it opens the editor window, docked on the left and titled *Configuration Editor*; clicking it again closes the window. The button stays highlighted while the window is open. HomeModelTestViewPlugins Editor BlinkBlox Suite ### The template [Section titled “The template”](#the-template) The first time you open the editor in a place, the plugin creates the folder it keeps schemas in and saves a file named `Template` into it, then opens that file: Configuration Editor 1 2 3 4 5 6 7 type Example = u8 event MyEvent { From: Server, Type: Reliable, Call: SingleSync, Data: Example } This happens only in a place that has never had the folder. Delete every file and the folder stays, empty: the plugin takes that as your choice and does not bring the template back. ## Where schemas are stored [Section titled “Where schemas are stored”](#where-schemas-are-stored) Each schema is a `StringValue` in `ServerStorage.BLINK_CONFIGURATION_FILES`: its `Name` is the file name and its `Value` is the source. The files are saved with the place, travel with it through Team Create and version history, and never reach a client, because nothing in `ServerStorage` replicates. You can manage them in the Explorer like any other instance – rename one, duplicate it, copy it into another place. The side menu reads the folder when it first opens and again after each save or delete, so a file changed in the Explorer shows up there after the next one. The folder keeps the name it had before the plugin was renamed to BlinkBlox, so places set up by older versions keep working. ## The side menu [Section titled “The side menu”](#the-side-menu) The button at the top of the strip on the left opens the side menu over the editor. It holds every saved file, in name order, each with three buttons: **Delete**, **Edit** and **Generate**. Clicking the menu button again closes it. * Files Configuration Editor 1 2 3 4 5 6 7 type Example = u8 event MyEvent { From: Server, Type: Reliable, Call: SingleSync, Data: Example } Search Combat Inventory Template Save * Saving Configuration Editor 1 2 3 4 5 6 7 type Example = u8 event MyEvent { From: Server, Type: Reliable, Call: SingleSync, Data: Example } Search Combat Inventory Template Combat CancelSave * Generating Configuration Editor 1 2 3 4 5 6 7 type Example = u8 event MyEvent { From: Server, Type: Reliable, Call: SingleSync, Data: Example } Search Combat Inventory Template **Selected**\ ReplicatedStorage CancelGenerate Save - **Edit** opens the file in the editor and closes the menu. - **Delete** removes the file at once. There is no confirmation. - **Generate** opens the generate prompt – see [Generating](#generating). ### Searching [Section titled “Searching”](#searching) Type into the search box and press `Enter` to show only the files whose names contain what you typed. The match is plain, case-sensitive text: since 0.28.0, `(`, `[`, `%`, `.` and `-` mean themselves rather than being read as a Lua pattern. Clicking the box clears it; press `Enter` on an empty box to show every file again. Leaving the box without pressing `Enter` changes nothing. ### Saving [Section titled “Saving”](#saving) **Save** at the bottom of the menu saves what is in the editor. The first click opens a **Name** box, filled in with the name of the file you are editing, and turns **Save** green; the second click saves under that name. A name that is already taken replaces that file’s contents, which is how you save changes to an existing file. **Cancel** closes the prompt without saving. Caution Clicking into the **Name** box clears it. To save under the name already filled in, press **Save** again without clicking the box. Saving under a new name does not switch the editor to the new file: the editor still counts as editing the file you opened, and [quick save](#quick-save-and-clear) keeps writing to that one. Open the new file with **Edit** to carry on in it. ### Quick save and clear [Section titled “Quick save and clear”](#quick-save-and-clear) The two buttons in the editor’s bottom-right corner work without opening the menu. * **Save** (the lower one) saves the editor’s contents over the file you are editing, without asking. If you are not editing a saved file – the text was imported, say – it opens the menu with the save prompt instead. * **Clear** (the upper one) empties the editor. It does not touch the saved file. Both do nothing while the side menu is open. Caution After **Clear**, the editor still counts as editing the same file. Quick-saving then writes the empty text over that file. If you cleared by mistake, reopen the file with **Edit** instead of saving. ### Importing [Section titled “Importing”](#importing) The button to the left of the search box imports a schema from your computer. Studio opens a file picker for `.blink` and `.txt` files, and the one you choose is loaded into the editor. It is not saved and not attached to any file: save it from the menu to keep it in the place. ## Generating [Section titled “Generating”](#generating) **Generate** on a file opens the generate prompt at the bottom of the menu. Select the destination in the Explorer – the prompt shows its full name under **Selected** – and press **Generate**. Until something is selected, the prompt asks you to select a location and **Generate** is dimmed and does nothing. Caution Generate into somewhere both the server and the clients can reach, such as `ReplicatedStorage`. Generated into `ServerStorage` or `ServerScriptService`, the client module cannot be required. The plugin remembers the last destination between sessions. When the prompt opens with nothing selected, that destination is selected for you; selecting something else in the Explorer overrides it. The destination is remembered by its full name, so after you rename or move it the plugin no longer finds it and you select it again – rather than generating into the old place. ### Script injection [Section titled “Script injection”](#script-injection) Creating scripts is a permission Studio grants plugins separately. The first time you generate, Studio asks whether to allow **BlinkBlox Editor** to inject scripts; allow it. If it is refused, the plugin opens its error window with *File generation failed, plugin doesn’t have script inject permissions.* and writes nothing. You can grant it later from **Plugins > Manage Plugins**. ### The output [Section titled “The output”](#the-output) The plugin writes three `ModuleScript`s into a folder named `Blink` under the destination, creating the folder if it is not there: Explorer ReplicatedStorage Blink Client Server Types Wrapperyours, left alone ServerStorage BLINK\_CONFIGURATION\_FILES Combat Inventory Template * `Server` is required from server scripts, `Client` from `LocalScript`s: `local Net = require(ReplicatedStorage.Blink.Server)`. * `Types` holds the schema’s exported types, for code that wants to name them. The folder keeps the name `Blink` from before the rename, so code that requires through it keeps working. Generating again **updates the three modules in place**. They stay the same instances, so an open script tab, a breakpoint, or an `ObjectValue` pointing at one survives regeneration. **Nothing else in the folder is touched**: a wrapper module or configuration script you keep beside the generated ones stays. Before 0.23.0 the plugin emptied the folder on every generation. The one exception is an instance of another class sitting on one of the three names – a `Folder` named `Server`, say – which is replaced, with a warning in the Output window saying so. The modules are written through `ScriptEditorService`, so a large schema whose output passes the 200,000 characters a script’s `Source` property accepts still generates. ### Profiles [Section titled “Profiles”](#profiles) A schema that uses [`@profile`](/BlinkBlox/language/profiles/) is generated as `release` unless told otherwise. To generate another profile, select the file in `ServerStorage.BLINK_CONFIGURATION_FILES` and add a **string** attribute named `Profile` in the Properties window, set to `dev`, `debug`, `test` or `release`. Any other value, or an attribute of another type, stops generation with an error naming it – *Unknown profile “beta” on Combat: expected dev, debug, test or release.* The attribute belongs to the file, so each schema keeps its own profile. ### Imports [Section titled “Imports”](#imports) An `import` in the plugin names another saved file rather than a path on disk: `import "Shared"` reads the file saved as `Shared`. See [Imports in the Studio plugin](/BlinkBlox/language/imports/#in-the-studio-plugin). ### Errors [Section titled “Errors”](#errors) If the file does not compile, nothing is written and a floating **Error** window opens with the compiler’s diagnostic – the same one the command line prints, with a plainer gutter. For the schema in [Live diagnostics](#live-diagnostics) below, with `Hit` misspelt as `Hitt` on line 14: Error ``` [E3007] Error: Unknown reference ╭-[input.blink:1:15] | 014 | Data: Hitt ----- | ╰-- Unknown reference | ----╯ ``` A schema always reports itself as `input.blink` here, since the plugin compiles text rather than a file on disk. Warnings do not stop generation; they are printed to the Output window. [Diagnostics](/BlinkBlox/reference/diagnostics/) lists every code. ## The editor [Section titled “The editor”](#the-editor) ### Highlighting [Section titled “Highlighting”](#highlighting) The editor colours the schema as you type: keywords in blue, primitives in teal, numbers and `Instance` classes in pale green, names you reference in light blue. A field name is left white, which is how `Hit:` reads differently from a reference to `Hit`. ### Autocomplete [Section titled “Autocomplete”](#autocomplete) A popup offers completions as you type: * At the start of a line, the declaration keywords: `set`, `map`, `type`, `enum`, `struct`, `event`, `function`, `import`, `scope` and `export`. * After a field’s `:` or a declaration’s `=`, the primitive types and the names your schema declares – as of the last time it parsed without an error. `Tab` accepts the highlighted item and `Up` / `Down` move through the list. A keyword expands to its skeleton with the cursor where the name goes: `struct` becomes a struct declaration with empty braces, `map` a map with an empty `[]:` pair, and so on. Configuration Editor 1 2 3 4 5 6 type Health = u8(0..100) struct Hit { Target: Instance(Player), Damage: u^u8u16u32unknown } ### Brackets and indentation [Section titled “Brackets and indentation”](#brackets-and-indentation) Typing `(`, `[`, `{` or `<` adds its closing bracket when nothing but a closing bracket or the end of the line follows the cursor, and deleting an opening bracket with a closing one right after it deletes both. Pressing `Enter` after a `{` indents the new line by one more tab and moves anything after the cursor – the closing brace, usually – to a line of its own. Pressing `Enter` at the end of an indented line keeps the indentation. ### Live diagnostics [Section titled “Live diagnostics”](#live-diagnostics) The editor parses the schema on every keystroke and marks the first problem it finds with a row of carets under where the problem starts: red for an error, amber for a warning. Hover over the carets to read the diagnostic – its severity, name, message and code, then what each label says with the line it is on. The picture above shows the same thing mid-word: `u` is not a type yet, so it is marked until you finish typing it. * Error Configuration Editor 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 option DefaultRate = 30 type Health = u8(0..100) struct Hit { Target: Instance(Player), Damage: u16(0..500) } event Damage { From: Client, Type: Reliable, Call: SingleSync, Data: Hitt^^^^Error AnalyzeUnknownReference: Unknown reference *BlinkBlox(3007)* Unknown reference \[L14] } * Warning Configuration Editor 1 2 3 4 5 6 7 8 9 type Health = u8(0..100) event HealthChanged^^^^^^^^^^^^^Warning AnalyzeMissingRateLimit: Rate limiting a Server event has no effect *BlinkBlox(3020)* This event is sent BY the server, not to it \[L3] { From: Server, Type: Unreliable, Call: SingleSync, Rate: 10, Data: Health } One diagnostic is shown at a time: the error when the schema does not parse, otherwise its first warning. Only the first line of what it points at is marked. Before 0.28.0 the editor never showed a warning at all – each one was cleared by the parse that raised it and printed to the Output window on every keystroke instead. Warnings now appear here and are not printed. ### Large files [Section titled “Large files”](#large-files) The editor does a fixed amount of work per keystroke however long the schema is. It draws the document once per change, and a diagnostic costs the same wherever it is – until 0.23.0, a diagnostic on line 800 rebuilt 800 instances on every keystroke, about 39ms. Line numbers past the first 2,000 are created as they are needed; a longer document used to crash the editor. The mouse wheel scrolls two lines at a time. # AI assistants > Giving Claude Code, Cursor and other coding assistants the BlinkBlox documentation as plain text. An assistant that has never seen BlinkBlox will write schemas in some other IDL’s syntax, or in upstream Blink’s, and guess at option names. Give it these docs instead. The site publishes them as plain text files, following the [llms.txt](https://llmstxt.org/) convention, rebuilt with every change to the site: | File | Contents | Use it when | | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | [`llms.txt`](https://xopoiii.github.io/BlinkBlox/llms.txt) | A short index pointing at the two files below. | The tool asks for an `llms.txt` URL. | | [`llms-full.txt`](https://xopoiii.github.io/BlinkBlox/llms-full.txt) | Every page of this site, in one Markdown file. | You want the assistant to have everything: the language, the guides, every option and diagnostic. | | [`llms-small.txt`](https://xopoiii.github.io/BlinkBlox/llms-small.txt) | The same pages with notes, collapsible sections and extra whitespace removed. | The context window is tight. | ## Claude Code [Section titled “Claude Code”](#claude-code) Tell Claude Code where the docs are in your project’s `CLAUDE.md`, so every session starts knowing: CLAUDE.md ```md ## Networking Networking is generated by BlinkBlox from `src/network.blink`. Never edit the generated `Server.luau` / `Client.luau`; change the schema and recompile with `blinkblox src/network.blink`. BlinkBlox documentation: https://xopoiii.github.io/BlinkBlox/llms-full.txt Fetch it before writing or changing a schema. ``` Claude Code fetches the URL when a task needs it. To have the docs in context without a fetch, save a copy into the repository and import it from `CLAUDE.md` with `@`: ```sh curl -o docs/blinkblox.md https://xopoiii.github.io/BlinkBlox/llms-full.txt ``` CLAUDE.md ```md @docs/blinkblox.md ``` A saved copy does not update itself. Fetch it again when you upgrade the compiler. ## Cursor [Section titled “Cursor”](#cursor) Add the docs as a custom documentation source: in Cursor’s settings, under the documentation section, add `https://xopoiii.github.io/BlinkBlox/llms-full.txt`, then reference it with `@Docs` in a chat. Alternatively, save the file into the project as above and mention it with `@` like any other file, or point a project rule at it. ## Other assistants [Section titled “Other assistants”](#other-assistants) Anything that can read a URL or a file works the same way: paste the `llms-full.txt` link, or attach the file. For an assistant with a small context window, use `llms-small.txt`. ## Checking its work [Section titled “Checking its work”](#checking-its-work) Whatever the assistant, have it **check the schema** after changing it. The compiler’s diagnostics name the rule and the fix (see [Diagnostics](/BlinkBlox/reference/diagnostics/)), and an assistant that reads them corrects its own mistakes far more reliably than one working from memory. ```sh blinkblox src/network.blink --check --json ``` [`--check`](/BlinkBlox/getting-started/cli/#checking-without-writing) runs the whole compile without touching the generated modules, and [`--json`](/BlinkBlox/getting-started/cli/#json-output) reports each diagnostic with its code, file, line and column, its labels and its notes, as one JSON document an assistant can read without picking through box-drawing characters. The exit code says whether the schema compiles. Put the command in `CLAUDE.md` or a project rule next to the documentation link: CLAUDE.md ```md After changing `src/network.blink`, run `blinkblox src/network.blink --check --json` and fix every error and warning it reports before recompiling. ``` There is no MCP server for BlinkBlox, and none is planned: an assistant that can run a command gets everything such a server would offer from the command above and from `llms-full.txt`. # Bandwidth > How BlinkBlox lays out bytes on the wire, and how to choose types, ranges and reliabilities that send fewer of them. Every byte a schema sends is decided by its types. This page explains how those bytes are laid out, with worked counts, so you can see what a change to the schema will cost before you make it. The counts here come from the compiler’s own size analysis, which the test suite checks against the bytes the generated serialisers actually write. ## How a packet is put together [Section titled “How a packet is put together”](#how-a-packet-is-put-together) **Reliable traffic is batched.** Every reliable event and function call fired during a frame is written into one buffer per recipient – one per player on the server, one on the client – and the buffer is sent once, on `Heartbeat`. With [`ManualReplication`](/BlinkBlox/language/options/#manualreplication) you choose when that happens by calling `StepReplication` yourself. The flush keeps its buffer for the next frame rather than growing one again from 64 bytes. **Unreliable traffic is not.** Each unreliable `Fire` is written into a fresh buffer and sent at once, as a packet of its own. Inside a packet, each item carries a small header in front of its payload: | Item | Header | | ------------------------------ | -------------------------------------------------------------------------------------------------- | | Reliable or `Unreliable` event | 1 byte: the event’s index | | `OrderedUnreliable` event | 3 bytes: the index and a 2-byte sequence number | | Function call | 2 bytes: the index and the call’s id | | Function reply | 3 bytes: the index, the call’s id and a success flag; a failed call’s reply is those 3 bytes alone | `Instance` and `unknown` values do not go into the buffer at all. They travel beside it, in the remote’s second argument, and are serialised by Roblox. They cost no buffer bytes, but they are not free, and they count against [`MaxInstancesPerPacket`](/BlinkBlox/language/options/#maxpacketsize-maxeventsperpacket-maxinstancesperpacket). ## Booleans and optionals share bytes [Section titled “Booleans and optionals share bytes”](#booleans-and-optionals-share-bytes) A `boolean` is one bit, not one byte. So is the presence flag of an optional (`T?`). Every boolean and every presence flag in the same block of the payload is packed into a shared bitfield, eight to a byte. A struct is not a block of its own: its fields share the bitfield of whatever contains them. Flags.blink ```blink struct Stance { Crouching: boolean, Sprinting: boolean, Aiming: boolean, Target: u8?, Weapon: u8? } ``` Three booleans and two presence flags are five bits, so `Stance` is one byte of flags plus whichever of the two `u8`s are present: **1 to 3 bytes**. Before 0.21.0 each of those five took a byte of its own. A new block starts where the generated code branches or loops, and bits do not cross into it: * each element of an array is a block, so booleans inside an array element share a byte per element; * an optional’s payload is a block, so the optional costs one bit in its parent and its own bits start fresh inside; * each variant of a tagged enum is a block; * a map’s key and value share one block per entry. ### Arrays of booleans [Section titled “Arrays of booleans”](#arrays-of-booleans) A plain `boolean[]` is packed eight elements to a byte after its length, rather than one bit-byte per element: | Type | Bytes | | ------------------ | -------------------------------------------------------------------------------------- | | `boolean[1000]` | 125 | | `boolean[..1000]` | 2 to 127 (a 2-byte length, then 1 byte per 8 elements) | | `boolean?[..1000]` | 2 to 2002 – not packed, since each element carries a presence bit as well as its value | ## Lengths are sized by their range [Section titled “Lengths are sized by their range”](#lengths-are-sized-by-their-range) A string, a buffer or an array carries its length in front of its contents. The length is written **relative to the declared minimum**, in the smallest unsigned integer that holds the span between the two bounds. An exact length needs no prefix at all, since the reader already knows it. | Type | Length prefix | Total bytes | | ------------------ | -------------------------------- | ----------- | | `string` | 2 (the default ceiling is 65535) | 2 to 65537 | | `string(0..64)` | 1 | 1 to 65 | | `string(0..400)` | 2 | 2 to 402 | | `string(300..400)` | 1 (the span is 100) | 301 to 401 | | `string(36)` | none | 36 | | `u8[]` | 2 | 2 to 65537 | | `u8[..16]` | 1 | 1 to 17 | | `u8[4]` | none | 4 | | `map { [u8]: u8 }` | 2, always | 2 and up | So bounding a field does two things: it lets the receiver refuse a value outside the bound before reading it (see [Securing the server](/BlinkBlox/guides/securing-the-server/)), and it usually shaves a byte off every send. A bound above 65535 (`string(0..100000)`) takes a 4-byte prefix. A map’s count is always a `u16` and a map cannot be bounded; a bounded array of key-value structs costs the same per entry and states its ceiling. ## Pick the width, then the range [Section titled “Pick the width, then the range”](#pick-the-width-then-the-range) A range on a number is a check, not an encoding: `u32(0..10)` still takes four bytes, and the receiver refuses anything outside `0..10`. Choose the narrowest type that holds every value you send, then add the range you mean. | Type | Bytes | Holds | | -------------- | ---------------------- | -------------------------------------------- | | `u8`, `i8` | 1 | 0 to 255, -128 to 127 | | `u16`, `i16` | 2 | 0 to 65535, -32768 to 32767 | | `u32`, `i32` | 4 | 0 to about 4.29 billion, about +-2.1 billion | | `f16` | 2 | about +-65504, with 11 significant bits | | `f32` | 4 | Luau’s `Vector3` precision | | `f64` | 8 | a Luau number, exactly | | `enum { ... }` | 1 | up to 256 values | | `set { ... }` | 1, 2 or 4 per 32 flags | 1 byte up to 8 flags, 2 up to 16, 4 up to 32 | | `boolean` | 1 bit | | **`f16`** is half the size of `f32` and loses precision fast as the magnitude grows. Every integer up to 2048 is exact; between 512 and 1024 the step is 0.5, between 1024 and 2048 it is 1, and near the top of its range it is 32. It suits directions, normalised values, small offsets and velocities. It does not suit world positions, where a character a thousand studs from the origin would move in half-stud steps. An `enum` costs one byte however long its names are. A state sent as `string` costs its length plus a prefix every time. ### Vectors and CFrames [Section titled “Vectors and CFrames”](#vectors-and-cframes) A vector’s component type sets its size, and a CFrame has one component type for its rotation (the first) and one for its position (the last): | Type | Bytes | Notes | | ------------------- | ----- | --------------------------------------------- | | `vector` | 12 | three `f32` | | `vector` | 6 | see `f16` above | | `vector` | 6 | whole numbers only, each within -32768..32767 | | `CFrame` | 24 | `f32` position, three `f32` Euler angles | | `CFrame` | 18 | `f16` rotation, `f32` position | | `CFrame` | 19 | `f32` position, 7-byte quaternion rotation | | `CFrame` | 13 | `f16` position, 7-byte quaternion rotation | `CFrame` encodes the rotation as the three smallest components of a unit quaternion, in 7 bytes instead of 12, to within about 0.002 degrees. It is opt-in because it is lossy: it suits characters, projectiles and cameras, not anything that compares rotations for equality. An integer rotation type keeps only whole radians and is almost never what you want. See [CFrames](/BlinkBlox/language/types/#cframes). ## A worked example [Section titled “A worked example”](#a-worked-example) The same player state, written twice: PlayerState.blink ```blink struct Naive { Health: f64, Stamina: f64, Crouching: boolean, Sprinting: boolean, Weapon: string?, Pose: CFrame } struct Tuned { Health: u8(0..100), Stamina: u8(0..100), Crouching: boolean, Sprinting: boolean, Weapon: enum { Pistol, Rifle, Knife }?, Pose: CFrame } ``` | Field | `Naive` | `Tuned` | | --------------------------------------------- | ---------------------------------- | -------------- | | `Health`, `Stamina` | 16 | 2 | | `Crouching`, `Sprinting`, `Weapon`’s presence | 1 (three bits) | 1 (three bits) | | `Weapon` = `"Rifle"` | 7 (2-byte prefix and 5 characters) | 1 | | `Pose` | 24 | 19 | | **Total** | **48** | **23** | Sent as a reliable event, add one byte for the index. At twenty updates a second to thirty players, that difference is about 15 KB a second of server upload. ## Unreliable events have a size limit [Section titled “Unreliable events have a size limit”](#unreliable-events-have-a-size-limit) Roblox drops an `UnreliableRemoteEvent` payload past roughly 900 bytes, silently and only under the conditions that produce a large packet. The compiler measures every `Unreliable` and `OrderedUnreliable` event, header included, against [`MaxUnreliableSize`](/BlinkBlox/language/options/#maxunreliablesize) (900 by default): * an event whose **smallest** possible payload is over the limit is an error, `E3018`; * an event whose **largest** possible payload is over the limit is a warning, `W3019`, which names the field that has no upper bound. At runtime every unreliable send is measured again, and one that has grown past the limit is dropped with a warning naming the event, rather than vanishing. Snapshot.blink ```blink event Snapshot { From: Server, Type: Unreliable, Call: SingleSync, Data: struct { Tick: u32, Players: struct { Id: u8, Position: vector, Crouching: boolean }[..64] } } ``` `Snapshot` is at most 518 bytes: the index, 4 for `Tick`, a 1-byte count, and 64 players of 8 bytes each (`Id`, three `f16`, and a byte holding `Crouching`’s bit). Write `[]` instead of `[..64]` and it compiles with `W3019`, because the array could then hold 65535 players. A fixed payload that can never fit is refused: Refused ```blink event Blob { From: Server, Type: Unreliable, Call: SingleAsync, Data: u8[1000] } ``` `OrderedUnreliable` costs two bytes more than `Unreliable` per packet, for the sequence number that lets the receiver discard a packet older than one it has already seen. Use it for state – a position, an aim direction – where a stale packet would make something visibly snap back, and plain `Unreliable` for independent signals. ## Checklist [Section titled “Checklist”](#checklist) * Bound every string, buffer and array. It is a smaller prefix and a check the server gets for free. * Use `u8`/`u16` and a range for counts, health, ammo and ids; `f64` only for values that need it. * Use an `enum` for anything that is one of a known set of names. * Group booleans and optionals in one struct; they share bytes. * Use `vector` for directions, `CFrame` for poses that do not need to be exact. * Keep unreliable payloads bounded well under 900 bytes. * Prefer one event with a struct over several events fired together: each event costs an index byte. ## Benchmarks [Section titled “Benchmarks”](#benchmarks) Measured in Studio against 0.29.0, an array of 1000 booleans costs BlinkBlox 3.19 Kbps, where zap and ByteNet spend about 8.5, because `boolean[]` is packed eight to a byte. Structs of plain `u8` fields cost every tool the same. [Benchmarks](/BlinkBlox/guides/benchmarks/) has the full numbers and how they were measured. # Benchmarks > Frame rate and bandwidth of BlinkBlox 0.29.0 against plain remotes, zap and ByteNet, measured in Studio. Each tool fires the same event 1000 times a frame, with the same data, for ten seconds, and every tool received everything it sent. The numbers below are medians. The percentiles, and the harness itself, are in [`benchmark/Benchmarks.md`](https://github.com/XopoIII/BlinkBlox/blob/main/benchmark/Benchmarks.md). The run was made on 2026-09-24 on an Apple M1 with 16 GB of memory, in Roblox Studio. The versions were BlinkBlox 0.29.0, zap 0.6.29 and ByteNet 0.4.3. ## Booleans [Section titled “Booleans”](#booleans) The payload is an array of 1000 `true` values: `boolean[0..1000]`. | Tool | FPS | Kbps | | -------------- | ------ | -------- | | Roblox remotes | 15 | 136800 | | **BlinkBlox** | **57** | **3.19** | | zap | 37 | 8.53 | | ByteNet | 22 | 8.33 | BlinkBlox sends about 2.7 times less data than zap or ByteNet here, because [`boolean[]`](/BlinkBlox/language/types/#arrays-of-booleans) is packed eight elements to a byte. The other two spend a whole byte on each boolean. ## Entities [Section titled “Entities”](#entities) The payload is 100 structs of six `u8` fields each: an id, a position, an orientation and an animation. | Tool | FPS | Kbps | | -------------- | ------ | --------- | | Roblox remotes | 16 | 721337 | | **BlinkBlox** | **60** | **41.57** | | zap | 42 | 41.86 | | ByteNet | 24 | 41.71 | Every tool encodes six `u8` fields in six bytes, so the bandwidth is the same within noise. The frame rate is where they differ: the generated serialisers cost less per event. ## Reading the numbers [Section titled “Reading the numbers”](#reading-the-numbers) 60 FPS is Studio's cap Studio does not render faster than 60 frames a second. A tool at 60 FPS is limited by the cap, not by its own cost, so the Entities result shows only that BlinkBlox kept up. It does not show by how much. * **The inbound limits are raised for the run.** [`benchmark/definitions/Definition.blink`](https://github.com/XopoIII/BlinkBlox/blob/main/benchmark/definitions/Definition.blink) sets the following options far above their defaults: * [`MaxPacketSize`](/BlinkBlox/language/options/#maxpacketsize-maxeventsperpacket-maxinstancesperpacket) and `MaxEventsPerPacket`; * `InboundBytesPerSecond` and `InboundBurst`. A thousand events a frame is a flood, and with the defaults a live server refuses most of it. Refusing it is the purpose of those limits. The checks still run, so their cost is included in the numbers above. See [Securing the server](/BlinkBlox/guides/securing-the-server/). * **The events go from client to server.** That direction is the one BlinkBlox validates. Every event is checked before a listener sees it. * **Bandwidth depends on your schema.** These two payloads are simple. Where a schema has bounded lengths, optionals or booleans spread across structs, the savings come from the techniques in [Bandwidth and sizes](/BlinkBlox/guides/bandwidth/). * **A different machine gives different numbers.** The ratios between the tools are the useful part. ## Running them yourself [Section titled “Running them yourself”](#running-them-yourself) The harness lives in [`benchmark/`](https://github.com/XopoIII/BlinkBlox/tree/main/benchmark). For now it is Windows-only: it uses 7-Zip, `wmic` and `build.bat` to fetch the tools and to record the machine’s specs. On other systems, fetch the tools by hand and fill in the specs yourself. # Migrating from Blink > Moving a game from upstream Blink 0.18.x to BlinkBlox -- what stays, what the new compiler refuses, and what a running game will notice. BlinkBlox forked from upstream Blink at `v0.18.8`, and was called Blink until 0.28.0. A schema written for Blink 0.18.x is a BlinkBlox schema: the language only grew. What changes is the compiler’s name, the bytes on the wire, a handful of schemas it now refuses, and some runtime behaviour on the server. ## The short version [Section titled “The short version”](#the-short-version) 1. Install the `blinkblox` compiler, and the BlinkBlox Editor plugin if you use Studio. See [Installation](/BlinkBlox/getting-started/installation/). 2. Compile your schema. Fix any new errors (below) and read the new warnings. 3. Replace deprecated spellings: `Poll: true` with `Call: Polling`, `.Next()` with `.Iter()`, and delete `option UseColon`. 4. Decide rate limits for what clients send, and install the two server handlers. See [Securing the server](/BlinkBlox/guides/securing-the-server/). 5. Ship the regenerated server and client modules **together**. They do not talk to modules built by upstream Blink, and a client refuses to run against a server built from a different schema. ## What stays the same [Section titled “What stays the same”](#what-stays-the-same) These keep their upstream names on purpose, so that nothing in a game has to be renamed and builds on either side of the 0.28.0 rename still find each other: | Thing | Name | | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | Schema files | `.blink` | | Remotes | `BLINK_RELIABLE_REMOTE`, `BLINK_UNRELIABLE_REMOTE` in `ReplicatedStorage`, prefixed by [`RemoteScope`](/BlinkBlox/language/options/#remotescope) when set | | Duplicate-instance guard | `_G._BLINK` | | Studio plugin output folder | `Blink` | | Studio plugin schema storage | `ServerStorage.BLINK_CONFIGURATION_FILES` | | Generated API | `Fire`, `FireAll`, `FireList`, `FireExcept`, `On`, `Iter`, `Invoke`, `StepReplication`, with the same `Casing` | | Options | the options upstream 0.18.x had keep their names and meaning; `UseColon` is deprecated | ## What changes in the tooling [Section titled “What changes in the tooling”](#what-changes-in-the-tooling) | | Upstream Blink | BlinkBlox | | ------------------- | ----------------- | ------------------------------------------------------------------------------ | | Command-line binary | `blink` | `blinkblox` | | Rokit | | `rokit add XopoIII/BlinkBlox blinkblox` | | pesde | | `xopoiii/blinkblox` | | Studio plugin | upstream’s plugin | BlinkBlox Editor | | Release artifacts | | `blinkblox--.tar.xz` (`.tar.gz` on Windows), `blinkblox-plugin.rbxm` | Scripts, CI jobs and git hooks that call `blink` need the new binary name. The flags, including the new `--profile`, are listed under [Command line](/BlinkBlox/getting-started/cli/). ## The wire format is not upstream’s [Section titled “The wire format is not upstream’s”](#the-wire-format-is-not-upstreams) BlinkBlox packs data differently from Blink 0.18.x, so a module built by one cannot decode traffic from a module built by the other: * **0.21.0** put booleans and optional flags into a shared bitfield, encoded lengths relative to their minimum, and added `OrderedUnreliable`. * **0.27.0** packed `boolean[]` eight elements to a byte. Since 0.23.0 each pair of modules also carries a **schema signature**, a hash of every event and function and the types they carry, plus a wire-format version. The server publishes it on its reliable remote, and the client checks it when it is required: a mismatch is an error at startup rather than one event silently decoded as another. A server built by upstream Blink publishes no signature at all, so a BlinkBlox client errors after waiting five seconds for one. The practical rule: regenerate both modules with the same compiler, from the same schema, with the same profile, and ship them together. See [Wire compatibility](/BlinkBlox/reference/wire-compatibility/) for exactly what the signature covers. ## Deprecated spellings [Section titled “Deprecated spellings”](#deprecated-spellings) Each still works. The first two warn when compiled. | Deprecated | Replace with | Notice | | ------------------------ | --------------- | -------------------------------------------------------------------------------------------------------- | | `Poll: true` on an event | `Call: Polling` | `W3017`. The `Call` value written beside `Poll: true` is discarded. | | `option UseColon` | delete the line | `W3017`. No generator has ever read it. | | `Event.Next()` | `Event.Iter()` | Annotated `@deprecated` in the generated module, so your editor flags it. The two are the same function. | ## Schemas the compiler now refuses [Section titled “Schemas the compiler now refuses”](#schemas-the-compiler-now-refuses) Some schemas that compiled on upstream Blink are errors now. Each of these compiled into a module that misbehaved at runtime – usually silently – and is refused at compile time instead. The [diagnostics reference](/BlinkBlox/reference/diagnostics/) has the full list. | Code | What is refused | What to do | | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | | `E3001` | A map keyed by a struct, tagged enum, map, set, array or type pack. Each decoded key is a fresh table that nothing can look up. | Key by a string, number, boolean or enum; move the rest into the value. | | `E3004` | A set flag, enum value or tagged-enum variant named twice. | Remove the duplicate. | | `E3005` | A declaration named `SetRateLimitHandler` or `SetDecodeErrorHandler`, which the module now uses; a type-pack element named after a Luau keyword or a name the generated code uses; a type named after a built-in Luau type (`number`, `string`, …) or a Roblox type the module uses (`Player`, `Instance`, …). | Rename it. | | `E3013` | An exported type containing an `Instance` or `unknown`. | Drop `export`, or move the Instance out of the type. | | `E3018` | An unreliable event whose smallest payload cannot fit in an unreliable packet. | Send it `Reliable`, or shrink it. | | `E3021` | An unbounded array or a map whose elements cost nothing to decode, such as `struct {}[]`. | Bound it (`[..16]`) or give the element a field. | | `E3022` | A type that refers to itself. | Restructure it; references are inlined, so a type cannot contain itself. | | `E3024` | The same option set twice. | Keep one. | | `E3029` | An enum with no values, or more than 256. | An enum travels as one byte. | | `E3030` | More than 256 reliable events and functions, or more than 256 unreliable events. | Each channel numbers its declarations with one byte; upstream sent the 257th as the first. | | `E2003` | A numeric option that is not a positive whole number, or an unknown `Casing`. | Fix the value. | New warnings are worth reading too. `W3019` means an unreliable event may exceed the \~900-byte limit Roblox enforces; bound the field it names. ## What a running game will notice [Section titled “What a running game will notice”](#what-a-running-game-will-notice) ### On the server [Section titled “On the server”](#on-the-server) * **Inbound packets are bounded by default.** A client’s packet is dropped if it is over 8192 bytes or carries more than 256 instances; decoding stops after 64 events in one packet; and each player’s packets may cost at most 65536 bytes a second. Reliable events are batched per frame, so a client that fires more than 64 of them in one frame loses the rest of that frame’s. See [`MaxPacketSize`](/BlinkBlox/language/options/#maxpacketsize-maxeventsperpacket-maxinstancesperpacket) and [`InboundBytesPerSecond`](/BlinkBlox/language/options/#inboundbytespersecond-inboundburst) if your game sends more. * **Rate limits apply only where you declare them.** Nothing is rate-limited until you add `Rate`, `DefaultRate` or `RequireRates`. * **Queues are capped.** A reliable event that arrives with no listener is queued up to 256, then dropped without a warning; a call to a function with no listener past 256 is answered with a failure; a polled event’s queue stops at 256 rows per `Players.MaxPlayers`. * **Decode failures are silent** unless you install `SetDecodeErrorHandler`, so that a client sending garbage cannot fill your output. Warnings about malformed, oversized or refused packets are printed at most once a second per player. * **The server can invoke a client**, with a `From: Server` function. Existing functions are `From: Client` by default and unchanged. ### On both sides [Section titled “On both sides”](#on-both-sides) * **Invocations time out.** An `Invoke` that is not answered within [`InvocationTimeout`](/BlinkBlox/language/options/#invocationtimeout) (10 seconds) fails the same way an errored handler does, instead of waiting forever. At most 32 may be outstanding at once; the 33rd errors. * **`Fire` enforces length bounds.** A string, buffer or array longer than its declared bound makes `Fire` throw even with `WriteValidations` off, instead of sending a length prefix that wrapped. When any write throws, the half-written event is taken back out, so the rest of the frame’s batch is unaffected. * **A second `.On` on a `Single` event warns.** It still replaces the first listener, and the first listener’s disconnect function stops working. * **An error in a Sync listener** is raised on its own thread, rather than being reported as a packet that failed to decode and taking the rest of the packet with it. * **Ranged floats refuse NaN**, and an open float range such as `f32(0..)` is no longer capped at 2^24. * **`Color3` channels are rounded and clamped** to 0..255, so an HDR channel above 1 arrives as 255 instead of wrapping. * **Edit mode keeps the API’s shape.** When `RunService:IsRunning()` is false every entry point is a stub, and `On` returns a disconnect function and `Iter` iterates zero times, so stories do not error. See [Generated API](/BlinkBlox/reference/generated-api/#edit-mode). # roblox-ts > Generating TypeScript declarations for the server and client modules, and using them from a roblox-ts project. BlinkBlox generates Luau. For a [roblox-ts](https://roblox-ts.com/) project it can also write a TypeScript declaration file (`.d.ts`) beside each module, so TypeScript code imports the generated module with its types. ## Turning it on [Section titled “Turning it on”](#turning-it-on) Set [`Typescript`](/BlinkBlox/language/options/#typescript) and point the outputs into your project’s source folder: network.blink ```blink option Typescript = true option ServerOutput = "src/server/network.luau" option ClientOutput = "src/client/network.luau" option PromiseLibrary = "ReplicatedStorage.rbxts_include.Promise" event Chat { From: Client, Type: Reliable, Call: SingleAsync, Rate: 2, Data: string(1..200) } event Announce { From: Server, Type: Reliable, Call: ManyAsync, Data: (Text: string(0..200), Seconds: u8) } function GetCoins { Yield: Promise, Data: u8, Return: u32 } ``` Compiling writes four files: | File | What it is | | ------------------------- | ----------------- | | `src/server/network.luau` | the server module | | `src/server/network.d.ts` | its declarations | | `src/client/network.luau` | the client module | | `src/client/network.d.ts` | its declarations | A module whose output is named `init.luau` gets `index.d.ts`, which is the name TypeScript looks for in a folder. roblox-ts copies the Luau files into `out` beside the compiled code and takes their types from the declaration of the same name. `PromiseLibrary` is spliced into the module as `require(...)`. The path above is where the default roblox-ts project places its Promise implementation; point it at yours if the layout differs. A `Yield: Promise` function needs it, and `Yield: Future` needs `FutureLibrary` in the same way. ## Using the declarations [Section titled “Using the declarations”](#using-the-declarations) src/server/chat.server.ts ```ts import { Chat, Announce, GetCoins } from "./network"; Chat.On((player, text) => { Announce.FireAll(`${player.Name}: ${text}`, 5); }); GetCoins.On((player, slot) => { return 100; }); ``` src/client/chat.client.ts ```ts import { Chat, Announce, GetCoins } from "./network"; Announce.On((text, seconds) => print(text, seconds)); Chat.Fire("hello"); GetCoins.Invoke(1).then((coins) => print(coins)); ``` Each declaration mirrors the Luau API on its side, with the same [`Casing`](/BlinkBlox/language/options/#casing): | Schema | TypeScript | | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- | | numbers | `number` | | `string`, `boolean`, `buffer` | `string`, `boolean`, `buffer` | | `vector`, `CFrame`, `Color3`, `BrickColor`, `DateTime`, `DateTimeMillis` | `Vector3`, `CFrame`, `Color3`, `BrickColor`, `DateTime`, `DateTime` | | `Instance(Part)` | `Part` | | `unknown` | `unknown` | | `T?` | `T \| undefined`, and an optional struct field is marked `?` | | `T[]` | `T[]` | | `map { [K]: V }` | `Map` | | `set { A, B }` | `{ A: boolean, B: boolean }` | | `enum { A, B }` | `"A" \| "B"` | | tagged enum | a union of the variants’ object types, each with its tag field | | type pack `(Text: string, Seconds: u8)` | separate parameters, named after the elements; as a function’s `Return` or in `Iter`, a `LuaTuple` | | `scope` | a `namespace` | | `Yield: Promise` | `Invoke` returns `Promise` | | a polled event | `Iter: () => IterableFunction>` | The full list of functions on each side is in [Generated API](/BlinkBlox/reference/generated-api/). ## Current gaps [Section titled “Current gaps”](#current-gaps) The declarations lag the Luau API in a few places. Until they are closed, work around them: * **The two server handlers are not declared.** `SetRateLimitHandler` and `SetDecodeErrorHandler` exist on the module but not in the `.d.ts`. Cast to reach them: src/server/security.server.ts ```ts import * as Network from "./network"; const Handlers = Network as unknown as { SetRateLimitHandler(handler: (player: Player, event: string | undefined, refused: number) => void): void; SetDecodeErrorHandler(handler: (player: Player, event: string | undefined, failure: string) => void): void; }; Handlers.SetRateLimitHandler((player, event, refused) => warn(player.Name, event, refused)); ``` * **Schema types are not exported.** A named type such as `struct Pos` is declared in the `.d.ts` but not exported, and neither is an `export`ed type’s `Read`/`Write` pair. Derive a type from a function that uses it instead, for example `Parameters[0]`. * **`Yield: Future` is typed as the bare return value**, not as a future. Prefer `Promise` or `Coroutine` in a roblox-ts project. * **The deprecated `Next` is not declared.** Use `Iter`. * **`TypesOutput` has no declaration file.** The shared types module is for Luau consumers. # Securing the server > A checklist for pointing a BlinkBlox server at the open internet -- what the generated module already refuses, and the decisions that are left to you. Every client can call `FireServer` with anything it likes, as often as it likes. The generated server module is written on that assumption: it should be safe to point at the open internet. Much of that is on by default and needs nothing from you. Some of it cannot be, because only the game knows how much traffic it means to receive – and a limit set too low drops real players’ events, which looks like lag rather than like a bug. This page is the checklist: what the server already does, the decisions you have to make, and what BlinkBlox does not protect you from. ## What the server does without being asked [Section titled “What the server does without being asked”](#what-the-server-does-without-being-asked) Every packet a client sends goes through these checks, in this order. Each one is cheaper than the next, so a hostile packet is refused as early as possible. | Order | Check | On failure | | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | 1 | The remote’s arguments are a `buffer` and a table. | Dropped unread, warned. | | 2 | The packet is at most [`MaxPacketSize`](/BlinkBlox/language/options/#maxpacketsize-maxeventsperpacket-maxinstancesperpacket) bytes (8192). | Dropped unread, warned. | | 3 | The player’s [inbound byte budget](/BlinkBlox/language/options/#inboundbytespersecond-inboundburst) can pay for it (65536 bytes a second, each packet costing at least 128). | Dropped unread, warned, rate-limit handler called with `Event = nil`. | | 4 | It carries at most [`MaxInstancesPerPacket`](/BlinkBlox/language/options/#maxpacketsize-maxeventsperpacket-maxinstancesperpacket) instance references (256). | Dropped unread, warned. | | 5 | Decoding runs one event at a time, and stops when the player has left. | The rest of the packet is skipped. | | 6 | At most [`MaxEventsPerPacket`](/BlinkBlox/language/options/#maxpacketsize-maxeventsperpacket-maxinstancesperpacket) events are decoded from one packet (64). | The rest of the packet is dropped, warned. | | 7 | Each event is decoded inside a `pcall`. An index that names no event, a truncated payload, a length or count that the packet cannot back, a value outside its range or an Instance of the wrong class all fail here. | The rest of the packet is dropped; your decode-error handler is called. | | 8 | The event’s [rate bucket](/BlinkBlox/language/events/#rate-burst) has a token, if the event has a rate. | The event is dropped before any listener runs; the rate-limit handler is called. | | 9 | A reliable event with no listener is queued, up to 256. | Further events are dropped silently. | Some of what step 7 covers is worth knowing in detail: * **A length is checked before the read it authorises.** A string, buffer or array length prefix is compared against its declared bound, and against the bytes still left in the packet, before anything is read or allocated for it. A client cannot buy a 65535-slot `table.create` with two bytes. * **Ranged floats refuse NaN.** Range checks are written so that NaN fails them rather than slipping through both comparisons. * **A loop always has something to run out of.** An array or map whose elements cost no bytes and no instances (`struct {}[]`) is refused at compile time (`E3021`), because nothing would stop its decode loop. And outside the decode loop: * Both remotes are checked for the right class when the module loads, so an instance of the wrong class planted under the remote’s name is refused rather than used. * Every warning the server prints about a client’s traffic, and every call it makes to your handlers about it, happens at most once a second per player (per kind, and per event for decode failures and rate limits). A client cannot use a stream of bad packets to fill your output or spawn your handler hundreds of times a second. * A player who leaves has their buffers, rate buckets, budgets and outstanding invocations released on `PlayerRemoving`. None of this needs an option. The limits it uses have defaults you can change, below. ## The checklist [Section titled “The checklist”](#the-checklist) 1. **Rate-limit every inbound event and function.** Rate limiting is off unless you ask for it. Give each event or function the client sends a `Rate` (events per second, per player) and optionally a `Burst` (how many may arrive at once; defaults to `Rate` rounded up). Then set `option RequireRates`, so that a new event without one fails the build instead of shipping unlimited. Game.blink ```blink option RequireRates = true event Chat { From: Client, Type: Reliable, Call: SingleAsync, Rate: 2, Burst: 5, Data: string(1..200) } event Aim { From: Client, Type: OrderedUnreliable, Call: SingleSync, Rate: 60, Data: (Pitch: f32(-90..90), Yaw: f32(-180..180)) } function BuyItem { Yield: Coroutine, Rate: 1, Burst: 3, Data: u16(1..500), Return: boolean } ``` `option DefaultRate` supplies a rate to every inbound declaration that does not name its own, and it satisfies `RequireRates`. Use it as a floor for a large schema, and still set a tighter `Rate` on anything that costs the server real work – a purchase, a datastore write, a raycast. A rate on a `From: Server` event or function does nothing and warns (`W3020`): only inbound traffic is limited. A `Burst` with no `Rate` behind it is an error (`E3020`). A function whose listener waits – a DataStore save, a purchase, anything that yields – also wants a [`Concurrency`](/BlinkBlox/language/functions/#concurrency): how many of one player’s calls may be running at once. `Rate` bounds how many start; `Concurrency` bounds how many are still waiting, which is what a stream of calls turns into when each one takes seconds to answer. The check runs after the event is decoded and before it is dispatched, so it protects your listener and the game logic behind it. It cannot run sooner: an event carries no length of its own, so the only way to find where the next event in a packet starts is to read this one. The decoding itself is bounded by the packet limits and `InboundBytesPerSecond` (step 3). A refused function call is still answered, with a failure, so the caller raises at once instead of waiting out the timeout. See [`Rate`, `Burst`](/BlinkBlox/language/events/#rate-burst) and [`DefaultRate`, `RequireRates`](/BlinkBlox/language/options/#defaultrate-requirerates). 2. **Bound every variable-length field and every number the game relies on.** A range is checked on the receiving side, before your listener sees the value. Write the range you actually mean: | Instead of | Write | Why | | ---------------------- | ------------------------------------------------ | ----------------------------------------------------- | | `string` | `string(1..200)` | An unbounded string may be up to 65535 bytes. | | `u8[]` | `u8[..16]` | An unbounded array may hold up to 65535 elements. | | `f32` | `f32(0..100)` | The range is checked, and a ranged float refuses NaN. | | `vector` | `vector(0..1)` | A vector’s range bounds its magnitude. | | `Instance` | `Instance(BasePart)` | The class is checked with `IsA` on receipt. | | `map { [string]: u8 }` | `struct { Key: string(1..32), Value: u8 }[..32]` | A map cannot be bounded: its count is always a `u16`. | The bound also sizes the length prefix, so a tighter range is usually a smaller packet as well; see [Bandwidth](/BlinkBlox/guides/bandwidth/). Lengths are checked on send too, whatever `WriteValidations` says: a value longer than its bound makes `Fire` throw rather than put a corrupt packet on the wire. Caution A range on a number does not make it smaller on the wire. `u32(0..10)` still costs four bytes; pick the narrowest type that holds the range, and write the range as well. 3. **Check the packet limits and the inbound budget against your real traffic.** | Option | Default | Bounds | | ----------------------- | ----------------------------------- | ---------------------------------------------------- | | `MaxPacketSize` | `8192` | Bytes in one packet. | | `MaxEventsPerPacket` | `64` | Events decoded from one packet. | | `MaxInstancesPerPacket` | `256` | Instance and `unknown` values carried by one packet. | | `InboundBytesPerSecond` | `8 * MaxPacketSize` (65536) | Bytes a second each player’s packets may cost. | | `InboundBurst` | the larger of the two above (65536) | The most budget a player may bank. | A client’s reliable events for one frame travel as one packet, so the per-packet limits are per-frame limits for reliable traffic: a client that fires more than 64 reliable events in a frame loses the rest of that frame’s events. Every unreliable `Fire` is a packet of its own and costs at least 128 bytes of budget, so at the default a client can send about 500 small unreliable packets a second – roughly seven a frame at 60 FPS, alongside its reliable traffic. Raise `InboundBytesPerSecond` if your game genuinely sends more. Whatever you allow a real client, you also allow an attacker. Raise a limit only as far as your traffic needs. `InboundBurst` may not be below `MaxPacketSize` (`E2003`), or the largest packet could never be afforded. See [`MaxPacketSize`](/BlinkBlox/language/options/#maxpacketsize-maxeventsperpacket-maxinstancesperpacket) and [`InboundBytesPerSecond`](/BlinkBlox/language/options/#inboundbytespersecond-inboundburst). 4. **Install the two handlers.** The server warns about refused traffic, once a second per player. To act on it – log it, flag the account, kick – give it a handler. Nothing is kicked automatically: a false positive under server lag would eject a legitimate player, and whether that is the right response is the game’s call. Security.server.luau ```luau local Net = require(ServerScriptService.Network.Server) -- Event is nil when whole packets were refused over the inbound byte budget. -- Refused counts the refusals this call stands for, since the last one. Net.SetRateLimitHandler(function(Player: Player, Event: string?, Refused: number) Analytics:Record(Player, "RateLimited", Event, Refused) end) -- Event is the event the packet claimed to carry, or nil if its index named none. Net.SetDecodeErrorHandler(function(Player: Player, Event: string?, Failure: string) Analytics:Record(Player, "DecodeFailure", Event, Failure) end) ``` Both handlers are called on a thread of their own, at most once a second per player (and per event), so a handler that yields or throws does not disturb decoding. Without a decode-error handler the server says nothing about packets that fail to decode, so that a client sending garbage every frame cannot fill the output. See [Packets that cannot be decoded](/BlinkBlox/language/events/#packets-that-cannot-be-decoded). 5. **Keep Sync listeners from yielding.** A `SingleSync` or `ManySync` listener runs on the decode thread. If it yields, the rest of the packet it arrived in is stuck behind it; with [`SyncValidation`](/BlinkBlox/language/options/#syncvalidation) on (the default), the next packet notices, warns and discards what was left. Use `SingleAsync` or `ManyAsync` for any listener that may yield – a datastore call, a `task.wait`, a remote round trip. An error thrown by a Sync listener is caught and raised on a thread of its own; it is not reported as the sender’s decode failure, and the rest of the packet is still delivered. 6. **Keep debug remotes out of release builds.** An admin command or a “give me money” test event left in a shipped schema is an entry point any client can reach. Mark it with a [profile](/BlinkBlox/language/profiles/): Game.blink ```blink @profile("dev") event GiveCurrency { From: Client, Type: Reliable, Call: SingleAsync, Data: u32 } ``` A build without a profile is `release`, so forgetting the flag leaves `GiveCurrency` out rather than shipping it. A compiled declaration that refers to one marked `dev` is refused (`E3027`), so an excluded type cannot leak into the build through a reference. Build client and server with the same profile – their schema signatures differ otherwise. 7. **Know what a function call can cost you.** Each side can have at most 32 invocations waiting for an answer at once – per client, and per player on the server. Past that, `Invoke` errors. An unanswered call fails after [`InvocationTimeout`](/BlinkBlox/language/options/#invocationtimeout) (10 seconds) and gives its slot back; calls to a player who leaves fail at once. A reply is accepted only from the player the call went to, and only for the function it was made to. A [`From: Server`](/BlinkBlox/language/functions/#from) function asks a client a question. Its answer is client data like any other: range-check what you can in the schema, validate the rest in code, and do not hold a lock or a queue slot across the call. 8. **Build both modules from the same schema and the same compiler.** The client checks the server’s [schema signature](/BlinkBlox/language/options/#schema-signatures) when it loads and refuses to run against a different one. That turns a stale client – one that would decode event 7 as whatever event 7 used to be – into an error at startup. See [Wire compatibility](/BlinkBlox/reference/wire-compatibility/). ## Queues a client can fill [Section titled “Queues a client can fill”](#queues-a-client-can-fill) Two queues on the server hold what a client sent before anything consumed it. Both are capped, so a client cannot grow server memory by sending an event the game never handles. | Queue | Cap | Past the cap | | -------------------------------------------------------------------------------------------- | ----------------------------------------- | ---------------------------------------------- | | A reliable event with no listener connected yet | 256 events per event | Dropped without a warning. | | An invocation with no listener connected yet | 256 calls per function | Answered with a failure, so the caller raises. | | A [polled](/BlinkBlox/language/events/#iterating-an-event-polling) event nobody has iterated | 256 x `Players.MaxPlayers` rows per event | Dropped without a warning. | Unreliable events are never queued: one that arrives with no listener is dropped. On the client the listener queue warns past 256 instead of dropping, because there it usually means a listener somebody forgot. ## What BlinkBlox does not protect you from [Section titled “What BlinkBlox does not protect you from”](#what-blinkblox-does-not-protect-you-from) BlinkBlox guarantees that what reaches your listener is **well-formed**: the declared types, within the declared ranges, at no more than the declared rate. It cannot know whether it is **legitimate**. * **Game logic.** A `BuyItem(42)` inside `u16(1..500)` is a valid packet whether or not the player can afford item 42. A `Teleport` position inside `vector(0..5000)` is valid even if it is inside a wall. Every value a client sends is a claim; check it against the server’s own state. * **What a client already knows.** Everything the server sends a client is readable by that client, and so is its copy of the generated module. The buffer encoding is compact, not encrypted. Do not send what a player should not see. * **A modified client.** The client module is the attacker’s to edit. The schema signature catches a stale build, not a tampered one; the server’s checks are the only ones that count. * **`unknown`.** An `unknown` value travels through Roblox’s own serialisation and is not validated at all – it can be any type the remote can carry. Avoid it in anything the client sends. * **Instances.** `Instance(Class)` checks the class, not ownership: a client can name any instance it can see, including another player’s character. * **Timing.** A lag switch – a client that holds its traffic and then releases it – looks exactly like a network hitch, and the inbound burst exists to let an honest hitch through. Judge time by when the server received something, and treat any timestamp or tick a client puts in a schema as a claim. * **Traffic that is not BlinkBlox’s.** Other remotes in your game, Roblox’s own replication and anything that happens before a packet reaches the generated handler are outside it. * **The server’s own sends.** The client trusts the server, and nothing limits what the server sends. With `WriteValidations` off (the default) the server checks lengths on send but not types or ranges, so a bug in server code can still send a client a value outside its range – which the client then refuses. # Events > Declaring one-way messages between client and server, and the API BlinkBlox generates for sending, receiving and limiting them. Events are BlinkBlox’s version of Roblox’s `RemoteEvent` and `UnreliableRemoteEvent`: one side sends, the other listens, and nobody waits for an answer. They are the main way client and server talk. When the sender needs a reply, use a [function](/BlinkBlox/language/functions/) instead. ## Declaring an event [Section titled “Declaring an event”](#declaring-an-event) Events are declared with the `event` keyword and a block of fields. ```blink event MyEvent { From: Server, Type: Reliable, Call: SingleAsync, Data: f64 } ``` `From`, `Type` and `Call` are required; `Data`, `Rate` and `Burst` are optional. ### `From` [Section titled “From”](#from) The side that fires the event: `Server` or `Client`. The other side listens to it. ### `Type` [Section titled “Type”](#type) The reliability of the event. * `Reliable` – guaranteed to arrive, in the order it was sent relative to every other reliable event and function call. Reliable traffic is batched: each side collects it into one buffer (the server keeps one per player) and sends the batch once a frame. See [`ManualReplication`](/BlinkBlox/language/options/#manualreplication). * `Unreliable` – may be lost, and may arrive out of order. Each `Fire` is sent at once, in a packet of its own, and a payload may be at most 900 bytes by default. BlinkBlox enforces that at compile time and again on send – see [`MaxUnreliableSize`](/BlinkBlox/language/options/#maxunreliablesize). * `OrderedUnreliable` – as `Unreliable`, plus a two-byte sequence number: a packet that arrives *after* a newer one is discarded rather than delivered late. Delivery is still not guaranteed and packets can still be lost – what is guaranteed is that your listener never sees an older state after a newer one. ### `Call` [Section titled “Call”](#call) The listening API generated on the receiving side. | `Call` | Listeners | May yield | Runs on | | ------------- | ------------------------------------------------------------- | --------- | ------------------- | | `SingleSync` | one | no | the decode thread | | `ManySync` | many | no | the decode thread | | `SingleAsync` | one | yes | a thread of its own | | `ManyAsync` | many | yes | a thread of its own | | `Polling` | none – you iterate with [`Iter`](#iterating-an-event-polling) | – | your loop | `Call` is required on every event, including on the side that only fires it, where it has no effect. It is not part of the [schema signature](/BlinkBlox/language/options/#schema-signatures), so changing it does not break a client built before the change. An `Async` listener is dispatched through a small thread pool rather than a fresh `task.spawn` per event: a thread is reused as long as the listener before it neither yielded nor errored. Caution A `Single` event keeps exactly one listener. Calling `.On()` a second time **replaces** the first and warns, rather than erroring – but the displaced listener’s disconnect function stops working at that moment, because it only detaches a listener that is still the current one. So two systems that each connect and later disconnect will leave a listener attached that neither believes it owns. If more than one place needs to hear an event, declare it `ManySync` or `ManyAsync`. Danger A `Sync` listener runs **on the decode thread**. Yielding in one stops decoding the packet it arrived in, and the events behind it in that packet are lost – see [`SyncValidation`](/BlinkBlox/language/options/#syncvalidation), which reports this but only once another packet turns up. An error thrown by a `Sync` listener costs nothing else: it is caught, raised again on a thread of its own – where an `Async` listener’s error surfaces too – and the rest of the packet is still delivered. Prefer `SingleAsync` or `ManyAsync` unless the per-event cost of dispatching to a thread genuinely matters to you. ### `Poll` [Section titled “Poll”](#poll) Deprecated, and worth reading even if you have never typed it – it may be in a schema you inherited. `Poll: true` is the 0.x way of asking for polling. `Call` is still required beside it, and its value is then **discarded**: an event declared `Call: SingleSync, Poll: true` compiles to `Polling`, not to `SingleSync`. It went undocumented and unwarned for long enough that a schema using it had no way of learning there was a current spelling. It still works and now warns (`W3017`). Replace it with `Call: Polling` and delete the `Poll` line. ### `Rate`, `Burst` [Section titled “Rate, Burst”](#rate-burst) Limits how often one player may send this event. Both fields are optional, and both are ignored on an event the server sends – only inbound traffic is limited, and setting either there warns (`W3020`). `Rate` is the sustained number of events per second accepted from a single player. It may be any number above zero, including a fraction: `Rate: 0.5` is one event every two seconds. `Burst` is the bucket capacity: how many may arrive at once before the rate applies. It defaults to `Rate` rounded up, and may not be below 1: each event spends one whole token, so a smaller bucket would refuse every event however long the client waited. A `Burst` with no `Rate` – and no [`DefaultRate`](/BlinkBlox/language/options/#defaultrate-requirerates) to supply one – is refused, since nothing would refill it. Over any stretch of `t` seconds a player gets at most `Burst + Rate × t` events through, and never more than `Burst` at once. With the default `Burst`, that is up to twice `Rate` in a single second, and `Rate` per second over longer spans. Refused events cost the player nothing. ```blink event Purchase { From: Client, Type: Reliable, Call: SingleAsync, Rate: 5, Burst: 10, Data: u16 } ``` Each player’s bucket starts full. Refused events are dropped before they reach your listener, and reported at most once per player per event per second. To do something else about them – log to your analytics, flag the account – pass a handler to `SetRateLimitHandler`. It is called on the same schedule as the warning, and `Refused` is how many refusals that call stands for: the one that made it, and those counted since the last. `Event` is `nil` when what was refused was whole packets over the [inbound budget](/BlinkBlox/language/options/#inboundbytespersecond-inboundburst) rather than one event: Server.luau ```luau local Net = require(path.to.Server) Net.SetRateLimitHandler(function(Player: Player, Event: string?, Refused: number) Analytics:Record(Player, Event, Refused) end) ``` The handler runs on a thread of its own, so one that yields or throws does not touch the packet being decoded. The client module has a `SetRateLimitHandler` too, which does nothing – it exists so code shared between both sides can call it without branching. The limit is checked after the event is deserialised and before it is dispatched. Payloads are variable length, so an event cannot be skipped without reading it: the cursor would be left mid-event and the rest of the packet would decode as garbage. What the limit saves is the listener and the game logic behind it. Bounding the decoding itself is what [`MaxEventsPerPacket`](/BlinkBlox/language/options/#maxpacketsize-maxeventsperpacket-maxinstancesperpacket) does. A refused [function](/BlinkBlox/language/functions/) call is still answered – with a failure, so the caller’s `Invoke` raises promptly rather than hanging. See [`DefaultRate` and `RequireRates`](/BlinkBlox/language/options/#defaultrate-requirerates) for applying a limit across a whole schema, and for making a missing one a compile error. ### `Data` [Section titled “Data”](#data) The data the event carries: any [type](/BlinkBlox/language/types/), written inline or by name. Omit the field when the event carries nothing. #### Type Packs [Section titled “Type Packs”](#type-packs) Several values are sent with a type pack (commonly called a tuple): a list of [types](/BlinkBlox/language/types/) separated by commas, inside parentheses. A pack can only be written directly in a `Data` or `Return` field. For example, a pack of different [number types](/BlinkBlox/language/types/#numbers): ```blink event MyTypePackEvent { From: Server, Type: Reliable, Call: SingleAsync, Data: (u8, u16, u32) } ``` Elements may be named. The name is what the parameter is called in the generated code, so a listener reads `function(chefId, dish)` rather than `function(Value1, Value2)`: ```blink event CookRequest { From: Client, Type: Reliable, Call: SingleAsync, Data: (chefId: f64, dish: string) } ``` Naming is optional and may be partial: `(u8, named: string, u16)` is a valid pack, and the elements left unnamed keep their positional `Value1`, `Value3`. A name becomes a parameter of the generated `Fire` and `Invoke`, so it has to be usable as one: `end`, `local` and anything that is not an identifier are refused at compile time (`E3005`), as are two elements sharing a name, and the names those functions already use themselves – `Player`, `Buffer`, `Instances`, `Load` and the like; the compiler says which. That is stricter than a [struct](/BlinkBlox/language/types/#structs) field, which becomes a table key and may be any string. Inside the module the elements travel under positional names, so a name can never collide with the serialisers’ own. ## Usage in Luau [Section titled “Usage in Luau”](#usage-in-luau) The generated module holds one table per event, named after it (inside a [scope](/BlinkBlox/language/scopes/), under the scope’s table). Which members it has depends on the side: | | Sending side | Listening side | | -------------- | --------------------------------------------------- | ----------------------------------- | | `From: Server` | server: `Fire`, `FireAll`, `FireList`, `FireExcept` | client: `On` (or `Iter`), `Predict` | | `From: Client` | client: `Fire` | server: `On` (or `Iter`), `Predict` | `Predict` exists only with [`option Predict`](/BlinkBlox/language/options/#predict). The names follow [`Casing`](/BlinkBlox/language/options/#casing). ### Firing an Event [Section titled “Firing an Event”](#firing-an-event) Client.luau ```luau local Net = require(path.to.Client) Net.CookRequest.Fire(42, "Soup") ``` Server.luau ```luau local Net = require(path.to.Server) Net.MyEvent.Fire(Player, 5) -- one player Net.MyEvent.FireAll(5) -- every player Net.MyEvent.FireList({ A, B }, 5) -- the players in a list Net.MyEvent.FireExcept(Player, 5) -- every player but one Net.MyTypePackEvent.FireAll(2^8 - 1, 2^16 - 1, 2^32 - 1) ``` A value that fails to serialise – see [`WriteValidations`](/BlinkBlox/language/options/#writevalidations) – makes `Fire` throw, and whatever it had written is taken back out of the batch, so the events queued around it are unaffected. ### Listening to an Event [Section titled “Listening to an Event”](#listening-to-an-event) Client.luau ```luau Net.MyEvent.On(function(Value) -- ... end) Net.MyTypePackEvent.On(function(Foo, Bar, FooBar) -- ... end) ``` Server.luau ```luau Net.CookRequest.On(function(Player, chefId, dish) -- ... end) ``` `On` returns a function that disconnects the listener: ```luau local Disconnect = Net.MyEvent.On(Listener) Disconnect() ``` A listener of a `Many` event may disconnect itself, or another listener, from inside a dispatch without the others missing that event. ### Events that arrive before a listener [Section titled “Events that arrive before a listener”](#events-that-arrive-before-a-listener) A reliable event that arrives while nothing is listening is queued, and the queue is handed to the listener the moment one connects – so a listener connected a frame late still sees what was sent before it. An unreliable event with no listener is dropped. The queue holds 256 events. On the server it is filled by clients, so once it is full further events are dropped without a word: an event the game declares but never listens to cannot be used to grow server memory or fill the output. On the client a queue past 256 warns instead, because there it usually means a listener somebody forgot. ### Packets that cannot be decoded [Section titled “Packets that cannot be decoded”](#packets-that-cannot-be-decoded) A packet that fails to decode – truncated, carrying an index no event has, or failing a range – is abandoned at the event that failed, since nothing after that point can be trusted. Events decoded before it in the same packet have already been delivered. The server says nothing about it by default, so that a client sending garbage every frame cannot flood the output. To log it, flag the account or kick, install a handler: Server.luau ```luau Net.SetDecodeErrorHandler(function(Player: Player, Event: string?, Failure: string) Analytics:Record(Player, Event, Failure) end) ``` `Event` is the event the packet claimed to carry, or `nil` if its index named no event at all – or arrived on a remote this side receives nothing on. On the client the handler takes `(Event, Failure)`, and without one the client warns, naming both: from the server, a packet that cannot be decoded is a bug rather than an attack – usually a `Sync` listener that yielded, or a client built from a different schema. On the server the handler is called at most once a second for each player, channel and event, like the rate-limit handler: a client sending a bad packet every frame costs the game one call a second, not one per packet. The server’s own warnings about what a client sent – a malformed packet, an oversized one, too many instances, too many events – keep the same schedule, per player. Either way the handler runs on a thread of its own. An error thrown by a `Sync` listener is not a decode failure. It is raised as an error of its own, the way an `Async` listener’s is, and the rest of the packet is still delivered. ### Iterating an Event (Polling) [Section titled “Iterating an Event (Polling)”](#iterating-an-event-polling) An event declared `Call: Polling` – or any event, under [`option UsePolling`](/BlinkBlox/language/options/#usepolling) – has no listeners. Arriving events wait in a queue, and `Iter` drains it: ```blink event Input { From: Client, Type: Unreliable, Call: Polling, Data: (Direction: vector, Jump: boolean) } ``` Server.luau ```luau RunService.Heartbeat:Connect(function() for Index, Player, Direction, Jump in Net.Input.Iter() do -- ... end end) ``` Client.luau ```luau -- For an event sent by the server, the same loop without the Player. for Index, Value in Net.MyEvent.Iter() do -- ... end ``` Each call to `Iter` returns an iterator that removes what it yields, so a second loop in the same frame sees only what arrived since. `Next` is the same function under its old name, deprecated since 0.14.1 and still generated beside `Iter` so older code keeps working; use `Iter`. On the server a polled event’s queue is filled by clients, so it stops taking events once it holds 256 for each player the server can hold (`256 * Players.MaxPlayers`). A game that iterates its polled events every frame never comes near that; one that leaves an event unpolled no longer lets clients grow it without end. # Functions > Declaring request-and-reply calls between client and server, how they fail, and the limits on calls in flight. Functions are BlinkBlox’s version of Roblox’s `RemoteFunction`. They let one side ask the other a question and wait for the answer. Unlike a `RemoteFunction`, a call always ends: it is answered, or it fails – the listener errored, the call was refused, the player left, or nobody answered within [`InvocationTimeout`](/BlinkBlox/language/options/#invocationtimeout). ## Declaring a function [Section titled “Declaring a function”](#declaring-a-function) Functions are declared with the `function` keyword and a block of fields. ```blink function GetBalance { Yield: Coroutine, Data: u8, Return: u32 } ``` `Yield` is required. `From`, `Data`, `Return`, `Rate`, `Burst` and `Concurrency` are optional. Calls and replies travel on the reliable channel, batched with reliable [events](/BlinkBlox/language/events/#type) and sent once a frame, so a call and its answer each wait for the next flush on their side on top of the network’s own latency. ### `From` [Section titled “From”](#from) Default: `Client` The side that **invokes**. The other side listens and answers. ```blink -- The client asks the server. The default, and what every function did before `From` existed. function GetBalance { Yield: Coroutine, Data: u8, Return: u32 } -- The server asks a client, and names which one. function GetClientSetting { From: Server, Yield: Coroutine, Data: u8, Return: string } ``` Server.luau ```luau local Answer = Net.GetClientSetting.Invoke(Player, 1) ``` Client.luau ```luau Net.GetClientSetting.On(function(Which) return Settings[Which] end) ``` Caution Asking a client a question means trusting a client’s answer, and waiting on one. A client may reply with anything its schema allows, may take as long as it likes, and may never reply at all – in which case the call fails after [`InvocationTimeout`](/BlinkBlox/language/options/#invocationtimeout). Treat the return value as you would any other data from a client, and do not hold anything important – a lock, a queue slot, a request handler – across the call. A client can only answer a call that was made **to it**, for the function it was made for: the server keeps each player’s calls apart, so a reply naming a call that belongs to somebody else, or answering one function with another’s return type, is ignored. ### `Yield` [Section titled “Yield”](#yield) How the caller waits for the answer. * `Coroutine` – the built-in Luau coroutine library. `Invoke` yields and returns the answer, or throws if the call failed. * `Future` – a future library you provide with [`option FutureLibrary`](/BlinkBlox/language/options/#futurelibrary-and-promiselibrary). redblox’s Future library is recommended. `Invoke` returns a future built with `Future.Try`, which completes with a success flag followed by the answer. * `Promise` – a promise library you provide with [`option PromiseLibrary`](/BlinkBlox/language/options/#futurelibrary-and-promiselibrary). evaera’s Promise library, or a fork of it, is recommended. `Invoke` returns a promise that resolves with the answer or rejects if the call failed. Cancelling it gives its slot back. A schema that uses `Future` or `Promise` without the matching option fails to generate. ### `Data` [Section titled “Data”](#data) The data the **caller** sends with the call – the client, unless the function is `From: Server`, in which case it is the server. Any [type](/BlinkBlox/language/types/), or a [type pack](/BlinkBlox/language/events/#type-packs). Omit it when the call carries nothing. ### `Return` [Section titled “Return”](#return) The data the **listener** sends back to the caller – the server, unless the function is `From: Server`, in which case it is the client. Any type or type pack, and omitted when the answer carries nothing. `Data` and `Return` may name their type pack elements the same way – `Data: (id: u8, label: string)` alongside `Return: (id: u16, label: string)` is fine. The generated code keeps the two sets apart, so there is no need to invent distinct names for the same thing going each way. ```blink function Rename { Yield: Coroutine, Data: (id: u8, label: string(1..32)), Return: (id: u8, label: string(1..32)) } ``` ### `Rate`, `Burst` [Section titled “Rate, Burst”](#rate-burst) Limits how often one player may invoke this function, exactly as on [events](/BlinkBlox/language/events/#rate-burst). They apply only to a `From: Client` function – the default. On a `From: Server` function they are ignored with a warning, exactly as on a `From: Server` event: rate limiting applies to inbound traffic, and the server is the party being protected. A refused invocation is answered with a failure rather than dropped, so the caller raises promptly instead of waiting out the timeout. ### `Concurrency` [Section titled “Concurrency”](#concurrency) How many of one player’s calls to this function the server runs at once. `Rate` limits how many calls *start* each second; it says nothing about how many are still *running*. A listener that waits – a DataStore request, an HTTP call, an invocation back to the same client – holds each call open for as long as the wait, so calls arriving at an allowed rate can still pile up into hundreds of suspended threads, each holding whatever its listener holds. A client that sends the requests itself is not held to the [thirty-two calls in flight](#thirty-two-calls-in-flight) an honest client module is. ```blink function SaveLoadout { Yield: Coroutine, Rate: 2, Concurrency: 1, Data: u8[..8], Return: boolean } ``` With `Concurrency: 1`, a second `SaveLoadout` from a player whose first is still saving is refused. It is answered with a failure straight away, like a call refused by `Rate`, and reported through the same [rate-limit handler](/BlinkBlox/language/events/#rate-burst), once a second per player. The place is given back when the call is answered – whether the listener returned, threw, or returned the wrong type – and a call waiting in the queue for a listener that has not connected yet counts as running. When the player leaves, their count goes with them. `Concurrency` is a whole number of at least 1. Like `Rate`, it applies to a `From: Client` function and warns (`W3020`) on a `From: Server` one. ## Usage in Luau [Section titled “Usage in Luau”](#usage-in-luau) The module holds one table per function, named after it. The caller’s side gets `Invoke`, the listener’s side gets `On`; the names follow [`Casing`](/BlinkBlox/language/options/#casing). ### Invoking a Function [Section titled “Invoking a Function”](#invoking-a-function) Client.luau (Coroutine) ```luau local Ok, Balance = pcall(Net.GetBalance.Invoke, 1) ``` Client.luau (Future) ```luau local Success, Balance = Net.GetBalance.Invoke(1):Await() ``` Client.luau (Promise) ```luau Net.GetBalance.Invoke(1) :andThen(function(Balance) -- ... end) :catch(warn) ``` On the server, `Invoke` of a `From: Server` function takes the player to ask first: `Net.GetClientSetting.Invoke(Player, 1)`. ### Listening to a Function [Section titled “Listening to a Function”](#listening-to-a-function) Server.luau ```luau Net.GetBalance.On(function(Player, Account) return Balances[Player][Account] end) ``` The listener receives the calling `Player` first on the server; on a client, answering a `From: Server` function, it receives only the data. Each call runs on a thread of its own, so the listener may yield – a datastore read, say – and whatever it returns is the answer. A function has one listener. `On` returns nothing, and calling it again replaces the listener. Calls that arrive before a listener connects are queued, up to 256 on the server, and run when it does. Past that the server answers each further call with a failure, so the caller’s `Invoke` raises promptly instead of hanging. A client’s queue is filled by the server, and warns once it passes 256 instead. ## When a call fails [Section titled “When a call fails”](#when-a-call-fails) Every failure reaches the caller the same way: a `Coroutine` invoke throws `There was an exception while processing "Name".`, a `Future` completes with `false`, and a `Promise` rejects. What causes it: | Cause | What else happens | | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | | The listener threw | The listening side warns `"Name" encountered an error, ...` with the error. | | The listener returned something its `Return` type does not allow | The same warning. The reply is taken back out of the batch rather than sent half-written. | | The call was refused by `Rate` | Reported through the [rate-limit handler](/BlinkBlox/language/events/#rate-burst). | | The player already had `Concurrency` calls running | Reported through the same handler, with the function’s name. | | The server’s queue for an unconnected listener was full | Nothing is reported. | | Nobody answered within `InvocationTimeout` | The caller’s side warns, naming the function and, on the server, the player. | | The player being asked left the game | Every call outstanding to them fails at once rather than timing out. | A late answer, arriving after its call has already failed, is ignored. Some calls fail before anything is sent, and throw from `Invoke` itself (or reject, or complete with `false`, for the other yield types): * The arguments do not serialise – see [`WriteValidations`](/BlinkBlox/language/options/#writevalidations). * The server invokes a player who is no longer in the game. * Thirty-two calls are already in flight. ### Thirty-two calls in flight [Section titled “Thirty-two calls in flight”](#thirty-two-calls-in-flight) A call is known by a one-byte id that its answer carries back, and at most **32** calls may be outstanding at once – per client module for calls a client makes, and per player for calls the server makes, so asking every player a question at once does not exhaust anything. A thirty-third concurrent call throws `32 calls are already awaiting a response, this call has been dropped.` A slot is given back when its call is answered, fails, times out or – for a promise – is cancelled, so reaching the cap takes thirty-two calls genuinely stuck at once. Without the timeout they would be stuck forever; with it, the worst case is [`InvocationTimeout`](/BlinkBlox/language/options/#invocationtimeout) seconds, ten by default. # Imports > Split a schema across several .blink files, each imported as a scope. An import pulls the declarations of another `.blink` file into this one, inside a [scope](/BlinkBlox/language/scopes/). Use imports to keep one file per feature, or to share a set of types between schemas. ## Importing a file [Section titled “Importing a file”](#importing-a-file) Write `import` and the path of the file, in quotes. The imported file’s declarations land in a scope named after the file: shared/Inventory.blink ```blink type ItemId = u16 struct Item { Id: ItemId, Count: u8, } event ItemAdded { From: Server, Type: Reliable, Call: SingleSync, Data: Item } ``` net.blink ```blink import "./shared/Inventory" event Equip { From: Client, Type: Reliable, Call: SingleSync, Rate: 5, Data: Inventory.ItemId } ``` `Inventory.ItemId` works exactly as it would if `net.blink` had declared `scope Inventory { ... }` itself. **The path is relative to the file that writes the import**, not to the directory you run the compiler from. Absolute paths work too. As on the [command line](/BlinkBlox/getting-started/cli/#compiling), the extension is optional: the compiler tries the path as written, then with `.txt`, then with `.blink`. An import is a declaration, so it goes after the file’s options, among its other declarations. ## Naming the scope [Section titled “Naming the scope”](#naming-the-scope) The scope takes the file’s name without its extension. To choose another, add `as` and a name: ```blink import "./shared/Inventory" as Items struct Loadout { Primary: Items.Item, } ``` The name after `as` is an identifier, written without quotes. `as "Items"` is refused. An import’s scope name follows the same rules as any other declaration’s: it must not collide with a name already in use. Importing two files that are both called `Types.blink`, from different directories, needs `as` on at least one of them. ## Every import is its own copy [Section titled “Every import is its own copy”](#every-import-is-its-own-copy) An import is not deduplicated. Each `import` statement parses its file afresh into its own scope, and everything in that scope is compiled – **events and functions included**. If `net.blink` imports `shared/Inventory.blink` under two names, or imports two files that each import it, the generated modules contain `ItemAdded` twice, as two separate events with their own ids and their own listeners. For types this costs little: two identical type aliases. For events it is almost never what you want. Keep events in files that are imported exactly once, and put what several files share in a file that holds only types. ## Cyclic imports [Section titled “Cyclic imports”](#cyclic-imports) Two files that import each other – directly, or through a chain – are refused: ```text [E3015] Error: Cyclic import ╭─[./b:1:3] │ 001 │ import "./a" ┆ ──┬── ┆ │ ┆ ╰── "./a" is already being imported, so this would never finish │ ────╯ ``` The diagnostic points at the import that closes the cycle. Importing the same file twice through different routes, as described above, is not a cycle, and is allowed. A path that does not resolve to a file is refused with `E3014`, “Unknown require”. ## Options in an imported file [Section titled “Options in an imported file”](#options-in-an-imported-file) Options belong to the file you compile. An imported file may contain its own `option` lines – so that it can also be compiled on its own – but they do not change the build that imports it: the output paths, limits and defaults all come from the entry file. Imported files are checked against their own options The compile-time checks that depend on options – [`RequireRates`](/BlinkBlox/language/options/#defaultrate-requirerates) refusing an inbound event with no rate, and the unreliable size limit refusing an event that cannot fit – run on each file with that file’s options. An inbound event in an imported file is not refused under the entry file’s `option RequireRates = true`. Until that changes, set `RequireRates` in every file you import as well. `DefaultRate` is not affected: the entry file’s default does apply to events from imported files. ## In the Studio plugin [Section titled “In the Studio plugin”](#in-the-studio-plugin) The [Studio plugin](/BlinkBlox/getting-started/studio-plugin/) keeps each schema as a separate file with a name and no directory. An import there names another file saved in the plugin: ```blink import "Inventory" import "Inventory" as Items ``` A path with `./` or a directory in it does not resolve in the plugin. If a schema has to compile in both, keep the imported files next to it on disk and write their names without `./`: `import "Inventory"` means the sibling file `Inventory.blink` to the compiler, and the file saved as `Inventory` to the plugin. ## In the generated modules [Section titled “In the generated modules”](#in-the-generated-modules) Imports act as scopes, so the same rules apply to them: the imported file’s events and functions sit in a nested table, and its types are exported with the scope’s name as a prefix. Server.luau ```luau local Net = require(path.to.Server) Net.Inventory.ItemAdded.Fire(Player, { Id = 12, Count = 1 }) local Id: Net.Inventory_ItemId = 12 local Item: Net.Items_Item = { Id = 12, Count = 1 } ``` # Options > Every option a schema accepts, what it changes in the generated modules, and what it costs. Options configure how BlinkBlox generates your modules: where they are written, what they are called, and how much a client is allowed to make the server do. They go at the top of a schema, before any declaration. ```blink option [OPTION] = [VALUE] ``` A few rules hold for all of them: * **Options come first.** An `option` after the first declaration is refused (`E3016`, “Option set after start of file”). * **Each option is set once.** Setting one twice is refused (`E3024`), unless the two settings sit under different [profiles](/BlinkBlox/language/profiles/): `@profile("dev")` and `@profile("release")` may each set `MaxPacketSize`, but not alongside an unmarked setting of it. * **Values are typed.** A boolean option takes `true` or `false`, a path takes a quoted string, `Casing` takes a bare word, and every **numeric option takes a positive whole number** – `0`, `-5` and `0.5` are all refused (`E2003`). An unknown option name is refused too, with the list of the ones that exist. ## At a glance [Section titled “At a glance”](#at-a-glance) | Option | Value | Default | Section | | --------------------------------- | -------------------------- | ----------------------- | ------------------------------------------------------------------------- | | `ServerOutput`, `ClientOutput` | path | required by the CLI | [Output](#serveroutput-clientoutput-typesoutput) | | `TypesOutput` | path | not generated | [Output](#serveroutput-clientoutput-typesoutput) | | `Typescript` | boolean | `false` | [Output](#typescript) | | `Casing` | `Pascal`, `Camel`, `Snake` | `Pascal` | [Output](#casing) | | `RemoteScope` | string | `""` | [Output](#remotescope) | | `FutureLibrary`, `PromiseLibrary` | Luau path | none | [Output](#futurelibrary-and-promiselibrary) | | `ManualReplication` | boolean | `false` | [Runtime behaviour](#manualreplication) | | `UsePolling` | boolean | `false` | [Runtime behaviour](#usepolling) | | `Predict` | boolean | `false` | [Runtime behaviour](#predict) | | `WriteValidations` | boolean | `false` | [Validation](#writevalidations) | | `SyncValidation` | boolean | `true` | [Validation](#syncvalidation) | | `MaxUnreliableSize` | bytes | `900` | [Validation](#maxunreliablesize) | | `MaxPacketSize` | bytes | `8192` | [Inbound limits](#maxpacketsize-maxeventsperpacket-maxinstancesperpacket) | | `MaxEventsPerPacket` | events | `64` | [Inbound limits](#maxpacketsize-maxeventsperpacket-maxinstancesperpacket) | | `MaxInstancesPerPacket` | instances | `256` | [Inbound limits](#maxpacketsize-maxeventsperpacket-maxinstancesperpacket) | | `InboundBytesPerSecond` | bytes | `8 * MaxPacketSize` | [Inbound limits](#inboundbytespersecond-inboundburst) | | `InboundBurst` | bytes | one second of the above | [Inbound limits](#inboundbytespersecond-inboundburst) | | `DefaultRate` | events a second | none | [Rate limits](#defaultrate-requirerates) | | `RequireRates` | boolean | `false` | [Rate limits](#defaultrate-requirerates) | | `InvocationTimeout` | seconds | `10` | [Invocations](#invocationtimeout) | | `UseColon` | boolean | – | [Deprecated](#usecolon) | ## Output [Section titled “Output”](#output) ### `ServerOutput`, `ClientOutput`, `TypesOutput` [Section titled “ServerOutput, ClientOutput, TypesOutput”](#serveroutput-clientoutput-typesoutput) Where BlinkBlox writes the generated modules. A relative path is resolved against the directory of the schema file, not the directory you run the CLI from. ```blink option TypesOutput = "../Network/Types.luau" option ServerOutput = "../Network/Server.luau" option ClientOutput = "../Network/Client.luau" ``` The [CLI](/BlinkBlox/getting-started/cli/) needs `ServerOutput` and `ClientOutput` and stops without them. The server and client paths always get a `.luau` extension, whatever extension you wrote. `TypesOutput` is optional. When set, BlinkBlox also writes a third, shared module that holds the Luau types of your declarations and the Read/Write pairs of your [exports](/BlinkBlox/language/types/#exports), but no events or functions – useful for code that has to name a schema type on both sides without requiring either network module. The [Studio plugin](/BlinkBlox/getting-started/studio-plugin/) places its output itself and does not read these paths. ### `Typescript` [Section titled “Typescript”](#typescript) Default: `false` Also generate TypeScript declaration files for the server and client modules, for use with roblox-ts. Each `.d.ts` is written beside its module and named after it – `Server.luau` gets `Server.d.ts`, and a module called `init.luau` gets `index.d.ts`. No declaration file is written for `TypesOutput`. ```blink option Typescript = true ``` See [roblox-ts](/BlinkBlox/guides/roblox-ts/) for the full setup. ### `Casing` [Section titled “Casing”](#casing) Default: `Pascal` Values: `Pascal`, `Camel`, `Snake` The casing of the functions BlinkBlox generates. It changes the generated API, not the names of your events, functions or types. ```blink option Casing = Camel ``` | `Pascal` | `Camel` | `Snake` | | ------------------------------------------- | ------------------------------------------- | ---------------------------------------------- | | `Fire`, `FireAll`, `FireList`, `FireExcept` | `fire`, `fireAll`, `fireList`, `fireExcept` | `fire`, `fire_all`, `fire_list`, `fire_except` | | `On`, `Invoke`, `Predict`, `Iter` | `on`, `invoke`, `predict`, `iter` | `on`, `invoke`, `predict`, `iter` | | `Read`, `Write` | `read`, `write` | `read`, `write` | | `StepReplication` | `stepReplication` | `step_replication` | | `SetRateLimitHandler` | `setRateLimitHandler` | `set_rate_limit_handler` | | `SetDecodeErrorHandler` | `setDecodeErrorHandler` | `set_decode_error_handler` | ### `RemoteScope` [Section titled “RemoteScope”](#remotescope) Default: `""` A prefix for the two remotes this schema creates. With `"PACKAGE"` they are named `PACKAGE_BLINK_RELIABLE_REMOTE` and `PACKAGE_BLINK_UNRELIABLE_REMOTE`; without it, `BLINK_RELIABLE_REMOTE` and `BLINK_UNRELIABLE_REMOTE`. ```blink option RemoteScope = "PACKAGE" ``` Every generated module registers its scope in `_G._BLINK` when it is required, and **errors** if a module with the same scope is already running in that Luau VM. So two schemas in one game – a library that ships its own networking inside your game’s, for instance – need different scopes. The scope is already part of the remote names, so it is not part of the [schema signature](#schema-signatures): a client with the wrong scope never finds the remote at all. ### `FutureLibrary` and `PromiseLibrary` [Section titled “FutureLibrary and PromiseLibrary”](#futurelibrary-and-promiselibrary) A [function](/BlinkBlox/language/functions/#yield) declared `Yield: Future` or `Yield: Promise` needs the library it yields through. The value is pasted into a `require(...)` at the top of the server and client modules, so it is a Luau expression rather than a file path. The modules define `ReplicatedStorage` themselves, so you can start from it. ```blink option FutureLibrary = "ReplicatedStorage.Packages.Future" option PromiseLibrary = "ReplicatedStorage.Packages.Promise" ``` Without it, a schema that uses the yield type fails to generate, and an empty string counts as without it: `Cannot use yield type: "Future", without providing a path to the future library.` The `require` is emitted only into a module that invokes such a function. The side that answers the call never touches the library, so it does not load it. ## Runtime behaviour [Section titled “Runtime behaviour”](#runtime-behaviour) ### `ManualReplication` [Section titled “ManualReplication”](#manualreplication) Default: `false` Reliable events and function calls are not sent the moment you fire them. Each side batches them into one buffer (the server keeps one per player) and sends the batch once a frame, from `RunService.Heartbeat`. Unreliable events are the exception: each one is sent at once, in a packet of its own. `ManualReplication = true` removes that Heartbeat connection, and nothing is sent until you call `StepReplication` yourself. The function is exported either way; with this option it is the only thing that flushes. ```blink option ManualReplication = true ``` Server.luau ```luau local RunService = game:GetService("RunService") local Net = require(path.to.Server) RunService.PostSimulation:Connect(function() -- ... fire this frame's events ... Net.StepReplication() end) ``` ### `UsePolling` [Section titled “UsePolling”](#usepolling) Default: `false` Receive every event through the polling API, as if each had been declared `Call: Polling`. The `Call` field is still required on each event, and its value is ignored. See [polling](/BlinkBlox/language/events/#iterating-an-event-polling). ```blink option UsePolling = true ``` ### `Predict` [Section titled “Predict”](#predict) Default: `false` Adds a `Predict` entry point to every event on the side that listens to it with `On`. It hands the event straight to that side’s own listeners without touching a remote. ```blink option Predict = true event Damaged { From: Server, Type: Reliable, Call: ManyAsync, Data: u8 } event Jumped { From: Client, Type: Reliable, Call: SingleSync, Data: u8 } ``` Server.luau ```luau -- An event declared `From: Client` is listened to on the server, so Predict lives there. Net.Jumped.Predict(Player, 5) ``` Client.luau ```luau -- An event declared `From: Server` is listened to on the client. Net.Damaged.Predict(5) ``` It takes exactly what a listener receives and is delivered the way an arriving event is: to every listener of a `Many` event, and, for a reliable event with no listener connected yet, into the queue that is handed over when one connects. It does not pass through the event’s rate limit – nothing arrived from a client. Two uses. Tests can drive receiving code without standing up a network, and Roblox stories – which run in edit mode, where no remotes exist – can exercise UI that reacts to events. A polled event has no `Predict`, since it has no listeners to hand the event to. Caution Off by default because it is an entry point into your listeners that does not exist on the wire. Enable it where you want it – a `@profile("dev")` or `@profile("test")` setting is a good fit – and leave it off in builds where you do not. ## Validation [Section titled “Validation”](#validation) ### `WriteValidations` [Section titled “WriteValidations”](#writevalidations) Default: `false` Check what you pass to `Fire` and `Invoke` before it is written: that each value has the right Luau type, that numbers, string lengths, buffer sizes, array lengths and vector magnitudes are inside their [ranges](/BlinkBlox/language/types/#ranges), and that an `Instance` is of its declared class. Helpful while developing; it costs a comparison per field, so it is commonly enabled only in development builds. ```blink @profile("dev") option WriteValidations = true ``` What it does **not** switch is anything the receiver relies on. Whatever this option says: * The **receiving** side always checks ranges and Instance classes on what it decodes, and drops a packet that fails. * A string, buffer or array length, or a map’s entry count, **outside its bounds** is always refused on send. The length prefix is sized from the bounds, so such a value would not fit it: it would go out with a wrapped length and every event behind it would decode from the wrong offset. * An enum value or tagged-enum variant that does not exist always errors on send. ### `SyncValidation` [Section titled “SyncValidation”](#syncvalidation) Default: `true` Reports a `Sync` listener that yielded. It is worth knowing exactly when it fires, because the obvious guess is wrong. BlinkBlox does **not** notice at the moment your listener yields. It notices when the **next packet arrives** and finds the previous decode still suspended inside one. So a listener that yields with no further traffic behind it is never reported – which is why adding a yield to a `SingleSync` callback and seeing no error is a correct observation rather than a broken check. Caution When it does fire, the suspended decode is **closed**, and every event still unread in that earlier packet is discarded. Your listener is not resumed and those events never arrive. The warning says so: `Event "Name" yielded in a Sync call, so the rest of the packet it arrived in was discarded.` This is not a style warning. A `Sync` listener runs on the decode thread, so yielding in one stops the packet where it stands. If a listener needs to yield, it needs an [Async call](/BlinkBlox/language/events/#call). The check costs one `coroutine.status` per packet, which is why it is on by default. ### `MaxUnreliableSize` [Section titled “MaxUnreliableSize”](#maxunreliablesize) Default: `900` bytes The largest payload an unreliable event may carry, counting its one-byte event id and, for `OrderedUnreliable`, its two-byte sequence number. Roblox drops an `UnreliableRemoteEvent` payload past roughly this size. It does so silently, and only under the conditions that produce a large packet, so the failure shows up in production for some players and never in testing. BlinkBlox checks this twice. At compile time it measures every unreliable event: one that can **never** fit is a hard error (`E3018`), and one that merely **might not** is a warning (`W3019`) naming the field that has no upper bound. At runtime the send path checks the finished buffer, and drops an event that is too large with a warning naming it rather than letting the packet vanish. ```blink option MaxUnreliableSize = 900 ``` ## Inbound limits [Section titled “Inbound limits”](#inbound-limits) These bound what a single client can make the **server** do, and all of them are checked before a packet is decoded. Only inbound server traffic is checked: the client trusts the server by construction, and capping what the server sends would drop legitimate replication. The [securing the server](/BlinkBlox/guides/securing-the-server/) guide puts them together with the rest. ### `MaxPacketSize`, `MaxEventsPerPacket`, `MaxInstancesPerPacket` [Section titled “MaxPacketSize, MaxEventsPerPacket, MaxInstancesPerPacket”](#maxpacketsize-maxeventsperpacket-maxinstancesperpacket) Defaults: `8192` bytes, `64` events, `256` instances Bound what a single inbound packet can make the server do. The server decodes a client’s buffer in a loop, one event per iteration. Without a limit, a client that sends `buffer.create(1000000)` buys up to a million decode-and-dispatch cycles inside one remote callback – enough to stall or crash the server. A packet larger than `MaxPacketSize`, or carrying more instance references than `MaxInstancesPerPacket`, is dropped before any of it is parsed. Once `MaxEventsPerPacket` events have been decoded from one packet, the rest is discarded. An unrecognised event id also stops the parse: the cursor is no longer at an event boundary, so nothing after it can be trusted. ```blink option MaxPacketSize = 16384 option MaxEventsPerPacket = 128 option MaxInstancesPerPacket = 512 ``` Before any of that, both remotes check that what arrived is a buffer and a table at all – a client can call `FireServer` with any arguments it likes. Each kind of refusal warns at most once a second per player, so a client sending garbage every frame cannot flood the output. ### `InboundBytesPerSecond`, `InboundBurst` [Section titled “InboundBytesPerSecond, InboundBurst”](#inboundbytespersecond-inboundburst) Defaults: `8 * MaxPacketSize` (65536) bytes a second, and a burst of one second of that Bound how many bytes a second each player’s packets may cost the server, before any of them is decoded. The limits above are per packet. A client firing hundreds of small, well-formed packets a frame passes every one of them, and the server would decode them all. The budget is a token bucket per player, shared by the reliable and the unreliable remote: each packet costs its size in bytes, and never less than `128`, so a flood of tiny packets is not free. A packet that finds the bucket short is dropped whole and unread. ```blink option InboundBytesPerSecond = 131072 option InboundBurst = 131072 ``` At the defaults that is eight full-size packets a second, or 512 small ones. The default rate follows `MaxPacketSize`, so raising that raises the budget with it. `InboundBurst` must be at least `MaxPacketSize` (`E2003` otherwise), or the largest packet could never be afforded however long the client waited. The burst is a whole second because the clock is the server’s: after a network hitch, Roblox delivers the packets that queued up together, and a refused reliable packet takes every event in it along. Caution Every unreliable `Fire` is a packet of its own. A client that fires more than about seven unreliable events a frame will reach the default budget; raise `InboundBytesPerSecond` for it. Refusals are reported the way refused events are: a warning at most once per player per second, and a call to [`SetRateLimitHandler`](/BlinkBlox/language/events/#rate-burst)’s handler with `Event` set to `nil`. ## Rate limits [Section titled “Rate limits”](#rate-limits) ### `DefaultRate`, `RequireRates` [Section titled “DefaultRate, RequireRates”](#defaultrate-requirerates) Defaults: none, `false` Rate limiting for inbound events and functions. Per-event limits are declared on the event itself with [`Rate` and `Burst`](/BlinkBlox/language/events/#rate-burst). `DefaultRate` supplies a rate, in events per second per player, for every inbound event and function that does not declare one. `RequireRates` turns a missing rate into a compile error (`E3020`) – the flag a project sets so that a forgotten limit fails the build instead of shipping. With a `DefaultRate`, nothing is missing. ```blink option DefaultRate = 30 option RequireRates = true ``` Unlike an event’s own `Rate`, which may be fractional, `DefaultRate` is a whole number like every numeric option. The packet limits, the inbound budget and this are different axes and none replaces the others. `MaxEventsPerPacket` bounds the work one packet can cause; the inbound budget bounds how many bytes of packets a second each player gets decoded at all; the event bucket bounds events per second, per player, per event, and so the game logic behind each listener. ## Invocations [Section titled “Invocations”](#invocations) ### `InvocationTimeout` [Section titled “InvocationTimeout”](#invocationtimeout) Default: `10` seconds How long an [`Invoke`](/BlinkBlox/language/functions/) waits for its answer before failing. A call that is never answered would otherwise wait forever, and it costs more than the one hung thread: each outstanding invocation holds one of thirty-two slots, and a call that never returns never gives its slot back. Thirty-two of those and every later `Invoke` is refused outright. When the timeout fires, the caller is failed the same way an errored handler fails it – so a `pcall` around `Invoke`, or a rejected promise, catches it – the slot is returned, and BlinkBlox warns naming the function. The timer belongs to that one call: a call answered in time cancels it, so it can never fail a later call, and an answer that arrives after the timeout is ignored rather than handed to whichever call came next. ```blink option InvocationTimeout = 30 ``` ## Deprecated [Section titled “Deprecated”](#deprecated) ### `UseColon` [Section titled “UseColon”](#usecolon) Accepted and ignored: no generator has read it since it was added. It warns (`W3017`, `Option "UseColon" is deprecated`) and will be removed in 1.0. Delete the line. ## Schema signatures [Section titled “Schema signatures”](#schema-signatures) Not an option – it is always on, and there is nothing to configure. It is documented here because you will meet its error message. Every generated pair carries a short signature of the schema it was built from. The server publishes it on the reliable remote; the client checks it before anything else and **errors on require** if they disagree: ```plaintext [BlinkBlox]: This client was built from a different schema than the server (client 18198d53..., server 64b832be...). Recompile both sides from the same .blink file. ``` This exists because the alternative failure is silent. Event ids are positional – the first event declared is `0`, the next is `1` – so inserting an event renumbers every event after it. A client one build behind then reads index `7` as whatever index `7` means now and dispatches one event’s payload to another’s listener. Nothing errors. The usual symptoms are `invalid argument #3 to 'writeu8'` or a listener that receives data it cannot make sense of. A [profile](/BlinkBlox/language/profiles/) changes which declarations are compiled, so the client and the server must be built with the same one. If you see the “did not publish a schema signature” variant, the server is running a build from before this check existed, or something other than BlinkBlox created a remote with the same name. It is a safety check, not a security one: the client owns its copy of the module and can edit the constant out. It catches drift between two builds, which is a mistake, not an attack. # Profiles > Compile parts of a schema only in some builds, so debug remotes and development options never reach the shipped game. A profile decides which parts of a schema a build compiles. Marking a statement with `@profile` compiles it only when that profile is active. This keeps debug tooling out of the game you ship without keeping a second schema file. ```blink -- Validate writes while developing; trust your own code in the shipped game. @profile("dev") option WriteValidations = true -- A debug remote that must never reach a live server. @profile("dev") event GiveMoney { From: Client, Type: Reliable, Call: SingleSync, Data: u32 } event Ping { From: Server, Type: Reliable, Call: SingleSync, Data: u8 } ``` A `release` build of this schema compiles `Ping` alone, with `WriteValidations` off. A `dev` build compiles both events, with `WriteValidations` on. Why it matters for the server: every event a client can send is surface a client can reach. A debug remote left in a shipped schema is a remote any exploiter can call, rate limited or not. A remote that is not compiled does not exist. There are four profiles: `dev`, `debug`, `test` and `release`. A statement without `@profile` is compiled under every one of them. ## Choosing a profile [Section titled “Choosing a profile”](#choosing-a-profile) On the [command line](/BlinkBlox/getting-started/cli/#flags), pass `--profile` (or `-p`): ```sh blinkblox net --profile dev ``` In the Studio plugin, set a string attribute named `Profile`, with one of the four names as its value, on the schema’s file in `ServerStorage.BLINK_CONFIGURATION_FILES`. See the [Studio plugin](/BlinkBlox/getting-started/studio-plugin/) page. The default is release A build without a profile is compiled as `release`. That is deliberate: a statement marked `dev` is one somebody meant to keep out of the shipped game, so a build that forgot the flag leaves it out rather than ships it. Upstream Blink defaults to `dev`; this is one of the places the fork differs. ## What can be marked [Section titled “What can be marked”](#what-can-be-marked) `@profile` goes in front of a type (`type`, `struct`, `enum`, `map`, `set`), an `event`, a `function`, a `scope`, an `import` or an `option`. Put it on its own line or on the same line as the statement; either works. **Everything inside a marked scope or import follows it.** A statement inside is compiled only if its scope is. Marking a statement inside an excluded scope with the active profile does not bring it back. ```blink @profile("dev") scope Debug { event Teleport { From: Client, Type: Reliable, Call: SingleSync, Data: vector } event Kill { From: Client, Type: Reliable, Call: SingleSync, Data: u8 } } ``` **An excluded import is still read.** A statement that is left out is still parsed and its names still registered, so the file an excluded `import` names must exist and must parse. What exclusion does is keep the declarations out of the generated modules. ## Rules [Section titled “Rules”](#rules) * **A compiled declaration cannot use an excluded one.** If `Ping` is compiled and uses a type marked `dev`, a `release` build is refused with `E3027` rather than quietly carrying the type into the build. Mark both, or neither: Refused under release: Ping uses a dev-only type ```blink @profile("dev") struct Money { Amount: u32, } event Ping { From: Server, Type: Reliable, Call: SingleSync, Data: Money } ``` The reverse is fine: a declaration marked `dev` may use anything that is always compiled. * **Names stay unique across profiles.** Two declarations with the same name are a duplicate, even under different profiles. You cannot declare `GiveMoney` once for `dev` and again, differently, for `release`. * **An option may be set once per profile, and once without one.** `@profile("dev")` and `@profile("release")` may each set `MaxPacketSize`. Setting it both with and without a profile is refused under every profile, so a schema is valid in all builds or in none: Refused: set with and without a profile ```blink option MaxPacketSize = 4096 @profile("dev") option MaxPacketSize = 1024 event Ping { From: Server, Type: Reliable, Call: SingleSync, Data: u8 } ``` * **A statement takes one profile.** Two `@profile` attributes on the same statement are refused (`E3028`). To compile a statement under two profiles but not the others, there is no syntax; leave it unmarked, or declare it under one. * **A misspelt profile is an error** (`E3026`), not a statement that silently never compiles. The attribute’s name is checked too: `@profile` is the only one, and anything else is `E3025`. * **An attribute needs a statement after it.** One at the end of a file, or in front of a closing brace, is refused (`E3028`). ## The client and the server [Section titled “The client and the server”](#the-client-and-the-server) A single compile writes the client and the server module from the same profile, so they always agree. They only disagree if you mix modules from two builds – a `dev` client with a `release` server. Their [schema signatures](/BlinkBlox/language/options/#schema-signatures) then differ, and the client module raises when it is required, rather than decoding one event as another. A schema with no `@profile` anywhere has the same signature under every profile, so the flag only matters where the schema uses it. # Scopes > Group related types, events and functions under a name, in the schema and in the generated modules. A scope groups declarations under a name. Inside the schema it is a namespace; in the generated modules it becomes a nested table and a prefix on type names. Use scopes to keep a large schema organised by feature – `Shop`, `Combat`, `Admin` – rather than as one flat list. ## Defining a scope [Section titled “Defining a scope”](#defining-a-scope) Write `scope`, a name, and the declarations inside braces. A scope may hold types, events, functions and other scopes: ```blink type Coins = u32 scope Shop { type ItemId = u16 struct Offer { Item: ItemId, Price: Coins, } event OffersChanged { From: Server, Type: Reliable, Call: SingleSync, Data: Offer[..32] } scope Admin { event SetPrice { From: Client, Type: Reliable, Call: SingleSync, Rate: 1, Data: Offer } } } struct Receipt { Item: Shop.ItemId, Paid: Coins, } ``` [Options](/BlinkBlox/language/options/) cannot go in a scope: they apply to the whole schema and must come before any declaration. ## Referring to names [Section titled “Referring to names”](#referring-to-names) **Inside a scope, everything declared around it is visible.** `Offer` uses `ItemId` from its own scope and `Coins` from the top level without qualifying either, and `SetPrice` in the nested `Admin` scope uses `Offer` the same way. **Outside a scope, qualify the name with the scope’s.** `Receipt` writes `Shop.ItemId`. A nested scope takes one qualifier per level: `Shop.Admin.SomeType`. An unqualified name from inside a scope is an unknown reference outside it. ## Names must not collide [Section titled “Names must not collide”](#names-must-not-collide) A scope is not a way to reuse a name. A declaration inside a scope may not repeat a name already visible from it – one declared at the top level, or in any scope around it: Refused: T is already declared at the top level ```blink type T = u8 scope Inner { type T = u16 } ``` Sibling scopes are separate, so `Shop.ItemId` and `Inventory.ItemId` can both exist, and so can two events named `Changed` in two different scopes. A scope’s name is declared once. Writing `scope Shop { ... }` a second time is a duplicate declaration, not a continuation of the first. ## In the generated modules [Section titled “In the generated modules”](#in-the-generated-modules) A scope becomes a nested table of the module, holding its events and functions. Its types are exported with the scope’s names joined to the type’s by underscores: Server.luau ```luau local Net = require(path.to.Server) Net.Shop.OffersChanged.FireAll({ { Item = 1, Price = 250 }, }) Net.Shop.Admin.SetPrice.On(function(Player: Player, Offer: Net.Shop_Offer) -- ... end) local Id: Net.Shop_ItemId = 1 ``` A type in a nested scope takes every level: an `ItemId` in `Shop.Admin` is exported as `Net.Shop_Admin_ItemId`. Caution The flattened names can collide with top-level ones. A top-level `type Shop_ItemId` next to `scope Shop { type ItemId }` passes the name check above – in the schema they are different names – but both are exported as `Shop_ItemId`, and the module fails to type-check. Avoid underscores in names that could meet a scope prefix. [Imports](/BlinkBlox/language/imports/) are scopes too: an imported file’s declarations land in a scope named after the file, and everything on this page applies to them. # Types > Every type the schema language supports, how to bound it, and exactly what it costs on the wire. This page covers every type BlinkBlox supports: what it becomes in Luau, how to constrain it, and how many bytes it costs to send. If anything is missing or wrong, please [open an issue](https://github.com/XopoIII/BlinkBlox/issues). ## Size on the wire [Section titled “Size on the wire”](#size-on-the-wire) Every type below is written into a buffer, except `Instance` and `unknown`, which travel beside it in the remote’s instance list. The compiler measures these sizes itself – it is how it refuses an unreliable event that cannot fit in [`MaxUnreliableSize`](/BlinkBlox/language/options/#maxunreliablesize). | Type | Bytes | | ---------------------------- | ---------------------------------------------------------------------------------------- | | `u8`, `i8` | 1 | | `u16`, `i16`, `f16` | 2 | | `u32`, `i32`, `f32` | 4 | | `f64` | 8 | | `boolean` | 1 **bit**, [shared](#booleans-and-optionals-share-a-bitfield) with neighbouring booleans | | `T?` | 1 bit, plus `T` when present. `Instance?` costs nothing extra | | `string(N)`, `buffer(N)` | `N` – an exact length has no prefix | | `string`, `buffer` | [length prefix](#lengths-are-offset-encoded) (1, 2 or 4) + the bytes | | `T[N]` | `N` elements – no prefix | | `T[]`, `T[A..B]` | length prefix + each element | | `boolean[]` | length prefix + one byte per eight elements | | `map` | 2 (the entry count) + each key and value | | `set` | 1 for up to 8 flags, 2 for up to 16, 4 for up to 32; another group per further 32 | | `enum` | 1 | | tagged `enum` | 1 + the variant’s fields | | `struct`, type pack | the sum of the fields | | `vector` | 3 components: 12 as `f32` (the default) | | `CFrame` | 3 position + 3 rotation components: 24 by default | | `CFrame` | 3 position components + 7: 19 by default | | `Color3` | 3 | | `BrickColor` | 2 | | `DateTime`, `DateTimeMillis` | 8 | | `Instance`, `unknown` | 0, and one slot in the instance list | Each event adds one byte for its id, and an `OrderedUnreliable` event two more for its sequence number. A function call adds the same id byte and one more naming the call; its answer carries both and a success flag. The [bandwidth guide](/BlinkBlox/guides/bandwidth/) covers how to spend fewer of them. ### Booleans and optionals share a bitfield [Section titled “Booleans and optionals share a bitfield”](#booleans-and-optionals-share-a-bitfield) A `boolean`, and the presence flag of an optional (`T?`), cost one **bit**. Consecutive ones in the same block share a byte, eight to a byte: a struct with eight boolean fields costs one byte, and an event whose type pack is `(boolean, u8?, boolean)` spends one byte on its three bits, followed by the `u8` when it is present. A block here is one run of generated code. The fields of a struct share the block they sit in, as do the elements of a type pack. A new block begins inside each array element, each map entry, each tagged-enum variant and the payload of each optional, so bits never share a byte across those boundaries. That is why `boolean?` costs a bit and then a whole byte when it is present: the value sits in the optional’s payload, not beside its flag. Arrays of plain booleans are the exception – they are [packed](#arrays-of-booleans) eight elements to a byte. ### Lengths are offset-encoded [Section titled “Lengths are offset-encoded”](#lengths-are-offset-encoded) A variable length – of a string, a buffer or an array – is sent as a prefix, in the smallest unsigned integer that holds the **span** of its range, and **relative to its minimum**: | Span (`max - min`) | Prefix | | ------------------- | ------- | | 0 (an exact length) | none | | up to 255 | 1 byte | | up to 65535 | 2 bytes | | more | 4 bytes | So `string(300..400)` spends one byte on its hundred possible lengths rather than the two that 400 alone would need, and a length below the minimum cannot be written at all. An unbounded `string`, `buffer` or array is treated as `0..65535`: a 2-byte prefix, and a value longer than 65535 is refused on send. A map’s entry count is always a 2-byte `u16`. A half-open length runs to the limit of the type. For an array that is 65535, but for a string or a buffer it is 4294967295, so `string(1..)` takes a 4-byte prefix where a bare `string` takes two. Give lengths an upper bound. On receipt, a length is checked against its range, and against the bytes left in the packet, before anything is read or allocated for it – so a hostile length prefix buys neither. ## Ranges [Section titled “Ranges”](#ranges) Ranges can be used with [numbers](#numbers), [strings](#strings), [buffers](#buffers), [vectors](#vectors) and [arrays](#arrays) to limit the values they can represent. ### Full Ranges [Section titled “Full Ranges”](#full-ranges) A full range has both a minimum and a maximum: `0..100`. ### Half Ranges [Section titled “Half Ranges”](#half-ranges) A half range gives only one side: `0..` or `..100`. The open side is the limit of the type – `255` for a `u8`, unbounded for a float. ### Exact Ranges [Section titled “Exact Ranges”](#exact-ranges) An exact range has a single value, such as `0` or `100`. On a length it means exactly that length. ### Examples [Section titled “Examples”](#examples) | Range | Min | Max | | -------- | ------------------ | ------------------ | | `0..100` | `0` | `100` | | `0..` | `0` | the type’s maximum | | `..100` | the type’s minimum | `100` | | `0` | `0` | `0` | | `100` | `100` | `100` | A range must fit its type (`u8(0..300)` is refused), and a range on an integer, or on a length, must use whole numbers. Floats accept negative and fractional bounds: `f32(-0.5..0.5)`. The **receiving** side always checks a value against its range, and abandons the packet at that event if it fails; a ranged float refuses `NaN` too. The sending side checks only under [`WriteValidations`](/BlinkBlox/language/options/#writevalidations) – except for lengths, where a value outside the range would corrupt the packet, so those are always checked. ## Numbers [Section titled “Numbers”](#numbers) BlinkBlox supports every number type the buffer library implements, plus a half-precision float. Number types start with a prefix – `u`, `i` or `f` – followed by the number of bits used to represent the number. The number of bits is also what it costs to send. ### Unsigned Integers [Section titled “Unsigned Integers”](#unsigned-integers) Whole numbers greater than or equal to zero. | Name | Size | Min | Max | | ----- | --------- | --- | --------------- | | `u8` | `1 byte` | `0` | `255` | | `u16` | `2 bytes` | `0` | `65,535` | | `u32` | `4 bytes` | `0` | `4,294,967,295` | ### Signed Integers [Section titled “Signed Integers”](#signed-integers) Whole numbers, positive or negative. | Name | Size | Min | Max | | ----- | --------- | ---------------- | --------------- | | `i8` | `1 byte` | `-128` | `127` | | `i16` | `2 bytes` | `-32,768` | `32,767` | | `i32` | `4 bytes` | `-2,147,483,648` | `2,147,483,647` | Caution An integer type without a range does not check its value on send unless [`WriteValidations`](/BlinkBlox/language/options/#writevalidations) is on. A fraction is truncated towards zero, and a value outside the type wraps: `300` sent as a `u8` arrives as `44`, and `-1` as `255`. Give an integer a range when its limits matter – the receiver then refuses anything outside it. ### Floating points [Section titled “Floating points”](#floating-points) Floating points represent numbers with a fractional part. The bit size of a float does not set a hard limit on its value so much as its precision. The table lists the largest integer each type represents exactly. | Name | Size | Exact integers up to | Largest value | | ----- | --------- | ----------------------- | --------------- | | `f16` | `2 bytes` | `2,048` | `65,504` | | `f32` | `4 bytes` | `16,777,216` | about `3.4e38` | | `f64` | `8 bytes` | `9,007,199,254,740,992` | about `1.8e308` | A range on a float may go past the exact-integer limit, and an open side of one, as in `f32(0..)`, is not bounded at all. A value past `65,504` sent as an `f16` arrives as infinity. Every float type carries `NaN`, both infinities and `-0`, though a ranged float refuses `NaN`. ### Bounding Numbers [Section titled “Bounding Numbers”](#bounding-numbers) Bound a number by placing [a range](#ranges) in parentheses after the type. ```blink type Health = u8(0..100) type Damage = u8(..100) type Aim = f32(-1..1) type UserId = f64 ``` ## Strings [Section titled “Strings”](#strings) Luau’s text container, declared as `string`. ### Bounding Strings [Section titled “Bounding Strings”](#bounding-strings) Bound a string’s length, in bytes, by placing [a range](#ranges) in parentheses after the type. ```blink type UUID = string(36) type Username = string(3..20) ``` `string(36)` is exactly 36 bytes and sends no length at all. A bounded length takes the prefix [described above](#lengths-are-offset-encoded) – one byte for `3..20` – and an unbounded `string` takes two and refuses anything past 65535 bytes. Caution An exact length is checked on send only under [`WriteValidations`](/BlinkBlox/language/options/#writevalidations). Without it, a longer string is cut to the declared length and a shorter one fails to write. ## Booleans [Section titled “Booleans”](#booleans) `true` or `false`, declared as `boolean`. A boolean costs one bit; see [the bitfield](#booleans-and-optionals-share-a-bitfield). ```blink type Success = boolean ``` ## Buffers [Section titled “Buffers”](#buffers) Declared as `buffer`. A buffer lets you pass your own serialised data while still taking advantage of BlinkBlox’s batching. ### Bounding Buffers [Section titled “Bounding Buffers”](#bounding-buffers) Bound a buffer’s size, in bytes, by placing [a range](#ranges) in parentheses after the type. Sizes are sent like string lengths. ```blink type BinaryBlob = buffer type Chunk = buffer(..800) ``` ## Vectors [Section titled “Vectors”](#vectors) A vector in 3D space, most often a position. Declared as `vector`, it is a `Vector3` in Luau. ### Bounding Vectors [Section titled “Bounding Vectors”](#bounding-vectors) Bound a vector’s length (magnitude) by placing [a range](#ranges) in parentheses after the type. For example, a direction whose length is between `0` and `1`: ```blink type Direction = vector(0..1) ``` A magnitude is never negative, so the range cannot be either. ### Specifying Encoding [Section titled “Specifying Encoding”](#specifying-encoding) Pass a [number type](#numbers) in angle brackets to choose how each of the three components is sent. The default is `f32`, 12 bytes; `vector` is 6. ```blink type Position = vector type GridCell = vector type CompactOffset = vector ``` Caution Luau stores vectors as three `f32`s internally, so an encoding larger than `f32` (such as `f64`) adds bytes and no precision. An integer encoding truncates each component and wraps outside its range, as [integers do](#signed-integers). ## Optionals [Section titled “Optionals”](#optionals) Make a type optional by appending `?` after the **entire type**: ```blink type Username = string(3..20)? type MaybeTarget = Instance(Player)? ``` An absent value costs one bit; a present one costs that bit plus the value. An optional `Instance` costs nothing extra: its absence is read from the instance list itself. `unknown` cannot be optional – it has no way to encode absence – and neither can the key or the value of a [map](#maps). ## Arrays [Section titled “Arrays”](#arrays) A list of values of one type, written as the type followed by square brackets. An array of strings: ```blink type Names = string[] ``` ### Bounding Arrays [Section titled “Bounding Arrays”](#bounding-arrays) Bound an array’s length by placing [a range](#ranges) inside the brackets: ```blink type Party = string(3..20)[1..8] type RecentIds = f64[..50] type Corners = vector[4] ``` `[4]` is exactly four elements and sends no length. Like an exact-length string, a longer array is refused on send under [`WriteValidations`](/BlinkBlox/language/options/#writevalidations) and cut to its first elements without it. A variable length is sent like a string’s, and an array holds at most 65535 elements. Before allocating, the receiver checks the length against what the rest of the packet can hold, so a length prefix of 65535 on a two-byte packet buys nothing. In an array of optionals, an absent element arrives in its own place: `u8?[]` sent as `{1, nil, 3}` arrives as `{1, nil, 3}`, not closed up. ### Arrays of booleans [Section titled “Arrays of booleans”](#arrays-of-booleans) An array of plain booleans is packed eight elements to a byte, after its length: `boolean[1000]` costs 125 bytes rather than 1000. An array of optional booleans, `boolean?[]`, is not packed, since each element carries a presence flag of its own as well as its value. ### Elements that cost nothing [Section titled “Elements that cost nothing”](#elements-that-cost-nothing) An array whose elements send no bytes and no instances – an empty struct, say – gives the decoder nothing to run out of, so a two-byte count would buy 65535 iterations. Such an array must have a range; without one it is refused (`E3021`). The same holds for a map whose keys and values both cost nothing. ## Maps [Section titled “Maps”](#maps) Key-value tables, with keys of one type and values of the same or another type. Declared with the `map` keyword. A map of `string` keys to `f64` values: ```blink map StringToNumber = { [string]: f64 } map UserIdToUsername = { [f64]: string } ``` ### What can be a key [Section titled “What can be a key”](#what-can-be-a-key) A key has to survive the round trip **as a key**. `string`, the number types, `boolean` and a plain `enum` all decode to the same value the sender had, so any of them works. A struct, a tagged enum, another map, a set, an array or a type pack does not. Each decodes to a **fresh table** for every entry, and in Luau a table is its own identity, so the receiving side ends up holding `[Table(0x...)] = value` – entries it can reach only by iterating, never by looking anything up. Every byte round-trips correctly, which is what makes it so easy to miss. BlinkBlox refuses those keys at compile time (`E3001`): Refused ```blink -- Each decoded key is a table nothing can match. map Grid = { [struct { X: u8, Y: u8 }]: u8 } ``` If you need a composite key, put the parts in the value and key the map by something simple: ```blink map Grid = { [u16]: struct { X: u8, Y: u8, Tile: u8 } } ``` ### Maps cannot be bounded [Section titled “Maps cannot be bounded”](#maps-cannot-be-bounded) Unlike [arrays](#bounding-arrays), a map takes no range. Its entry count is written as an unconditional `u16`, so every map is unbounded by construction – up to 65535 entries – and a decoder will loop over whatever count the packet names. For an inbound event whose size you need to reason about, prefer a bounded array of key-value structs. It costs the same bytes, states its ceiling in the schema, and the decoder checks the length before it allocates. ### Generics [Section titled “Generics”](#generics) Maps support generics, which are a tool for reuse. A map template: ```blink map Map = { [K]: V } map StringToNumber = Map ``` ## Sets [Section titled “Sets”](#sets) A fixed set of named flags, each `true` or `false`. Declared with the `set` keyword: ```blink set Flags = { FeatureA, FeatureB, FeatureC } ``` In Luau a set is a table with every flag as a key: `{ FeatureA: boolean, FeatureB: boolean, ... }`. On the wire the flags are packed into groups of up to 32 – one byte for up to 8 flags, two for up to 16, four for up to 32 – so the three flags above cost one byte. ## Enums [Section titled “Enums”](#enums) BlinkBlox supports two kinds of enum: unit enums and tagged enums. Either kind travels as a one-byte index, so an enum holds between 1 and 256 values or variants. An empty one, or one past 256, is refused at compile time (`E3029`), as is a repeated value or variant. ### Unit Enums [Section titled “Unit Enums”](#unit-enums) A set of possible values, declared with the `enum` keyword. The state of a character: ```blink enum CharacterStatus = { Idling, Walking, Running, Jumping, Falling } ``` In Luau each value is a string: `"Idling" | "Walking" | ...`. Unit enums do not take generics. ### Tagged Enums [Section titled “Tagged Enums”](#tagged-enums) A set of variants, each with data attached. Declare one with `enum`, then a string naming the tag field, then the variants. Each variant is a name followed by the fields of a struct: ```blink enum MouseEvent = "Type" { Move { Delta: vector, Position: vector, }, Drag { Delta: vector, Position: vector, }, Click { Button: enum { Left, Right, Middle }, Position: vector } } ``` A variant may not have a field named after the tag (`E3005`). BlinkBlox has no union type; a tagged enum is how you write one: ```blink enum Union = "Type" { Number { Value: f64 }, String { Value: string } } ``` This results in the following Luau type: ```luau type Union = | { Type: "Number", Value: number } | { Type: "String", Value: string } ``` #### Generics [Section titled “Generics”](#generics-1) Like [maps](#maps), tagged enums support generics. A tagged union template: ```blink enum Union = "Type" { A { Value: A }, B { Value: B }, } enum NumberStringUnion = Union ``` ## Structs [Section titled “Structs”](#structs) A fixed set of named fields. Declared with the `struct` keyword, with no `=` before the braces. A theoretical game entity: ```blink struct Entity { Health: u8(0..100), Position: vector, Rotation: u8, Animations: struct { First: u8?, Second: u8, Third: u8 } } ``` A field name that is not an identifier can be quoted: `["Display Name"]: string`. Fields are sent in order with no names on the wire, so a struct costs exactly the sum of its fields. ### Merging [Section titled “Merging”](#merging) A struct can merge the fields of other structs into itself, the equivalent of a table union in Luau. A merge is two dots followed by the struct’s name: ```blink struct foo { foo: u8 } struct bar { bar: string } struct foo_bar { ..foo, ..bar } ``` The resulting Luau type for `foo_bar`: ```luau type foo_bar = { foo: number, bar: string } ``` A merge that would repeat a field is refused. ### Generics [Section titled “Generics”](#generics-2) Structs, like [maps](#maps) and [tagged enums](#tagged-enums), support generics. A packet fragment typed with a generic struct: ```blink struct Entity { Health: u8(0..100), Position: vector } struct Fragment { Index: u8, Sequence: u16, Fragments: u8, Data: T } struct EntitiesFragment { ..Fragment } event Replicate { From: Server, Type: Reliable, Call: ManyAsync, Data: Fragment } ``` A generic struct is used where a type is expected, as in the event above, or merged into a named one. ## Naming types [Section titled “Naming types”](#naming-types) Each composite keyword – `struct`, `map`, `set`, `enum` – declares a named type of its kind, and `map` and `enum` may also name an instance of a generic of their kind. The `type` keyword names anything built from a primitive: a number, string, `vector`, `CFrame` and so on, with its range, optional mark and array brackets. ```blink struct Item { Id: u16 } type Coins = u32(..1000000) type Inventory = Coins[..64] ``` `type` cannot name a struct, map, set or enum, even as an array: `type Items = Item[]` is refused (`E3001`). Write `Item[]` where it is used instead, or wrap it in a struct. ## Unknowns [Section titled “Unknowns”](#unknowns) The `unknown` type holds any value that cannot be known until runtime. For unions, use [tagged enums](#tagged-enums) instead. ```blink event Debug { From: Server, Type: Reliable, Call: ManyAsync, Data: unknown } ``` ## Instances [Section titled “Instances”](#instances) Roblox instances, declared as `Instance`. Like `unknown`, an instance costs no buffer bytes: it travels in the remote’s instance list, and counts towards `MaxInstancesPerPacket`. ```blink type AnInstance = Instance ``` Danger If a non-optional instance is `nil` on the receiving side, the packet fails to decode and the rest of it is dropped. An instance can turn `nil` for many reasons: instance streaming, an instance that only exists on the sender’s side, one destroyed while the packet was in flight. If you send instances that might not exist on the other side, mark them [optional](#optionals). ### Specifying Class [Section titled “Specifying Class”](#specifying-class) Narrow an instance to a class by naming it in parentheses: ```blink type Target = Instance(Player) type Part = Instance(BasePart) ``` A declared type is exported to Luau under its own name, so it cannot take the name of a Roblox type the module uses – `Player`, `Instance`, `CFrame`, `Vector3`, a class it names in parentheses – or of a built-in Luau type such as `number` (`E3005`). The receiver checks the class with `IsA`, so subclasses are accepted: an Instance typed as `BasePart` also accepts a `Part`. An instance of the wrong class fails the packet like any other bad value. ## CFrames [Section titled “CFrames”](#cframes) A position and a rotation about it in 3D space. Declared as `CFrame`. ```blink type Location = CFrame ``` By default a CFrame is 24 bytes: the position as three `f32`s, and the rotation as three Euler angles, also `f32`. ### Specifying Encoding [Section titled “Specifying Encoding”](#specifying-encoding-1) Pass up to two [number types](#numbers) in angle brackets. The **first** is used for the rotation and the **last** for the position, so a single type is used for both. A CFrame with an `f16` rotation and an `f32` position: ```blink type MyCFrame = CFrame type SmallCFrame = CFrame ``` The first is 18 bytes, the second 12. Caution The precision limits of [vectors](#specifying-encoding) apply to CFrames too. An [integer type](#unsigned-integers) as the rotation’s encoding keeps only whole radians of each angle, and an unsigned one wraps a negative angle round: -1.57 is sent as 255 in a `u8`. Use a float, or `quat`, for a rotation. ### Quaternion Rotation [Section titled “Quaternion Rotation”](#quaternion-rotation) Writing `quat` as one of the components encodes the rotation as a unit quaternion instead of three Euler angles, in 7 bytes rather than 12: a byte naming the largest of its four components, and the other three as `i16`. The other component, if there is one, is the position’s type, whichever order the two are written in: ```blink type Pose = CFrame type SmallPose = CFrame ``` `Pose` is 19 bytes (an `f32` position and the quaternion), `SmallPose` 13. The rotation is quantised, so it comes back within about 0.002 degrees of what was sent rather than exactly. That suits characters, projectiles and cameras. It does not suit anything that compares rotations for equality or snaps to a grid, which is why it is not the default. `quat` is only a CFrame component. It is refused anywhere else – on its own, in a `vector`, or twice in one CFrame (`E3023`). ## Other Roblox Types [Section titled “Other Roblox Types”](#other-roblox-types) | Type | Bytes | Encoding | | ---------------- | ----- | ------------------------------------------------------------------------------ | | `Color3` | 3 | One byte per channel. Each is rounded to the nearest of 256 steps and clamped. | | `BrickColor` | 2 | The colour’s `Number`, as a `u16`. | | `DateTime` | 8 | `UnixTimestamp` as an `f64`: whole seconds. | | `DateTimeMillis` | 8 | `UnixTimestampMillis` as an `f64`: milliseconds. Decodes to a `DateTime`. | ```blink struct Appearance { Tint: Color3, Team: BrickColor, JoinedAt: DateTime, LastSeen: DateTimeMillis } ``` Caution `Color3` carries the 0 to 1 range and nothing outside it. An HDR channel above 1 – `Highlight` and neon colours use them – arrives as 1, and a negative one as 0. Use a `vector`, or three floats, if you need to send one intact. ## Exports [Section titled “Exports”](#exports) An exported type gets its own `Read` and `Write` functions, which turn a value into a buffer and back without an event. Exports are a way to reuse schema types elsewhere: saving to a datastore, or replicating ECS components by hand. Prefix a type’s declaration with `export`: ```blink export struct MyInterface { field: u8, } ``` ### Usage in Luau [Section titled “Usage in Luau”](#usage-in-luau) Exports appear in the server and client modules and, when [`TypesOutput`](/BlinkBlox/language/options/#serveroutput-clientoutput-typesoutput) is set, in the types module. Their shape: ```luau type MyInterfaceExport = { Read: (buffer) -> MyInterface, Write: (MyInterface) -> buffer, } ``` Example.luau ```luau local Net = require(path.to.Client) local Serialized = Net.MyInterface.Write({ field = 5 }) local Deserialized = Net.MyInterface.Read(Serialized) ``` `Write` returns a buffer of exactly the value’s size, and `Read` decodes from the start of the buffer it is given. Either throws if it fails, and leaves the module’s own outgoing batch and incoming packet untouched when it does. # Diagnostics > Every error and warning code the compiler emits, what each means and what to do about it, and the messages the generated modules print at runtime. The compiler reports problems as numbered diagnostics. An **error** (`E`) stops the compile and writes nothing; a **warning** (`W`) is printed and the modules are still generated. The same codes appear in the Studio plugin’s editor as you type. Codes are banded by the stage that raises them – `1xxx` reading characters, `2xxx` parsing, `3xxx` checking what was parsed – and never renumbered, so the gaps are codes that were retired. A code is shown as `E` or `W` by severity; `3020` can be either. ## Reading a diagnostic [Section titled “Reading a diagnostic”](#reading-a-diagnostic) Game.blink ```blink event Snapshot { From: Server, Type: Unreliable, Call: SingleSync, Data: struct { Names: string[], Tick: u32 } } ``` ```txt [W3019] Warning: Unreliable event "Snapshot" may exceed the packet limit ╭─[Game.blink:1:7] │ 001 │ event Snapshot { ┆ ────┬──── ┆ │ ┆ ╰── Unbounded, and the limit is 900 bytes 005 │ Data: struct { Names: string[], Tick: u32 } ┆ ───┬─── ┆ │ ┆ ╰── This has no upper bound │ = note: Bound the variable-length fields -- `string(0..64)`, `u8[..16]` -- or send it reliably. │ ────╯ ``` The first line is the code, the severity and the message. Below it, each labelled line of the schema with its line number: the primary label under what is wrong, secondary labels under what explains it, and `note` lines with the fix. With `--compact` the CLI prints one line instead: ```txt [W3019] [L001:L001] [Game.blink] Warning: Unreliable event "Snapshot" may exceed the packet limit ``` A program reading the diagnostics – an editor task, a CI step, an assistant – should use `--json` instead, which gives each one’s code, file, line and column, labels and notes as JSON. See [JSON output](/BlinkBlox/getting-started/cli/#json-output). ## Errors and warnings [Section titled “Errors and warnings”](#errors-and-warnings) ### Reading and parsing [Section titled “Reading and parsing”](#reading-and-parsing) | Code | Message | What to do | | ------- | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `E1001` | Unexpected token | A character that begins no token of the language. Remove it, or check for a stray symbol or an unclosed string. | | `E2001` | Unexpected end of file | A declaration or list was left open. Close the brace, bracket or parenthesis. | | `E2002` | Unexpected token | The grammar expected something else here; the label says what. Often a missing comma, `:` or `=`. | | `E2003` | Unknown option “X” | An `option` name that does not exist, or an event or function field that does not. The label lists the valid ones. | | `E2003` | Invalid value for option “X” | A numeric option that is not a positive whole number, a `Casing` other than `Pascal`, `Camel` or `Snake`, or an `InboundBurst` below `MaxPacketSize`. | | `E2003` | Unknown option “X” (on a field’s value) | A `From`, `Type`, `Call` or `Yield` value that is not one of the allowed words. | | `E2003` | Invalid value for “Rate” / “Burst” / “Concurrency” | `Rate` must be above zero; `Burst` must be at least 1, since each event spends a whole token; `Concurrency` must be a whole number of at least 1. | | `E2005` | Field “X” is missing | An event needs `From`, `Type` and `Call`; a function needs `Yield`. Add the field. | ### Declarations and types [Section titled “Declarations and types”](#declarations-and-types) | Code | Message | What to do | | ------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `E3001` | Cannot cast “X” to “Y” | A reference to a declaration of the wrong kind, such as a `map` name where a struct is expected. | | `E3001` | Expected a struct to merge | `..Name` inside a struct must name a struct. | | `E3001` | A map cannot be keyed by … | A struct, tagged enum, map, set, array or type pack decodes to a fresh table that nothing can look up. Key by a string, number, boolean or enum. | | `E3002` | Invalid optional type | A map’s key or value may not be optional. | | `E3002` | Type cannot be optional | `unknown` and `quat` have no way to encode absence. | | `E3004` | Field “X” was already specified | An event or function field given twice. | | `E3004` | Duplicate field / flag / value / variant / element | A name repeated inside a struct, set, enum, tagged enum or type pack. | | `E3004` | Merged struct contains a duplicate field | Two merged structs, or a merge and a field, share a field name. | | `E3005` | Reserved identifier | `StepReplication`, `SetRateLimitHandler` and `SetDecodeErrorHandler` name module members. Rename the declaration. | | `E3005` | “X” cannot name a type pack element | A named element becomes a parameter: it must be a Luau identifier, not a keyword, and not a name the generated function already uses, such as `Player`. | | `E3005` | Reserved identifier (a type named `number`, `Player`, …) | A declared type is exported to Luau under its own name. It cannot take a built-in Luau type’s name (`any`, `boolean`, `buffer`, `never`, `number`, `string`, `thread`, `unknown`), and a top-level type cannot take the name of a Roblox type the module uses: `Player`, `Instance`, `RemoteEvent`, `UnreliableRemoteEvent`, `CFrame`, `Vector3`, `Color3`, `DateTime`, `BrickColor`, or a class the schema names in `Instance(...)`. Rename it. | | `E3005` | Enum tag used as field in variant | A tagged enum’s tag field may not also be a field of a variant. | | `E3007` | Unknown reference | The name is not declared, or not declared yet: a declaration is in scope only after it closes. | | `E3008` | Type doesn’t accept a range | Only numbers, `string`, `buffer`, `vector` and arrays take a range. | | `E3009` | Malformed range / Expected an integer / Range outside bounds | Fix the range: integer types and lengths need whole numbers, and the range must fit the type (an array holds at most 65535). | | `E3010` | Duplicate declaration | Two declarations, scopes or imports share a name in one scope, including across profiles. | | `E3012` | Unit enums don’t support generics | Only structs, maps and tagged enums are generic. | | `E3013` | … is not exportable / Generic types can’t be exported / Types containing an Instance or unknown can’t be exported | `export` applies to non-generic types whose values travel entirely in the buffer. | | `E3022` | Recursive type | A type cannot contain itself: references are inlined where they appear. | | `E3023` | Type doesn’t accept components / Too many components / Unknown or invalid primitive used as component | Components go on `vector` (one) and `CFrame` (two), and must be number types or `quat`. | | `E3023` | `quat` is not a type on its own / Only a CFrame’s rotation can be a quaternion / A CFrame has one rotation | `quat` is valid only as one component of a `CFrame`. | | `E3029` | Too many values / variants, or Empty enum | An enum travels as one byte: it holds 1 to 256 values or variants. | | `E3030` | Too many reliable events and functions / Too many unreliable events | A channel numbers its declarations with one byte, so it holds 256. Imports count; declarations a profile leaves out do not. Split the schema’s traffic, or move events to the other channel. | ### Files, options and profiles [Section titled “Files, options and profiles”](#files-options-and-profiles) | Code | Message | What to do | | ------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `E3014` | Unknown require | The imported file was not found. Check the path, relative to the importing file. | | `E3015` | Cyclic import | Two files import each other, directly or through others. Move what they share into a third file. | | `E3016` | Option set after start of file | Move every `option` above the first declaration. | | `W3017` | Option “UseColon” is deprecated | It has no effect. Delete the line. | | `W3017` | Field “Poll” is deprecated | Write `Call: Polling` and delete `Poll`. The `Call` value beside `Poll: true` was being ignored. | | `E3024` | Duplicate option | An option set twice where one build would apply both. Each profile may set it once; set without a profile, it may not also be set with one. | | `E3025` | Unknown attribute | The only attribute is `@profile`. | | `E3026` | Unknown profile | Profiles are `dev`, `debug`, `test` and `release`. | | `E3027` | Reference to an excluded declaration | A compiled declaration uses one this build’s profile leaves out. Give both the same profile, or neither. | | `E3028` | Duplicate attribute / Attribute without a statement | A statement takes one `@profile`, and it must be followed by a declaration, import or option. | ### Network safety [Section titled “Network safety”](#network-safety) | Code | Message | What to do | | ------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | `E3018` | Unreliable event “X” cannot fit in one packet | Its smallest payload is over [`MaxUnreliableSize`](/BlinkBlox/language/options/#maxunreliablesize). Send it `Reliable`, or shrink it. | | `W3019` | Unreliable event “X” may exceed the packet limit | Its largest payload is over the limit. Bound the field the secondary label names, or send it reliably. | | `W3020` | Rate limiting a Server event / function has no effect | Rate and concurrency limits apply to inbound traffic only. Remove `Rate`, `Burst` and `Concurrency`. | | `E3020` | “Burst” was set without a “Rate” | Add a `Rate` (or `option DefaultRate`), or remove the `Burst`. | | `E3020` | Inbound “X” has no rate limit | `option RequireRates` is set. Give it a `Rate`, or set `option DefaultRate`. | | `E3021` | Array / Map of a type that costs nothing to decode | An unbounded repetition of elements that read no bytes gives a hostile count nothing to run out of. Bound it (`[..16]`) or give the element a sent field. | ## Errors without a code [Section titled “Errors without a code”](#errors-without-a-code) A few problems are reported as plain errors rather than diagnostics: | Message | Cause | | ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | | `A client output path must be defined.` / `A server output path must be defined.` | The CLI needs `ClientOutput` and `ServerOutput`. | | `Cannot use yield type: "Future", without providing a path to the future library.` | A `Yield: Future` function needs `option FutureLibrary`; `Promise` likewise needs `PromiseLibrary`. | ## Runtime messages [Section titled “Runtime messages”](#runtime-messages) The generated modules report problems through `warn` and `error`, prefixed `[BlinkBlox]`. Warnings the server prints about a client’s traffic are limited to one a second per player and kind. | Message | Side | Meaning | | ------------------------------------------------------------------------------------------------------------------ | ------ | ------------------------------------------------------------------------------------------------- | | `Dropped a malformed packet from X.` | server | The remote was called with arguments that are not a buffer and a table. | | `Dropped an oversized packet from X.` | server | Over `MaxPacketSize`. | | `Dropped a packet with too many instances from X.` | server | Over `MaxInstancesPerPacket`. | | `Dropped the rest of a packet from X, too many events.` | server | `MaxEventsPerPacket` events were decoded; the rest was dropped. | | `Dropped packets from X over the inbound budget of N bytes a second, N refused.` | server | The player’s `InboundBytesPerSecond` budget ran out. | | `Rate limited event "E" from X, N refused.` | server | The event’s `Rate` refused it. | | `Dropped a packet that could not be decoded, at event "E": ...` | client | A packet from the server failed to decode and no decode-error handler is installed. | | `Event queue of "E" exceeded 256, did you forget to implement a listener?` | client | Reliable events are queuing for a listener that never connected. | | `"E" already has a listener. ... keeps only the newest one, and the replaced listener's disconnect stops working.` | both | A second `On` on a `Single` event. | | `Event "E" yielded in a Sync call, so the rest of the packet it arrived in was discarded. ...` | both | A Sync listener yielded; see [`SyncValidation`](/BlinkBlox/language/options/#syncvalidation). | | `Dropped unreliable event "E", N bytes exceeds the N byte limit.` | both | An unreliable send grew past `MaxUnreliableSize` and was not sent. | | `"F" was never answered and has been failed after N seconds.` | both | An `Invoke` timed out; on the server the message names the player. | | `"F" encountered an error, ...` | both | A function’s listener errored or returned a value that failed to serialise; the caller is failed. | | `There was an exception while processing "F".` | both | Raised by a `Coroutine` `Invoke` whose call failed. | | `32 calls are already awaiting a response, this call has been dropped.` | both | Raised by the 33rd outstanding `Invoke`. | | `Cannot invoke X, they are no longer in the game.` | server | Raised by `Invoke` on a player who has left. | | `This client was built from a different schema than the server (client ..., server ...).` | client | Raised on require; see [Wire compatibility](/BlinkBlox/reference/wire-compatibility/). | | `The server did not publish a schema signature. ...` | client | Raised on require: the server is a build from before signatures, or not BlinkBlox. | | `The reliable remote is not a RemoteEvent.` / `The unreliable remote is not an UnreliableRemoteEvent.` | both | Something else holds the remote’s name in `ReplicatedStorage`. | | `An instance of BlinkBlox is already running with the remote scope "S". ...` | both | Two modules with the same `RemoteScope` were required in one Luau environment. | A decode failure passed to `SetDecodeErrorHandler` carries the error that stopped the decode: a range or length check (`Expected "Value" to be smaller than or equal to 100, got 250 instead.`), a length the packet cannot back, `Unknown event index.` for an index no event has, or `This channel receives no events.` for traffic on a channel this side never listens to. # Generated API > Every function and field of the generated server, client and types modules, with signatures, casing variants, edit-mode stubs and limits. A schema compiles to a server module, a client module and, with [`TypesOutput`](/BlinkBlox/language/options/#serveroutput-clientoutput-typesoutput), a shared types module. This page lists everything they expose. Names are shown in the default `Pascal` [casing](#casing). In the signatures below, `...Data` stands for an event’s or function’s `Data` as parameters: one parameter named `Value` for a single type, or one per element of a [type pack](/BlinkBlox/language/events/#type-packs), named as the schema names them or `Value1`, `Value2`… when it does not. `...Return` is a function’s `Return` the same way. An event with no `Data` takes a single `nil` parameter. ## Requiring the modules [Section titled “Requiring the modules”](#requiring-the-modules) Server.server.luau ```luau local Net = require(ServerScriptService.Network.Server) ``` Client.client.luau ```luau local Net = require(ReplicatedStorage.Network.Client) ``` | Module | Requirable from | When it loads | | ------ | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Server | the server only; errors elsewhere | Creates the two remotes if they are missing, publishes the schema signature, and checks both remotes’ classes. | | Client | a client only; errors elsewhere | Waits for the two remotes, checks their classes, then checks the [schema signature](/BlinkBlox/reference/wire-compatibility/) and errors on a mismatch. May yield. | | Types | anywhere | Nothing: it holds types and exported `Read`/`Write` pairs. | Requiring a second server or client module with the same [`RemoteScope`](/BlinkBlox/language/options/#remotescope) in the same Luau environment errors: `An instance of BlinkBlox is already running with the remote scope "BLINK"`. ## Module members [Section titled “Module members”](#module-members) | Member | Server | Client | | --------------------------------- | ------------------------------------------------------------------ | ----------------------------------------------------- | | `StepReplication()` | Sends every player’s queued reliable traffic. | Sends the client’s queued reliable traffic. | | `SetRateLimitHandler(Handler?)` | `Handler: (Player: Player, Event: string?, Refused: number) -> ()` | Accepted and ignored: the client does not rate-limit. | | `SetDecodeErrorHandler(Handler?)` | `Handler: (Player: Player, Event: string?, Failure: string) -> ()` | `Handler: (Event: string?, Failure: string) -> ()` | * `StepReplication` is connected to `RunService.Heartbeat` unless [`ManualReplication`](/BlinkBlox/language/options/#manualreplication) is set, in which case you call it. * The rate-limit handler is called at most once a second per player and event, with `Refused` the number of refusals since the last call. `Event` is `nil` when whole packets were refused over the [inbound byte budget](/BlinkBlox/language/options/#inboundbytespersecond-inboundburst). * The decode-error handler receives the event the packet claimed to carry, or `nil` if its index named none. On the server it is called at most once a second per player, channel and event, and without one the server is silent. On the client, without one, each failure is a warning. * Both handlers run on a thread of their own. Pass `nil` to remove one. ## Events [Section titled “Events”](#events) Which members an event has depends on the side and on its [`From`](/BlinkBlox/language/events/#from): | Event | Server module | Client module | | -------------- | ------------------------------------------- | ----------------------------------------- | | `From: Server` | `Fire`, `FireAll`, `FireList`, `FireExcept` | `On` and `Predict`, or `Iter` when polled | | `From: Client` | `On` and `Predict`, or `Iter` when polled | `Fire` | `Predict` exists only with [`option Predict`](/BlinkBlox/language/options/#predict). An event is polled when it is declared `Call: Polling` or [`UsePolling`](/BlinkBlox/language/options/#usepolling) is set. ### Sending [Section titled “Sending”](#sending) | Member | Side | Signature | Sends to | | ------------ | ------ | --------------------------------- | ------------------------- | | `Fire` | server | `(Player: Player, ...Data) -> ()` | one player | | `FireAll` | server | `(...Data) -> ()` | every player | | `FireList` | server | `(List: {Player}, ...Data) -> ()` | each player in `List` | | `FireExcept` | server | `(Except: Player, ...Data) -> ()` | every player but `Except` | | `Fire` | client | `(...Data) -> ()` | the server | * A **reliable** send is written into the recipient’s batch and sent on the next `StepReplication`. * An **unreliable** send is sent at once, as its own packet. One larger than [`MaxUnreliableSize`](/BlinkBlox/language/options/#maxunreliablesize) is dropped with a warning. * A send throws if a value fails validation: always for a string, buffer or array longer than its bound, and for types, ranges and classes with [`WriteValidations`](/BlinkBlox/language/options/#writevalidations). Whatever the throw had already written is taken back out, so the rest of the batch is unaffected. ### Receiving [Section titled “Receiving”](#receiving) | Member | Side | Signature | | --------- | ------ | ------------------------------------------------------------ | | `On` | server | `(Listener: (Player: Player, ...Data) -> ()) -> (() -> ())` | | `On` | client | `(Listener: (...Data) -> ()) -> (() -> ())` | | `Predict` | server | `(Player: Player, ...Data) -> ()` | | `Predict` | client | `(...Data) -> ()` | | `Iter` | server | `() -> iterator of (Index: number, Player: Player, ...Data)` | | `Iter` | client | `() -> iterator of (Index: number, ...Data)` | | `Next` | both | Deprecated alias of `Iter`, the same function. | * `On` returns a function that disconnects the listener. * A `Single` event keeps one listener. A second `On` replaces the first and warns, and the first listener’s disconnect function stops doing anything. A `Many` event keeps them all. * A `Sync` listener is called on the decode thread and must not yield; an `Async` one runs on a thread of its own. See [`Call`](/BlinkBlox/language/events/#call). * A **reliable** event that arrives with no listener is queued and replayed to the first listener that connects. The server keeps at most 256 per event and drops the rest silently; the client warns past 256. An **unreliable** event with no listener is dropped. * `Predict` delivers to this side’s own listeners, exactly as an arriving event would be delivered, queue included. It sends nothing. * `Iter` takes rows out of the event’s queue as it goes, so each row is seen once: ```luau for Index, Player, Value in Net.Jump.Iter() do -- ... end ``` On the server a polled queue holds at most 256 x `Players.MaxPlayers` rows; the rest are dropped. ## Functions [Section titled “Functions”](#functions) | Function | Server module | Client module | | ---------------------------- | ------------- | ------------- | | `From: Client` (the default) | `On` | `Invoke` | | `From: Server` | `Invoke` | `On` | | Member | Side | Signature | | -------- | ------ | ------------------------------------------------------------------ | | `Invoke` | client | `(...Data) -> (...Return)`, or a Future or Promise | | `Invoke` | server | `(Player: Player, ...Data) -> (...Return)`, or a Future or Promise | | `On` | server | `(Listener: (Player: Player, ...Data) -> (...Return)) -> ()` | | `On` | client | `(Listener: (...Data) -> (...Return)) -> ()` | What `Invoke` returns depends on the function’s [`Yield`](/BlinkBlox/language/functions/#yield): | `Yield` | `Invoke` returns | On failure | | ----------- | ----------------------------------------------------- | -------------------------------------------------------- | | `Coroutine` | the return values, after yielding | throws `There was an exception while processing "Name".` | | `Future` | `Future.Try(...)`: a future of `(Success, ...Return)` | the future’s success is `false` | | `Promise` | `Promise.new(...)` resolving with the return values | the promise rejects; cancelling it releases the call | * A call fails when the listener errors, when its return value fails to serialise, when a rate limit refuses it, when the call is not answered within [`InvocationTimeout`](/BlinkBlox/language/options/#invocationtimeout) (10 seconds), and, on the server, when the player leaves or has already left. * At most **32** calls may be outstanding at once: per client, and per player on the server. The 33rd `Invoke` errors at once. * `On` keeps one listener and returns nothing; a second `On` replaces the first. Calls that arrive before it connects are queued and run when it does – on the server up to 256, past which each is answered with a failure. * The listener’s return values are the reply. An error in the listener is warned on the listening side and fails the caller. ## Exported types [Section titled “Exported types”](#exported-types) A type declared with `export` gets a `Read`/`Write` pair in all three modules, under its name: | Member | Signature | | ------------ | -------------------------- | | `Name.Read` | `(Buffer: buffer) -> Name` | | `Name.Write` | `(Value: Name) -> buffer` | They serialise one value into a buffer of exactly its size and back, with the same encoding and the same receive-side checks the events use. They work in edit mode too. A type containing an `Instance` or `unknown` cannot be exported (`E3013`). See [Exports](/BlinkBlox/language/types/#exports). ## Types and scopes [Section titled “Types and scopes”](#types-and-scopes) Every named type in the schema is declared in each module as a Luau `export type`, so `Net.Name` works in a type annotation. A [scope](/BlinkBlox/language/scopes/) becomes a nested table: `Net.Combat.Hit.Fire(...)`. A type inside a scope is exported as `Scope_Name`, for example `Net.Combat_Damage`. ## Casing [Section titled “Casing”](#casing) [`option Casing`](/BlinkBlox/language/options/#casing) renames the generated members. The names of your own declarations are never changed. | `Pascal` (default) | `Camel` | `Snake` | | ----------------------- | ----------------------- | -------------------------- | | `Fire` | `fire` | `fire` | | `FireAll` | `fireAll` | `fire_all` | | `FireList` | `fireList` | `fire_list` | | `FireExcept` | `fireExcept` | `fire_except` | | `On` | `on` | `on` | | `Iter` | `iter` | `iter` | | `Next` | `next` | `next` | | `Predict` | `predict` | `predict` | | `Invoke` | `invoke` | `invoke` | | `Read` | `read` | `read` | | `Write` | `write` | `write` | | `StepReplication` | `stepReplication` | `step_replication` | | `SetRateLimitHandler` | `setRateLimitHandler` | `set_rate_limit_handler` | | `SetDecodeErrorHandler` | `setDecodeErrorHandler` | `set_decode_error_handler` | ## Edit mode [Section titled “Edit mode”](#edit-mode) When `RunService:IsRunning()` is false – in edit mode, and in Roblox stories (Hoarcekat, UI Labs, Flipbook) – no remotes exist, and both modules return stubs that keep the real API’s shape: | Member | Stub | | ----------------------------------------------------------------- | ---------------------------------------------------------------------- | | `Fire`, `FireAll`, `FireList`, `FireExcept` | does nothing | | `On` (events and functions) | does nothing and returns a disconnect function that does nothing | | `Iter`, `Next` | returns an iterator that ends at once, so a `for` loop runs zero times | | `Predict` | does nothing | | `Invoke` | does nothing and returns `nil` | | `StepReplication`, `SetRateLimitHandler`, `SetDecodeErrorHandler` | do nothing | | exported `Read`, `Write` | work normally | The stub is returned before the module checks which side it is on, so a story can require either module – but not both. The `RemoteScope` guard above runs first, in edit mode too, and the server and client modules of one schema share a scope, so the second one required errors. Caution In edit mode `Invoke` returns `nil` even for a `Future` or `Promise` function, so code that chains on its result (`:andThen(...)`) errors in a story. Guard it with `RunService:IsRunning()`. ## Reserved names [Section titled “Reserved names”](#reserved-names) | Name | Why | | ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | `StepReplication`, `SetRateLimitHandler`, `SetDecodeErrorHandler` | Module members. A top-level declaration may not take these names (`E3005`). | | Luau keywords (`end`, `local`, …) | A named type-pack element becomes a parameter, so it must be a Luau identifier (`E3005`). Struct fields, flags and tags may be keywords; they are quoted. | | `Player`, `Buffer`, `Load`, `Size`, `Success` and other names the generated functions use | Refused as type-pack element names, which would shadow them (`E3005`); the diagnostic names the clash. | | A tagged enum’s tag | May not also be a field of one of its variants (`E3005`). | Caution The reserved check compares the `Pascal` spelling only. Under `Camel` or `Snake` casing, a top-level declaration named `stepReplication` or `step_replication` compiles and replaces the module member of that name. Avoid those names. ## Limits [Section titled “Limits”](#limits) | Limit | Value | | --------------------------------------- | ---------------------------------------------------------------------------------- | | Outstanding invocations | 32 per client; 32 per player on the server | | Invocation timeout | `InvocationTimeout`, 10 seconds by default | | Queued reliable events with no listener | 256 per event (server drops, client warns) | | Queued calls with no listener | 256 per function on the server, then answered with a failure | | Polled queue on the server | 256 x `Players.MaxPlayers` rows per event | | Enum values or tagged-enum variants | 256 | | Declarations per channel | 256 (reliable events and functions share one channel, unreliable events the other) | | Unbounded string, buffer or array | 65535 bytes or elements | # Options > Every option a schema accepts, with its value, its default and what it does, in one table. Options go at the top of a schema, before any declaration, as `option Name = Value`. Each may be set once, or once per [profile](/BlinkBlox/language/profiles/). The [Options](/BlinkBlox/language/options/) page explains each one in full. | Option | Value | Default | What it does | | -------------------------------------------------------------------------------------------------------------- | -------------------------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | [`Casing`](/BlinkBlox/language/options/#casing) | `Pascal`, `Camel` or `Snake` | `Pascal` | Casing of the generated methods: `FireAll`, `fireAll` or `fire_all`. | | [`ServerOutput`](/BlinkBlox/language/options/#serveroutput-clientoutput-typesoutput) | path string | none; required by the CLI | Where the server module is written, relative to the schema. | | [`ClientOutput`](/BlinkBlox/language/options/#serveroutput-clientoutput-typesoutput) | path string | none; required by the CLI | Where the client module is written. | | [`TypesOutput`](/BlinkBlox/language/options/#serveroutput-clientoutput-typesoutput) | path string | none | Where a shared module with the schema’s types and exported `Read`/`Write` pairs is written. Not written when unset. | | [`Typescript`](/BlinkBlox/language/options/#typescript) | boolean | `false` | Also write a `.d.ts` beside the server and client modules. CLI only. | | [`UsePolling`](/BlinkBlox/language/options/#usepolling) | boolean | `false` | Give every event the polling API (`Iter`), as if each were `Call: Polling`. | | [`FutureLibrary`](/BlinkBlox/language/options/#futurelibrary-and-promiselibrary) | Luau path string | none | What `require` loads for `Yield: Future`. Required by any such function. | | [`PromiseLibrary`](/BlinkBlox/language/options/#futurelibrary-and-promiselibrary) | Luau path string | none | What `require` loads for `Yield: Promise`. Required by any such function. | | [`SyncValidation`](/BlinkBlox/language/options/#syncvalidation) | boolean | `true` | Warn when a Sync listener yielded, and discard the rest of the packet it held up. | | [`WriteValidations`](/BlinkBlox/language/options/#writevalidations) | boolean | `false` | Check types, ranges and Instance classes on send. Length bounds are checked on send regardless. | | [`ManualReplication`](/BlinkBlox/language/options/#manualreplication) | boolean | `false` | Do not flush reliable traffic on `Heartbeat`; call `StepReplication` yourself. | | [`RemoteScope`](/BlinkBlox/language/options/#remotescope) | string | none | Prefix for the remotes’ names: `"PKG"` gives `PKG_BLINK_RELIABLE_REMOTE`. | | [`MaxPacketSize`](/BlinkBlox/language/options/#maxpacketsize-maxeventsperpacket-maxinstancesperpacket) | whole number | `8192` | Largest inbound packet the server reads, in bytes. | | [`MaxEventsPerPacket`](/BlinkBlox/language/options/#maxpacketsize-maxeventsperpacket-maxinstancesperpacket) | whole number | `64` | Events the server decodes from one packet before dropping the rest. | | [`MaxInstancesPerPacket`](/BlinkBlox/language/options/#maxpacketsize-maxeventsperpacket-maxinstancesperpacket) | whole number | `256` | Instance and `unknown` values one inbound packet may carry. | | [`InboundBytesPerSecond`](/BlinkBlox/language/options/#inboundbytespersecond-inboundburst) | whole number | `8 * MaxPacketSize` (65536) | Bytes a second each player’s packets may cost the server; a packet costs at least 128. | | [`InboundBurst`](/BlinkBlox/language/options/#inboundbytespersecond-inboundburst) | whole number, at least `MaxPacketSize` | the larger of `InboundBytesPerSecond` and `MaxPacketSize` (65536) | The most inbound budget a player may bank. | | [`MaxUnreliableSize`](/BlinkBlox/language/options/#maxunreliablesize) | whole number | `900` | Largest unreliable payload, checked at compile time and on send. | | [`DefaultRate`](/BlinkBlox/language/options/#defaultrate-requirerates) | whole number | none | Events per second per player for every inbound event and function without its own `Rate`. | | [`RequireRates`](/BlinkBlox/language/options/#defaultrate-requirerates) | boolean | `false` | Make an inbound event or function with no effective rate a compile error (`E3020`). | | [`Predict`](/BlinkBlox/language/options/#predict) | boolean | `false` | Add `Predict` to every event this side listens to (not polled ones): it delivers to local listeners without a remote. | | [`InvocationTimeout`](/BlinkBlox/language/options/#invocationtimeout) | whole number | `10` | Seconds before an unanswered `Invoke` fails. | | `UseColon` | boolean | none | Deprecated and ignored; warns `W3017`. Delete it. | ## Value syntax [Section titled “Value syntax”](#value-syntax) | Value | Written as | Example | | ---------------- | -------------------------------------------------------------- | -------------------------------------------------------------- | | boolean | `true` or `false` | `option Predict = true` | | path string | a quoted path, relative to the schema’s folder unless absolute | `option ServerOutput = "../src/Network/Server.luau"` | | Luau path string | a quoted Luau expression, inserted into `require(...)` | `option PromiseLibrary = "ReplicatedStorage.Packages.Promise"` | | whole number | a positive integer | `option MaxPacketSize = 16384` | | casing | a bare word | `option Casing = Camel` | A number that is zero, negative or fractional is refused (`E2003`), as is an unknown option name or casing. An option after the first declaration is `E3016`; one set twice for the same build is `E3024`. ## What the options do not change [Section titled “What the options do not change”](#what-the-options-do-not-change) None of these options changes the bytes on the wire, so none of them is part of the [schema signature](/BlinkBlox/reference/wire-compatibility/). A server and a client built from the same declarations with different options still talk to each other. `RemoteScope` is the exception in practice: it changes the remotes’ names, so a client with a different scope never finds the server’s remotes at all. # Wire compatibility > When two generated modules can talk to each other -- the schema signature, the wire-format version, and which releases changed the bytes on the wire. A server module and a client module talk only if they agree on every byte: which index names which event, and how each type is laid out. Two things decide that – the **schema** they were built from, and the **wire format** of the compiler that built them. Both are summed up in one value, the schema signature, which the client checks when it loads. The rule that follows: **build the server and the client from the same schema, with the same compiler release and the same profile, and ship them together.** ## Why it matters [Section titled “Why it matters”](#why-it-matters) Events are numbered by position. The first reliable event or function declared is index 0, the next is 1, and unreliable events are counted separately. Inserting an event in the middle of a schema renumbers every event after it. A client one build behind then reads index 7 as whatever index 7 used to be, decodes one event’s payload as another’s, and hands it to the wrong listener. Nothing errors – the decoder has no way to know. The signature exists to turn that into an error at startup. ## The schema signature [Section titled “The schema signature”](#the-schema-signature) Each generated module carries a 16-character hexadecimal signature. The server publishes it as the `SchemaSignature` attribute of its reliable remote, set before the remote is parented when the server creates it. The client compares it with its own when it is required: | Situation | What the client does | | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Signatures match | Loads normally. | | Signatures differ | Errors on require: `This client was built from a different schema than the server (client ..., server ...). Recompile both sides from the same .blink file.` | | No signature on the remote | Waits up to five seconds for it, then errors: `The server did not publish a schema signature.` The server is a build from before 0.23.0, upstream Blink, or something else that created a remote with that name. | | The remotes do not exist | Waits for them indefinitely, as `WaitForChild` does. This is what a different `RemoteScope` looks like. | ### What it covers [Section titled “What it covers”](#what-it-covers) The signature is a hash of the wire-format version and, for every event and function in declaration order, including those inside scopes and imports: | Declaration | Included | | ----------- | --------------------------------------------------------------------------------------- | | Event | channel (reliable or unreliable), index, `From`, `Type`, name, and the full `Data` type | | Function | index, `From`, name, `Data` and `Return` types | A type is included as a whole: every primitive with its range, components and Instance class, struct field names, enum values, set flags, tagged-enum tags and variants, and type-pack element names. A named type that no event or function uses is not included, since nothing sends it. ### What it does not cover [Section titled “What it does not cover”](#what-it-does-not-cover) * **Options.** They decide how a module is generated, not what it sends: `Casing`, `Typescript`, the output paths, `SyncValidation`, `WriteValidations`, `Predict`, the packet limits, the rate options and `InvocationTimeout` can all differ between the two sides. `RemoteScope` is left out too, because it is already part of the remotes’ names. * **`Call`, `Rate` and `Burst`.** They decide how the receiving side dispatches and throttles an event, and change no byte. * Comments, formatting, and the order of type declarations. A profile is covered indirectly: a declaration a profile leaves out is not in the schema that build sees, so a `dev` client and a `release` server have different signatures whenever the schema marks anything with `@profile`. Caution Renaming something changes the signature even where it changes no byte on the wire. That includes renaming an event, a struct field, or a type-pack element. Rebuild both sides after any rename. It is a check against mistakes, not a security measure. The client owns its copy of the module and can edit the check out; the server’s own validation is what protects it. ## The wire-format version [Section titled “The wire-format version”](#the-wire-format-version) The schema alone cannot see a change in how the compiler encodes a type: a release that packs booleans differently changes every packet without touching a single schema. So the signature also hashes `WIRE_VERSION`, a number the compiler bumps by hand whenever a type’s bytes change. | `WIRE_VERSION` | Releases | Change | | -------------- | ---------------- | ------------------------------------------------- | | 1 | 0.23.0 to 0.26.x | The first version, introduced with the signature. | | 2 | 0.27.0 onward | `boolean[]` packed eight elements to a byte. | ## Releases that changed the wire [Section titled “Releases that changed the wire”](#releases-that-changed-the-wire) | Release | Change | Compatible with | | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | 0.21.0 | Booleans and optional flags share a bitfield; lengths are encoded relative to their minimum; `OrderedUnreliable` added. The fork’s first wire-format change. | 0.21.0 and 0.22.x. Neither carries a signature, so a mismatch between them is silent. | | 0.23.0 | Schema signature added, at `WIRE_VERSION` 1. No byte of a payload changed, but a client now refuses a server without a signature. | 0.23.0 to 0.26.x. | | 0.27.0 | `boolean[]` packed eight to a byte; `WIRE_VERSION` 2. `CFrame` added (opt-in, so only schemas that use it are affected). | 0.27.0 onward. | | 0.28.0 | Renamed from Blink to BlinkBlox. The remotes keep their names – `BLINK_RELIABLE_REMOTE`, `BLINK_UNRELIABLE_REMOTE` – as do `_G._BLINK` and the `SchemaSignature` attribute, so builds either side of the rename still find each other. | 0.27.0 onward. | | 0.29.0 | No change to the format. | 0.27.0 onward. | | 0.30.0 | No change to the format. | 0.27.0 onward. | | 0.31.0 | `f16` writes -0 as `0x8000` instead of `0x0000`. An older reader decodes it as 0, so builds either side agree on everything but the sign of a zero. | 0.27.0 onward. | | 0.32.0 | No change to the format. | 0.27.0 onward. | | 0.33.0 | No change to the format. | 0.27.0 onward. | Modules from different releases with the same `WIRE_VERSION` interoperate, but a fix to how a value is read or handled applies only on the side built by the release that has it. Ship both sides from one release. ## One byte per event index [Section titled “One byte per event index”](#one-byte-per-event-index) Each channel numbers its declarations with a single byte: reliable events and functions share one channel, `Unreliable` and `OrderedUnreliable` events the other. So each channel holds at most 256 declarations, counting what the schema imports and not counting what its profile leaves out. The compiler refuses a 257th (`E3030`); before 0.30.0 it compiled, and was sent with the first one’s index.