Start typing to search packages!
wire
By @mrkirdid
Roblox
Mirrored from WallyWire
Typed networking for Roblox. You write each event, function or replicated value once, with schemas. From that you get:
- typed
Fire,Connect,CallandGet, in both the old and new Luau type solvers - buffer encoding, sized to fit each value
- one batched remote call per player per frame
- validation of every value a client sends
There's no build step and no dependencies.
[dependencies]
Wire = "mrkirdid/wire@^1.0.0"
Define
One module per feature, somewhere both sides can require it (e.g. Shared/Net/Shop.luau):
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Wire = require(ReplicatedStorage.Shared.Packages.Wire)
local T = Wire.T
return Wire.Namespace("Shop", {
Buy = Wire.FromClient(T.String(40), T.Int(1, 99)),
Bought = Wire.FromServer(T.String(40)),
Price = Wire.Function(T.String(40)):Returns(T.Int(0, 1000000)),
SaleActive = Wire.Value(T.Boolean(), false),
Cash = Wire.PlayerValue(T.Int(0, 1000000), 0),
})
Use
-- server
local Shop = require(Shared.Net.Shop)
Shop.Buy.Server:Connect(function(Player, ItemId, Amount)
-- ItemId is a string of at most 40 chars and Amount a whole number from 1 to 99, already checked
Shop.Bought.Server:Fire(Player, ItemId)
end)
Shop.Price.Server:SetHandler(function(Player, ItemId)
return 250
end)
Shop.SaleActive.Server:Set(true)
Shop.Cash.Server:Set(Player, 1200)
-- client
local Shop = require(Shared.Net.Shop)
Shop.Buy.Client:Fire("Sword", 1)
Shop.Bought.Client:Connect(function(ItemId) end)
local Price = Shop.Price.Client:Call("Sword") -- yields, errors on failure or timeout
Shop.Cash.Client:Observe(function(Cash) end) -- runs now, then on every change
Every definition has a .Server half and a .Client half. Each side only sees what it can call, and touching the wrong half errors with a message saying so.
Definitions
| Server | Client | |
|---|---|---|
Wire.FromClient(...) | Connect(fn(Player, ...)), Once | Fire(...) |
Wire.FromServer(...) | Fire(Player, ...), FireAll, FireExcept(Player, ...), FireList({Player}, ...) | Connect(fn(...)), Once |
Wire.Function(...):Returns(Schema) | SetHandler(fn(Player, ...) -> value) | Call(...) -> value |
Wire.Value(Schema, Default) | Get(), Set(value) | Get(), Observe(fn) |
Wire.PlayerValue(Schema, Default) | Get(Player), Set(Player, value) | Get(), Observe(fn) (its own value) |
Options chain off a definition:
:RateLimit(PerSecond, Burst?): per player, onFromClientandFunction. The default is 100/s. Anything over the limit is dropped before it's decoded.:Unreliable(): on events. It usesUnreliableRemoteEvent, has a 900-byte limit (instance references count toward it) and is never batched. It can carry Instances but notT.Any. Roblox doesn't hold unreliable messages for clients that aren't listening yet.:Timeout(Seconds): onFunction. The default is 10s.
Connect, Once and Observe return a connection with :Disconnect().
Schemas (Wire.T)
| Schema | Bytes | Notes |
|---|---|---|
T.Boolean() | 1 | |
T.Int(Min?, Max?) | 1–4 (8 past 32 bits) | Picks the smallest fit: Int(0, 255) is 1 byte. Whole numbers only. |
T.Number(Min?, Max?) | 8 | Full precision. |
T.Float(Min?, Max?) | 4 | About 7 digits. Use it for positions, never for ids or money. |
T.String(Max = 4096) | length + 1 | Max is in bytes. Must be valid UTF-8. |
T.Literal("A", "B", ...) | 1 | One of a fixed set of strings. |
T.Buffer(Max = 32768) | length + 1 | |
T.Vector3() / T.Vector2() | 12 / 8 | f32 |
T.CFrame() | 24 | Position plus axis-angle; always reads back as a valid rotation. |
T.Color3() | 3 | |
T.UDim2() | 16 | |
T.EnumItem(Enum.X) | 1–2 | |
T.Instance(Class?), T.Player() | 0 | The instance goes as a reference. If the receiver can't see it (not replicated or streamed out), it arrives as nil, so wrap it in Optional. |
T.Optional(S) | 1 + S | |
T.Array(S, Max = 1024) | count + items | |
T.Map(K, V, Max = 1024) | count + pairs | |
T.Struct({ Field = S, ... }) | sum of fields | Cast it for the type: T.Struct({...}) :: Wire.Schema<{ Name: string }>. |
T.Any() | 0 | Roblox serialization, not validated. Treat anything a client sends this way as hostile. |
NaN and infinity are refused everywhere. A bad value fails at the sender's own line, and the error names the route and the argument (Shop.Buy: argument 2: expected integer in 1..99, got number 120). A bad value from a client means the message is dropped and the server warns once per 10 seconds per player.
What it fixes compared to KirNet
- Typed per side, from the schemas. No casts or type-gen extension needed.
- Validated on the server. Each type is checked, along with ranges, string and array limits, NaN, unknown routes, lengths that run past the end, and leftover bytes. Spam is dropped before decoding.
- Connections can be disconnected, and only your own listener is removed.
- No dropped messages at client startup. Messages the server fires before the client connects are held and delivered when it does. Messages for a module the client hasn't required yet are replayed when it does.
Callnever returns a silent nil. It errors on a timeout, a server error or a missing handler. The server's error details stay on the server.- Values catch late joiners up and use one serializer.
- Order is kept. A
FireAllfollowed by aFirearrives in the same order. - Nothing is lossy unless you ask for it. Floats are only rounded where you chose
T.Float.
Good to know
- The module that defines a namespace has to be required on the server too. It always is if a service uses it. Otherwise the client warns after 10s.
- Arguments are typed up to six per event. Past that, send one
Struct. - The server does not hold client messages: connect your
.Serverlisteners inInit. A client message that arrives with nothing connected is dropped, and the server warns once. - One frame's traffic is split into more than one packet when it passes 64KB or 256 instance references. A single message over those limits is refused at the call.
- A namespace name must be an identifier (letters, digits and
_), and each definition can be registered only once. - Both sides must run the same definitions. If they don't, messages fail their "used up exactly" check and get dropped loudly.
Developing
rojo build test.project.json -o WireTest.rbxlx
Open it in Studio and press Play. The output should end with [WireTest] client: 22 passed, 0 failed and [WireTest] server: 7 passed, 0 failed.
Package Details
Install command (Click to copy)
Version
1.0.0
License
MIT
Safe for commercial use
Automated license review — not legal advice.
