Installation

Pigeon is one folder of Luau files. Put it in ReplicatedStorage and require it. There is nothing to build and nothing else to install.

What you are copying

The whole library is src/Pigeon. Six files, no dependencies beyond Roblox itself.

FileWhat it is
init.luauThe module you require. Becomes the ModuleScript named Pigeon.
Carrier.luauCarriers: the named channels you send and listen on.
Network.luauThe pooled RemoteEvents and the transport underneath.
Transformer.luauTransformers: which pooled remote traffic rides on, and group teardown.
StagedTable.luauStaged tables: plain tables whose writes replicate.
Types.luauThe Luau types Pigeon exports.

Pigeon does not pull in any other library. It only asks Roblox for Players, RunService, ReplicatedStorage and HttpService.

Where the folder goes

ReplicatedStorage. Both machines require the same module, so it has to be somewhere both of them can see, and ReplicatedStorage is that place.

Careful

Do not put Pigeon in ServerScriptService or ServerStorage. The client cannot see either one, so the client half of every channel would have nothing to require. StarterPlayerScripts has the same problem in reverse.

With Rojo

Copy src/Pigeon into your project and map it into ReplicatedStorage. This is the smallest project file that does it.

{
  "name": "MyGame",
  "tree": {
    "$className": "DataModel",

    "ReplicatedStorage": {
      "Pigeon": {
        "$path": "src/Pigeon"
      }
    }
  }
}

The key on the left is the Instance name, so "Pigeon" is what you will require. The $path on the right is where the files live on disk. Rojo sees init.luau inside the folder and turns the folder itself into a ModuleScript, with the other five files as its children.

Here is the same mapping inside a fuller project, next to your own code.

{
  "name": "MyGame",
  "tree": {
    "$className": "DataModel",

    "ReplicatedStorage": {
      "Pigeon": {
        "$path": "src/Pigeon"
      },
      "Shared": {
        "$path": "src/shared"
      }
    },

    "ServerScriptService": {
      "Server": {
        "$path": "src/server"
      }
    },

    "StarterPlayer": {
      "StarterPlayerScripts": {
        "$className": "StarterPlayerScripts",
        "Client": {
          "$path": "src/client"
        }
      }
    }
  }
}

Without Rojo

You can build the same tree by hand in Studio. The names have to match exactly, because the files require each other by name.

  1. Make a ModuleScript in ReplicatedStorage and name it Pigeon.
  2. Paste the contents of init.luau into it.
  3. Make five ModuleScripts inside it, named Types, Network, Transformer, StagedTable and Carrier.
  4. Paste the matching file into each one.

The finished tree looks like this.

ReplicatedStorage
└── Pigeon            (ModuleScript, from init.luau)
    ├── Carrier
    ├── Network
    ├── StagedTable
    ├── Transformer
    └── Types

String requires

Four of the five inner files require their neighbours by string, like this:

-- inside Carrier.luau
local types = require("./Types")
local Network = require("./Network")

"./Types" means "the module called Types next to me". That is a string require, and your place has to allow it. If string requires are off, the first inner require errors and Pigeon will not load.

init.luau is the exception. It uses ordinary Instance requires, so the entry point itself does not depend on the setting:

-- inside init.luau
local Types = require(script.Types)
local Network = require(script.Network)

Note

If you cannot turn string requires on, you can swap them yourself. Every require("./Name") means exactly the same thing as require(script.Parent.Name). Types.luau has no requires at all, so there are only four files to touch, and Pigeon then works without the setting.

Requiring it

From a server Script, usually in ServerScriptService:

Server
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Pigeon = require(ReplicatedStorage.Pigeon)

local shop = Pigeon.new("Shop")

shop:On("Buy", function(player, itemId)
	shop:BroadcastTo({ player }, "Stock", itemId, 3)
end)

From a LocalScript, usually in StarterPlayerScripts:

Client
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Pigeon = require(ReplicatedStorage:WaitForChild("Pigeon"))

local shop = Pigeon.new("Shop")

shop:On("Stock", print)
shop:Init()

shop:Emit("Buy", "sword")

The server handler gets the sending player first. The client handler does not, because on the client there is only ever one sender.

Note

Use WaitForChild on the client. Your LocalScript can run before the Pigeon folder has replicated, and plain ReplicatedStorage.Pigeon errors when it has not arrived yet. On the server it is already there, so the plain index is fine.

What Pigeon adds at runtime

The server creates one Folder called PigeonRefs in ReplicatedStorage and puts the pooled remotes inside it. They are named PigeonRef_1, PigeonRef_2 and so on, each with a matching PigeonRefU_1 unreliable twin. The folder also carries a RefCount attribute so clients know how big the pool is.

ReplicatedStorage
└── PigeonRefs        (Folder, created by the server)
    ├── PigeonRef_1   (RemoteEvent)
    └── PigeonRefU_1  (UnreliableRemoteEvent)

Careful

Do not create, rename or delete anything in PigeonRefs yourself. Pigeon owns that folder. Remove or rename a ref and everything that needed it is held on the client for 30 seconds and then dropped, and the channels riding on it never open.

Checking it works

Drop these two scripts in, press play, and look at the output. If you see the message on the client, everything is wired up.

Server
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Pigeon = require(ReplicatedStorage.Pigeon)

local test = Pigeon.new("InstallCheck")

Players.PlayerAdded:Connect(function(player)
	test:BroadcastTo({ player }, "Hello", player.Name)
end)
Client
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Pigeon = require(ReplicatedStorage:WaitForChild("Pigeon"))

local test = Pigeon.new("InstallCheck")

test:On("Hello", function(name)
	print("Pigeon works. Hello,", name)
end)

test:Init()

The server fires on PlayerAdded, which almost certainly happens before your LocalScript has finished loading. It still arrives, because the server holds it until the client calls Init. That is Startup and Buffering doing its job on your first run.

Next