v1.4.0 — Networking on Twill's own wire format
Net has a built-in wire format, and a corrupt call costs only itself.
Before upgrading
Section titled “Before upgrading”Declarations are the only call sites that change.
-- beforelocal Packet = require("@game/ReplicatedStorage/Twill/Packages/Packet")Net.Declare("BuyItem", { Packet.String }, { Packet.Boolean8 })
-- afterlocal Types = Net.TypesNet.Declare("BuyItem", { Types.String(32) }, { Types.Boolean })Handle, IsHandled, Get and List are unchanged. Twill.Packet and the
bundled Packet package are removed.
A corrupt call costs only itself
Section titled “A corrupt call costs only itself”Twill.Net has its own wire format, and every call carries
the length of its own body. That field, four bytes per call, does three things:
- A corrupt call stops at its own frame. The reader moves to where the body ends whether or not reading it worked, so the calls behind it still arrive.
- A refused call is never decoded. Rate limiting skips a call it has already decided to drop.
- A hostile length is bounded. It is checked against the frame it arrived in before anything is allocated.
1 = payload1 | 2 = refused | 3 = payload3Remotes are declared with Net.Types
Section titled “Remotes are declared with Net.Types”local Net = require("@game/ReplicatedStorage/Twill/Net")local Types = Net.Types
local buy = Net.Declare("BuyItem", { Types.String(32) }, { Types.Boolean })local aim = Net.DeclareUnreliable("Aim", { Types.Vector3F32 })Both sides compile the same declaration, so no type information travels. A struct sends its values without field names, and a thousand booleans take 125 bytes.
Types.String(32) is a ceiling enforced at the sender:
BuyItem argument 1 is 300 bytes, past its ceiling of 32Before, a string past 255 bytes wrapped its length prefix and corrupted everything written after it.
Clients cannot name players or reach server threads
Section titled “Clients cannot name players or reach server threads”Types.Player is refused in an argument list. The caller already arrives as the
handler’s first argument, from the engine:
'Trade' argument 1 names a player, which only the server may say;the caller is given to the handler alreadyOnly a client asks questions. The server never waits on a client, and the number that identifies a question is looked up only in the asker’s own table.
An Instance sent by a client is still the client’s choice. The decoder checks
that it exists and is the class you named, but not that the caller may touch it.
Check that in Validate.
Metering happens before decoding
Section titled “Metering happens before decoding”The bytes of a whole message are weighed before it is opened, and each call is metered by name before its arguments are decoded. Batching many calls into one message costs what those calls weigh. A joining player is metered from their first call.
Calls wait for numbering
Section titled “Calls wait for numbering”A remote declared on a client receives its number from the server a moment later. A call made before then is held and sent once the number arrives:
Net.OnReady(function() -- every remote declared here has its numberend)Replication waits on Net.OnReady instead of polling for up to 30 seconds.
Other changes
Section titled “Other changes”- Half floats round-trip very small values, both infinities, and NaN.
Types.CFrameRotrefuses a CFrame that scales, skews or mirrors, instead of flattening it.Types.CFramekeeps all twelve components.- A constant outside a declared set is refused when sent, instead of arriving as
nil. - A union tag naming an undeclared member is refused, instead of selecting an arbitrary decoder.
Net.Listreports each remote’s number alongside its signature.
Full changelog
Section titled “Full changelog”Every change in this release is listed in the changelog.