Coming from RemoteEvents
A game on plain remotes has no definitions to convert: what a RemoteEvent carries is whatever the
call sites pass it, and what the server accepts is whatever each listener checks by hand. Moving to
BlinkBlox means writing that down once, as a schema, and letting the generated modules do the
checking.
There is no command for this one. The schema is short to write, and the remotes you have already tell you what goes in it.
A game on a networking library has a guide of its own, and for most a converter: zap, ByteNet, Packet, QuickNet, Warp and Blink.
The short version
Section titled “The short version”- Install
blinkblox. See Installation. - List the remotes, and for each one who fires it and what it carries.
- Write an
eventfor eachRemoteEventand afunctionfor eachRemoteFunction, with a type for each argument. - Give every event a client fires a
Rate, and bound every string and array a client sends. - Compile it:
blinkblox net. The generated modules use two remotes of their own, so the remotes you have keep working and events 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.
Writing the schema
Section titled “Writing the schema”Take one remote at a time. Its listener on the server already says most of what the schema needs:
Remotes.Hit.OnServerEvent:Connect(function(Player, Target, Damage) if typeof(Target) ~= "Instance" or not Target:IsA("Model") then return end if type(Damage) ~= "number" or Damage ~= Damage or Damage < 0 or Damage > 100 then return end
-- the game's own rules: is the target in reach, is the player aliveend)
Remotes.Health:FireClient(Player, Humanoid.Health)
Remotes.Price.OnServerInvoke = function(Player, ItemId) return Prices[ItemId] or 0endEach remote becomes a declaration, and each check on an argument becomes that argument’s type:
option ServerOutput = "src/server/Network.luau"option ClientOutput = "src/shared/Network.luau"option RequireRates = true
event Hit { From: Client, Type: Reliable, Call: SingleAsync, Rate: 10, Data: (Target: Instance(Model), Damage: u8(..100))}
event Health { From: Server, Type: Unreliable, Call: SingleSync, Data: u8}
function Price { Yield: Coroutine, Rate: 5, Concurrency: 1, Data: u16, Return: u32}| What you have | What to write |
|---|---|
a RemoteEvent the client fires |
event with From: Client and a Rate |
a RemoteEvent the server fires |
event with From: Server |
a RemoteEvent fired both ways |
two events, one for each direction |
an UnreliableRemoteEvent |
Type: Unreliable |
a RemoteFunction the client invokes |
function |
a RemoteFunction the server invokes |
function with From: Server |
| a listener that may yield | Call: SingleAsync, or ManyAsync for several listeners |
| a listener that never yields | Call: SingleSync or ManySync |
typeof(X) == "Instance" and X:IsA("Model") |
Instance(Model) |
| a number checked against a range | u8(..100), f32(-1..1) |
| a string checked for length | string(1..32) |
| a table of fixed fields | a struct |
| a list | T[..16] |
| a value that may be nil | T? |
The types and their bounds are in Types, an event’s fields in Events, a function’s in Functions.
An event may carry several values, as a remote’s arguments do. Naming them, as Hit’s are above, is
optional.
What the checks become
Section titled “What the checks become”The checks at the top of a listener are what a schema replaces. Once an argument has a type, a value of another type, outside its range or over its length never reaches the listener: the event is not delivered, the rest of its packet is dropped, and the decode-error handler is told who sent it.
So the listener keeps only the game’s own rules:
Net.Hit.On(function(Player, Target, Damage) -- Target is a Model and Damage is a whole number from 0 to 100. -- the game's own rules: is the target in reach, is the player aliveend)What a schema cannot check is anything about the game: that the target is in reach, that the player owns the item, that the price is right. Those stay.
Two things the hand-written code usually did not do at all:
- A rate limit. A remote accepts as many calls as a client makes.
RateandBurstare the events a second accepted from one player and how many may arrive at once; withoption RequireRatesan event a client fires cannot be added without one. - A bound on length. A remote accepts a string or a table of any size. Bound each one a client
sends:
string(1..32),u16[..64].
The call sites
Section titled “The call sites”| Remotes | BlinkBlox | |
|---|---|---|
| Client to server | Remotes.Hit:FireServer(...) |
Net.Hit.Fire(...) |
| Server to one player | Remotes.Health:FireClient(Player, ...) |
Net.Health.Fire(Player, ...) |
| Server to everyone | Remotes.Health:FireAllClients(...) |
Net.Health.FireAll(...) |
| Listen on the server | Remotes.Hit.OnServerEvent:Connect(Listener) |
Net.Hit.On(Listener) |
| Listen on the client | Remotes.Health.OnClientEvent:Connect(Listener) |
Net.Health.On(Listener) |
| Call | Remotes.Price:InvokeServer(...) |
Net.Price.Invoke(...) |
| Answer | Remotes.Price.OnServerInvoke = Listener |
Net.Price.On(Listener) |
Calls use a dot, not a colon. On returns a function that disconnects the listener where Connect
returns a connection. The server also gets FireList and FireExcept, which send to a list of
players and to all but one.
There is nothing to create or to wait for: no Instance.new("RemoteEvent"), no WaitForChild. The
server module makes its two remotes when it is required, and the client module finds them.
The whole generated API is in the reference.
A bridge for the call sites
Section titled “A bridge for the call sites”To write the schema first and move the call sites later, wrap each module once. The wrapper answers to a remote’s own methods:
--- A BlinkBlox module under the names of RemoteEvent and RemoteFunction. Temporary: delete it once--- the call sites use Fire, FireAll, On and Invoke themselves.local function Signal(On) return { Connect = function(_, Listener) return { Disconnect = On(Listener) } end, }end
local function RemoteNames(Net, IsServer) local Remotes = {} for Name, Value in Net do if type(Value) ~= "table" or not (Value.Fire or Value.On or Value.Invoke) then continue end
local Remote = {} if Value.Invoke and IsServer then function Remote:InvokeClient(Player, ...) return Value.Invoke(Player, ...) end elseif Value.Invoke then function Remote:InvokeServer(...) return Value.Invoke(...) end elseif Value.FireAll then function Remote:FireClient(Player, ...) Value.Fire(Player, ...) end function Remote:FireAllClients(...) Value.FireAll(...) end elseif Value.Fire then function Remote:FireServer(...) Value.Fire(...) end end
if Value.On then -- An event's listeners connect to the signal; a function's one listener is assigned. Remote.OnServerEvent = Signal(Value.On) Remote.OnClientEvent = Remote.OnServerEvent setmetatable(Remote, { __newindex = function(_, Key, Listener) if Key == "OnServerInvoke" or Key == "OnClientInvoke" then Value.On(Listener) end end, }) end
Remotes[Name] = Remote end
return Remotesend
return RemoteNameslocal Remotes = require(ReplicatedStorage.RemoteNames)(require(ServerScriptService.Network), true)
Remotes.Hit.OnServerEvent:Connect(function(Player, Target, Damage) -- unchanged, less the checksend)Pass true on the server and false on the client. An event has only the members of its direction,
so a call the schema does not allow – a client firing an event declared From: Server – is a
missing method here.
The wrapper is untyped, so the call sites behind it lose the type checking the generated modules give until they move.
Moving one remote at a time
Section titled “Moving one remote at a time”The generated modules do not touch the remotes a game already has, so nothing has to move at once:
- Write the schema with the remotes you are moving first.
- Point their call sites at the generated modules, and delete those remotes.
- Add the next remotes to the schema, compile, and repeat.
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”- Reliable events are batched. A reliable send is written into a batch that goes out on the next
Heartbeat, so many small events cost one remote call. See Bandwidth. - 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 send can throw. A string or an array outside its bounds is an error where it is sent, and with
WriteValidationsso is a value of the wrong type or range. A remote would have sent it.
Securing the server is the checklist for all of it, and common pitfalls lists what tends to go wrong.
