Coming from Packet
Packet defines each packet with a call, Packet("Hit", Packet.Instance, Packet.NumberU8), and builds
its serialiser when the game starts. BlinkBlox compiles a schema ahead of time into a server module
and a client module. So the definitions become a schema, and the calls go to the generated modules.
blinkblox --from packet does the first half: it runs your definitions against a stand-in for
Packet that records every packet instead of making a remote, and writes them out as a schema.
Packet’s definitions do not say which side sends a packet, or how often a client may. The draft asks you for both.
This page was written against Packet 1.7.0.
The short version
Section titled “The short version”- Install
blinkblox. See Installation. - Record the definitions:
blinkblox Packets.luau --from packet. It writesPackets.blinkbeside the file and prints what you have to decide. - Work through the
TODO(convert)marks: the direction of each event and function, aRatefor each one a client sends, an upper bound for each length a client sends, and the two output paths. - Compile it:
blinkblox Packets. Both libraries can run in one game, each on its own remotes, so packets can move one at a time. - Change the call sites (below), or wrap the modules in the bridge and change them later.
- Install the server’s handlers. See Securing the server.
Recording the definitions
Section titled “Recording the definitions”blinkblox Packets.luau --from packetA require whose path, alias or Instance ends in Packet gets the stand-in, a module beside the
file is run too, and any other require gets a placeholder and is named in the printed list. The
draft is written beside the file with .blink for an extension, and is not replaced without --yes;
--check prints it instead. The command line page
has the details.
For these definitions:
local ReplicatedStorage = game:GetService("ReplicatedStorage")local Packet = require(ReplicatedStorage.Packet)
return { Hit = Packet("Hit", Packet.Instance, Packet.NumberU8, Packet.StringLong), Health = Packet("Health", Packet.NumberU8), Price = Packet("Price", Packet.NumberU16):Response(Packet.NumberU32),}it writes:
-- Converted from Packet 1.7.0 definitions by BlinkBlox. A draft: read it before you use it.-- Each TODO(convert) marks what Packet's definitions have no way to say. The guide explains them:-- https://xopoiii.github.io/BlinkBlox/guides/coming-from-packet/---- Once every event a client fires has a Rate, turn this on, so that none is added without one:-- option RequireRates = true
-- TODO(convert): a client sends this; bound #3 (string length)event Hit { -- TODO(convert): Packet does not say which side sends this; say it here From: Client, Type: Reliable, Call: ManyAsync, -- TODO(convert): Rate: ?, Burst: ? Data: (Instance, u8, string)}
event Health { -- TODO(convert): Packet does not say which side sends this; say it here From: Client, Type: Reliable, Call: ManyAsync, -- TODO(convert): Rate: ?, Burst: ? Data: u8}
function Price { -- TODO(convert): Packet does not say which side calls this; add From: Server if the server does Yield: Coroutine, -- TODO(convert): Rate: ?, Concurrency: ? Data: u16, Return: u32}and prints:
Draft written to Packets.blink.Before it is used: - 2 event(s) whose direction you must state: Hit, Health - 1 function(s) whose caller you must state: Price - 2 event(s) a client fires with no Rate: Hit, Health - 1 function(s) a client calls with no Rate or Concurrency: Price - 1 length(s) or value(s) a client sends with no bound; each is marked - Add option ServerOutput and option ClientOutput: where the two modules are written. - Set option RequireRates = true once every event a client fires has a Rate. - On the server, set SetRateLimitHandler, SetDecodeErrorHandler, SetPacketDropHandler and SetListenerErrorHandler: they are how the game hears of a refusal. - Check MaxPacketSize, MaxEventsPerPacket and InboundBytesPerSecond against what the game sends.The draft always compiles, and what it could not know is a comment, never a guess. A packet’s events come out in the order the definitions made them.
Stating the direction
Section titled “Stating the direction”A Packet packet goes either way: the client calls Fire, the server calls Fire or FireClient. A
BlinkBlox event has one direction, because the two are not alike – what a client sends is checked,
bounded and rate limited, and what the server sends is not.
Until you say otherwise the draft writes From: Client, the direction that gets the checks. State
each one, and delete the Rate mark from what the server sends:
option ServerOutput = "src/server/Network.luau"option ClientOutput = "src/shared/Network.luau"option RequireRates = true
event Hit { From: Client, Type: Reliable, Call: ManyAsync, Rate: 10, Data: (Target: Instance, Damage: u8, Note: string(..64))}
event Health { From: Server, Type: Reliable, Call: ManyAsync, Data: u8}
function Price { Yield: Coroutine, Rate: 5, Concurrency: 1, Data: u16, Return: u32}A packet the game sends both ways becomes two events, one for each direction. A packet with a
Response is a function: the client calls the server unless it
says From: Server.
The values of an event can be named, as Hit’s are above. Packet’s are known by position only.
Packet has one channel, so every event is Type: Reliable. An event that is sent every frame and
only matters when fresh can now be Unreliable.
What converts, and what does not
Section titled “What converts, and what does not”| Packet | BlinkBlox | |
|---|---|---|
Packet("Name", A, B) |
event Name { Data: (A, B) } |
A name that is not an identifier has its other characters replaced with _. |
:Response(C) |
function Name { Data, Return: C } |
|
NumberU8 .. NumberU32, NumberS8 .. NumberS32 |
u8 .. u32, i8 .. i32 |
The 24-bit ones included. |
NumberF16, NumberF24, NumberF32, NumberF64 |
f16, f24, f32, f64 |
|
String, Buffer |
string(..255), buffer(..255) |
Their one-byte length is a bound, and is kept. |
StringLong, BufferLong |
string, buffer |
Marked when a client sends them. |
Boolean8, Instance, Any |
boolean, Instance, unknown |
unknown is not validated. |
Vector3S16, Vector3F24, Vector3F32 |
vector<i16>, vector<f24>, vector |
|
Vector2S16, Vector2F24, Vector2F32 |
Vector2 |
|
CFrameF24U8, CFrameF32U8, CFrameF32U16 |
CFrame |
Its encodings are chosen in the schema. |
Color3, BrickColor, UDim, UDim2, NumberRange, ColorSequence |
the same | |
NumberU4, Boolean1, BooleanNumber |
u8(..15)[2], boolean[8], a struct |
The tables Packet reads them into. |
{ T } |
T[] |
|
{ Key = T, .. } |
struct { Key: T, .. } |
Fields in alphabetical order, as Packet sorts them. |
Nil, Rect, Region3, NumberSequence, EnumItem, Characters, Static1 .. Static3 |
unknown |
No equivalent; each is named in the list. A single enum is Enum(Material). |
| a table of several types with no names | unknown |
A struct needs names. |
A struct used by more than one packet is declared once, as Shape1, Shape2 and so on: a table
has no name the converter can see, so choose one. A type name the stand-in does not know stops the
command and says so.
The call sites
Section titled “The call sites”| Packet | BlinkBlox | |
|---|---|---|
| Client to server | Packets.Hit:Fire(...) |
Net.Hit.Fire(...) |
| Server to one player | Packets.Health:FireClient(Player, ...) |
Net.Health.Fire(Player, ...) |
| Server to everyone | Packets.Health:Fire(...) |
Net.Health.FireAll(...) |
| Listen on the server | Packets.Hit.OnServerEvent:Connect(Listener) |
Net.Hit.On(Listener) |
| Listen on the client | Packets.Health.OnClientEvent:Connect(Listener) |
Net.Health.On(Listener) |
| Call | Packets.Price:Fire(...) returns the response |
Net.Price.Invoke(...) |
| Answer | Packets.Price.OnServerInvoke = Listener |
Net.Price.On(Listener) |
Calls use a dot, not a colon, and On returns a function that disconnects the listener where
Connect returns a connection. The server also gets FireList and FireExcept.
The whole generated API is in the reference.
A bridge for the call sites
Section titled “A bridge for the call sites”To move the definitions first and the call sites later, wrap each module once:
--- A BlinkBlox module under Packet's names. Temporary: delete it once the call sites use Fire,--- FireAll, On and Invoke themselves.local function Signal(On) return { Connect = function(_, Listener) local Disconnect = On(Listener) return { Disconnect = Disconnect } end, }end
local function PacketNames(Net, IsServer) local Packets = {} for Name, Value in Net do if type(Value) ~= "table" or not (Value.Fire or Value.On or Value.Invoke) then continue end
local Packet = {} if Value.Invoke and IsServer then function Packet:FireClient(Player, ...) return Value.Invoke(Player, ...) end elseif Value.Invoke then function Packet:Fire(...) return Value.Invoke(...) end elseif Value.FireAll then function Packet:Fire(...) Value.FireAll(...) end function Packet:FireClient(Player, ...) Value.Fire(Player, ...) end elseif Value.Fire then function Packet:Fire(...) Value.Fire(...) end end
if Value.On then -- An event's listeners connect to the signal; a function's one listener is assigned. Packet.OnServerEvent = Signal(Value.On) Packet.OnClientEvent = Packet.OnServerEvent setmetatable(Packet, { __newindex = function(_, Key, Listener) if Key == "OnServerInvoke" or Key == "OnClientInvoke" then Value.On(Listener) end end, }) end
Packets[Name] = Packet end
return Packetsend
return PacketNameslocal Packets = require(ReplicatedStorage.PacketNames)(require(ServerScriptService.Network), true)
Packets.Hit.OnServerEvent:Connect(function(Player, Target, Damage, Note) -- unchangedend)Pass true on the server and false on the client. An event has only the members of its direction,
so a call the schema no longer allows is a missing method here, where under Packet it was sent.
ResponseTimeout is not carried: a call’s timeout is the schema’s
InvocationTimeout.
The wrapper is untyped, so the call sites behind it lose their type checking until they move.
Moving one packet at a time
Section titled “Moving one packet at a time”Packet and BlinkBlox each create their own remotes, so both can run in the same game:
- Record the definitions and keep only the packets you are moving in the draft.
- Delete those packets from the Packet definitions.
- Point their call sites at the new modules.
- Repeat until no packet is left, then remove Packet.
BlinkBlox’s own server and client modules have to ship together: a client refuses to run against a server compiled from a different schema. See wire compatibility.
What a running game will notice
Section titled “What a running game will notice”- A client that fires too fast is refused, and the server is told through
SetRateLimitHandler. Choose rates an honest client never reaches. - A packet the server refuses is reported, with the player who sent it: through
SetPacketDropHandlerwhen it is refused before it is read, and throughSetDecodeErrorHandlerwhen an event in it does not decode. - A listener that errors is reported through
SetListenerErrorHandler. - An event has one direction. Sending it the other way is no longer possible.
Securing the server is the checklist for all of it, and common pitfalls lists what tends to go wrong.
