Pigeon
The module you get back from require. It has four functions and two
fields. Most games only ever call the first function.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Pigeon = require(ReplicatedStorage.Pigeon)
| Member | What it is |
|---|---|
Pigeon.new | Looks up or builds a carrier. |
Pigeon.StagedTable | Builds a staged table. |
Pigeon.Transformer | Builds a transformer. |
Pigeon.GetRef | Resolves the RemoteEvent a transformer rides on. |
Pigeon.Network | The low level transport. |
Pigeon.Types | The types module, which is just true at runtime. |
The same module runs on the server and on the client. Nothing here is one sided. It is the carrier methods that split, and those are on the Carrier page.
Functions
Pigeon.new Shared
Pigeon.new(name: string, options: {Unreliable: boolean?, Timeout: number?, Transformer: Transformer?}?) -> PigeonCarrier
Returns the carrier for a channel name. The name is the channel and the only thing
both machines have to agree on. Build Pigeon.new("Shop") on the server
and on the client and those two are talking.
| Parameter | Type | What it is |
|---|---|---|
name | string | The channel name. Must not be empty. |
options | table? | Transport settings. Passing one at all skips the cache. |
Returns a carrier. See Carrier for what you can do with it.
local shop = Pigeon.new("Shop")
shop:When("Buy", function(player, itemId)
return giveItem(player, itemId)
end)
local shop = Pigeon.new("Shop")
shop:On("StockChanged", updateShopUi)
shop:Init()
print(shop:Call("Buy", "sword"))
The caching rule
There are two behaviours in one function. The options table is the switch.
Pigeon.new
local a = Pigeon.new("Shop")
local b = Pigeon.new("Shop")
print(a == b) --> true
local c = Pigeon.new("Shop", { Timeout = 3 })
local d = Pigeon.new("Shop", { Timeout = 3 })
print(c == d) --> false
Options describe one particular carrier. If Pigeon handed back a cached one your settings would be thrown away without a word, so it builds a fresh carrier instead.
Careful
The only check is whether options is nil. Even
Pigeon.new("Shop", {}) skips the cache, because an empty table is
still not nil. Pass nothing when you want the shared carrier.
The cache never hands back a dead carrier. A cached carrier removes itself when it
is dropped, whether that was carrier:Destroy() or its transformer being
destroyed, so the next call builds a live replacement. A carrier that comes back
already dropped is not cached at all.
local shop = Pigeon.new("Shop")
shop:Destroy()
print(Pigeon.new("Shop") == shop) --> false
The options table
| Field | Type | Default | What it does |
|---|---|---|---|
Unreliable |
boolean? |
false |
Sends fire and forget messages over the lossy lane. |
Timeout |
number? |
10 seconds |
How long a request waits for an answer. Call, CallTo, Ping, Handshake and RequestTable all read it. |
Transformer |
Transformer? |
one built from name |
The transformer this carrier rides on. |
Unreliable has to be exactly true. Anything else, including
a truthy value like 1, leaves the carrier reliable. See
Unreliable Sending.
Timeout is stored on the carrier and left as nil when you
do not pass one, and the transport falls back to 10 seconds at send time. Both
Unreliable and Timeout stay on the carrier as plain fields,
so you can change them later.
local chat = Pigeon.new("Chat")
chat.Timeout = 3
chat.Unreliable = true
Transformer puts several named channels on one remote and lets you drop
them all in one call. Leave it out and the carrier builds its own transformer using
the channel name as the uuid. See Transformers.
Errors
| Error | When |
|---|---|
ERR_NO_NAME |
name is not a string, or is the empty string. |
ERR_NO_TRANSFORMER |
options.Transformer was given but is not a table with a string uuid. |
Pigeon.new("") -- ERR_NO_NAME
Pigeon.new("Shop", { Transformer = "Combat" }) -- ERR_NO_TRANSFORMER
Both are raised inside Pigeon, one level up from the carrier constructor, so the traceback lands on Pigeon's own line rather than on the call that made the carrier. The error name is what tells you which of the two went wrong.
Warning
Because options build a fresh carrier every time, you can end up with two
carriers on one name. Incoming messages are routed by name alone, so the two
share the channel: a packet goes to every carrier that bound its event with
On or When, and every listener they registered for it
runs. A carrier that
did not bind that event is skipped, its middleware included. Keep the receiving
side to a single carrier per name. See
Known Issues.
Pigeon.StagedTable Shared
Pigeon.StagedTable(initial: {[any]: any}?) -> StageTable
Builds a staged table: a plain table whose writes replicate to the clients holding it. You write to it on the server and every client that has it sees the change.
| Parameter | Type | What it is |
|---|---|---|
initial | {[any]: any}? | Starting contents. Deep copied in, not held by reference. |
Returns a staged table. See StagedTable.
local state = Pigeon.StagedTable({ round = 1, scores = {} })
local carrier = Pigeon.new("Match")
carrier:CaptureTable("match", state)
local t = state:GetTable()
t.round = 2
t.scores.Alice = 10
Making one does not publish it. Nothing replicates until a server carrier captures
it with CaptureTable or pushes it out with ForceTable.
Clients get theirs from RequestTable rather than by building one.
Careful
initial is checked as it is copied, and a bad value errors right
there rather than later on the wire. Staged tables carry plain Roblox data only.
| Error | What you put in |
|---|---|
ERR_STAGE_UNSUPPORTED | A type that cannot travel as data, such as an Instance. |
ERR_STAGE_BEHAVIOUR | A function or a thread. |
ERR_STAGE_METATABLE | A table with a metatable, which includes class instances. |
ERR_STAGE_CYCLE | A table that refers back into itself. |
ERR_STAGE_TABLE_KEY | A table used as a key. |
Each message names the path that broke it, like root.stats.health, so
you do not have to hunt for the offending field.
Note
The function is called StagedTable but the Luau type is called
StageTable. That is not a typo on this page. See
Types.
Pigeon.Transformer Shared
Pigeon.Transformer(uuid: string?) -> Transformer
Builds a transformer. A transformer decides which pooled RemoteEvent traffic rides on, and it is the switch that turns a group of carriers off.
| Parameter | Type | What it is |
|---|---|---|
uuid | string? | The identity to adopt. Left out, you get a fresh GUID. |
Returns a transformer. See Transformer.
local combat = Pigeon.Transformer("Combat")
local damage = Pigeon.new("Damage", { Transformer = combat })
local status = Pigeon.new("Status", { Transformer = combat })
-- One call drops both channels and every listener on them.
combat:Destroy()
The uuid is what gets hashed to pick a remote, so pass the same string on both machines when both of them build one. A generated GUID is fine when the transformer only exists to group things on one side.
Note
A carrier with no Transformer option builds one using its channel
name as the uuid. So Pigeon.Transformer("Shop") hashes to the same
remote as a plain Pigeon.new("Shop") carrier. Sharing a remote is
harmless. A carrier only hears events sent on its own channel name.
Building one has a side effect: it counts toward the number of transformers Pigeon knows about, which is what the remote pool is sized from. On the server it also creates the remote for that bucket straight away and republishes the pool size so clients can find it. Destroying the transformer takes the count back down. See The Ref Pool.
Pigeon.GetRef Shared Yields
Pigeon.GetRef(transformer: Transformer) -> RemoteEvent
Resolves the pooled RemoteEvent a transformer's traffic goes out on. Carriers do this for you, so you rarely need it. It is handy when you want to see which remote a channel actually landed on.
| Parameter | Type | What it is |
|---|---|---|
transformer | Transformer | The transformer to resolve. |
Returns the reliable RemoteEvent serving that transformer.
local combat = Pigeon.Transformer("Combat")
local remote = Pigeon.GetRef(combat)
print(remote.Name) --> PigeonRef_1
print(remote.Parent.Name) --> PigeonRefs
The remotes live in a folder called PigeonRefs under
ReplicatedStorage. Each bucket has a reliable
PigeonRef_N and an UnreliableRemoteEvent twin called
PigeonRefU_N. This function only gives you the reliable one.
Careful
On the client this yields until the remote has replicated from the server. On the server it creates the remote if it is not there yet, so it returns straight away.
Do not hold on to what you get back. The pool is one remote per 16 transformers, at least 1 and at most 32, so the pool grows and shrinks as transformers come and go, and the same uuid can move to a different bucket when it does. Ask again each time.
It also does not care whether the transformer was destroyed. A destroyed transformer still hashes to a bucket, so you still get a remote back.
Fields
Pigeon.Network Shared
Pigeon.Network: Network
The low level transport that carriers are built on. It owns the remote pool, the buffering, the call timeouts and the raw send functions. You do not need it for normal work, but it is the place to look when you want numbers.
print(Pigeon.Network.KnownTransformers) -- how many transformers exist here
print(Pigeon.Network.GetRefCount()) -- how many remotes the pool holds
local counts = Pigeon.Network.Diagnostics()
print(counts.pending, counts.bufferedPackets, counts.outbound)
Full list on the Network page.
Pigeon.Types Shared
Pigeon.Types: boolean
The types module. It holds nothing but export type declarations, and
the file returns true, so this field is the boolean true at
runtime. There is nothing in it to read or call.
print(Pigeon.Types) --> true
print(typeof(Pigeon.Types)) --> boolean
Careful
You cannot write Pigeon.Types.PigeonCarrier as a type annotation.
The types are re-exported on the module itself, so write
Pigeon.PigeonCarrier.
Exported types
Five types are exported from the module for strict mode. They are re-exports of the ones in the types module.
local shop: Pigeon.PigeonCarrier = Pigeon.new("Shop")
local combat: Pigeon.Transformer = Pigeon.Transformer("Combat")
local state: Pigeon.StageTable = Pigeon.StagedTable()
| Type | What it describes |
|---|---|
Pigeon.PigeonCarrier | A carrier. |
Pigeon.Transformer | A transformer. |
Pigeon.StageTable | A staged table. |
Pigeon.MiddlewareFunction | An incoming or outgoing middleware function. |
Pigeon.StageGuard | A capture or release guard on a staged table. |
Pigeon.Transformer is both a function and a type. Luau keeps values and
types apart, so the same name works in both places and nothing clashes.
See Types for the full definitions.