Guide: Watching Remotes
Seeing what a game sends to its server, replaying it, and reading the raw packet stream underneath.
Almost everything a game does that matters happens over a RemoteEvent or a RemoteFunction. Watching that traffic tells you what arguments the server expects, which is the difference between guessing at a script and writing one that works.
There are three levels to this, from friendliest to rawest: hook __namecall, inspect the signal's connections directly, or hook the RakNet packet stream.
The same hook from the hooking guide, tightened into something you would actually leave running.
local old
local logging = true
old = hookmetamethod(game, "__namecall", newcclosure(function(self, ...)
local method = getnamecallmethod()
if logging and not checkcaller() then
if method == "FireServer" or method == "InvokeServer" then
local args = {...}
rconsoleprint(("[%s] %s\n"):format(method, self:GetFullName()))
for i, v in ipairs(args) do
rconsoleprint((" [%d] %s = %s\n"):format(i, typeof(v), tostring(v)))
end
end
end
return old(self, ...)
end))
rconsolecreate()
rconsolesettitle("remote spy")Log to rconsole, not print
A busy game will flood the editor console faster than you can read it. A dedicated rconsole window scrolls independently and survives the editor being covered.
Once you know the shape of a call you can make it yourself. For server-bound traffic that is just calling the remote. For *client*-bound signals — the ones the server fires at you — firesignal runs the connected handlers locally without any network involvement.
local remote = game:GetService("ReplicatedStorage"):WaitForChild("Buy")
-- Server-bound: a normal call.
remote:FireServer("sword", 1)
-- Client-bound: run whatever the game connected, locally.
local incoming = game:GetService("ReplicatedStorage"):WaitForChild("Reward")
firesignal(incoming.OnClientEvent, 500)
-- Or inspect the connections instead of firing blind.
for _, connection in ipairs(getconnections(incoming.OnClientEvent)) do
print(connection.Function, connection.State)
endgetconnections gives you the handler functions themselves, which you can then feed to debug.getupvalues to read the state they close over. cansignalreplicate tells you whether a given signal can be pushed back across the network boundary with replicatesignal.
Under the remotes sits the actual packet stream. raknet.add_send_hook gives you every outgoing packet before it leaves, with its id, its payload and its reliability, and lets you block it.
local hook = raknet.add_send_hook(function(packet)
-- Log the id and size of everything leaving.
print(("id=%d size=%d channel=%d"):format(packet.Id, packet.Size, packet.OrderingChannel))
-- Drop a specific packet type entirely.
if packet.Id == 83 then
packet:Block()
end
end)
-- ... later
raknet.remove_send_hook(hook)Blocking packets breaks things
The client and server share state assumptions. Dropping the wrong packet gets you desynced, kicked, or worse. raknet.desync exists for the cases where that is what you want, deliberately and briefly.
See Signals for firesignal, getconnections, replicatesignal and the connection proxy; Metatable for the namecall hook; RakNet for the packet-level surface; and Console for the rconsole output used above.