
Remotes that say what they carry
A RemoteEvent takes whatever you hand it, and some of it doesn't survive the trip. A table keyed by UserId arrives with its keys turned into strings. A list stops at its first nil. An object turns up as a plain table with its methods gone. Most of the time nothing tells you. The other side just reads nil, usually once real players are in the server.
Since 16 October a remote in a ShotScript project can carry a signature, and shot check holds everything that goes through it to that line.
One line in the .instance file
In a ShotScript project a remote is a small file in its system's folder, and the file holds its class. Our example is the lap board of a kart racing game. Laps/LapBoard.instance sends the board to every player after each lap, and Laps/LapRank.instance lets a player ask where they placed. The signature goes after the class:
RemoteEvent (board : Board)
RemoteFunction (track : string) : RankReply
The parameters are what comes after the Player that Roblox adds. FireAllClients(board) sends one value, and the server's handler for LapRank gets (player, track). Only a RemoteFunction has a return type, which is what OnServerInvoke hands back. The types are the system's own, from its Types.shot:
public type Lap = { Name : string, Seconds : number }
public type Board = {
Best : {[string] : number},
Top : {Lap},
}
public type RankReply = { Place : number, Reason : string? }
A signature adds nothing to the Luau the project compiles to, so the game is the same with or without one. The scripts reach both remotes with use LapBoard and use LapRank, the same line that reaches a module, and since 20 October the checks follow a remote reached that way. This is the server sending the board when someone finishes a lap:
function Finish(player : Player, seconds : number)
local key = tostring(player.UserId)
if not best[key] or seconds < best[key] then
best[key] = seconds
end
table.insert(finished, { Name = player.Name, Seconds = seconds })
LapBoard:FireAllClients(boardFor())
end
Keys that turn into strings
Best laps keyed by UserId is the obvious way to write it, so that's how we wrote it first, with Best : {[number] : number}. With the signature on LapBoard, shot check stops at the type itself:

To see what that saves you from, we took the signature out, so the file just says RemoteEvent, and wrote a scenario. Alice finishes a lap in 41.3 seconds and Bo in 44.8, then Alice's client looks her up on the board it got. Check passed it with 0 errors. The run didn't.

The board did arrive, and both laps are in Top. Her best time is there too, under "1001" the string, so a lookup with her UserId finds nothing. In a live game that's a leaderboard showing a blank next to your name and nothing in the output. A scenario run notices keys changing as they cross, but only once a scenario sends one. The signature catches it while you're still writing the type.
With the key written as tostring(player.UserId) on both sides, the same scenario prints Alice's best on her board: 41.3 and passes.
Lists with holes
A list Roblox sends stops at its first nil. Make Top a list that can hold nil and check says so:
Kart/Laps/Types.shot:5 Board.Top : {Lap?} goes through LapBoard: a list arrives cut at the first nil, so the items after it are lost; send {Lap} without the empty ones, built with table.insert
The sneakier version has the right type and still makes holes. Fill the top laps by index inside an if, and every lap the if turns down leaves a gap:

This one passes a quick test, because every lap in it is under a minute. It goes wrong the first time a slow lap comes before a fast one. The list then stops at the gap, and the laps after it never reach the players. The board in our project uses table.insert, which leaves no gaps.
Sends and handlers that don't match
The other checks hold each Fire and each handler to the signature. One value too many on FireAllClients:
Kart/Laps/LapsServer.shot:43 LapBoard:FireAllClients sends 2 values, but its signature (board : Board) takes 1: the handler never reads the extra ones; send what the signature says, or add the parameter in Laps/LapBoard.instance
A client handler waiting for a second value that never comes:
Kart/Laps/LapsClient.shot:10 LapBoard.OnClientEvent's handler reads 2 values, but the signature (board : Board) sends 1: sentAt is always nil; drop it, or add the parameter in Laps/LapBoard.instance
An InvokeServer with the track left out:
Kart/Laps/LapsClient.shot:16 LapRank:InvokeServer sends 0 values, but track : string isn't optional: the handler gets nil for track; pass it, or make it optional (track : string?) in Laps/LapRank.instance
And the server's handler typed differently from the signature:
Kart/Laps/LapsServer.shot:47 LapRank.OnServerInvoke's handler takes track : number, but the signature sends track : string: write track : string (or any, checked before use), or change the signature in Laps/LapRank.instance
Our real handler takes track : any, and that's allowed on purpose. A client can send the server anything at all, so the handler looks at the value before it trusts it:
local function rank(player : Player, track : any) : RankReply
if track ~= "Canyon" then
return { Place = 0, Reason = "No such track" }
end
What it leaves alone
A Fire is checked by how many values it sends, and a handler by the types it declares. The values themselves aren't followed into the payload, so a table built with no type on it gets through. Holes are only found when the list is filled and put into the payload in the same function. A remote reached from another system, or passed around as a value, isn't checked.
A remote with no signature is checked the way it always was, so you can add them one remote at a time, starting with the one that carries your leaderboard.
