Forest Logo
search
package_2

roblox-net

By @hurreman

Roblox

Mirrored from Wally

roblox-net

One declared set of remotes per game, shared by server and client. It merges two existing approaches:

  • Slime Nebula's networking layer: EventsConfig, RemoteBinder, the player and global event queues, and TokenBucket.
  • The Net module from Magic Neighborhood and Steal an Egg: one list of remotes, and "only fire to clients that are listening".
# wally.toml
[dependencies]
Net = "hurreman/roblox-net@0.1.0"
-- src/shared/Remotes.luau: required by server and client alike
local Net = require(ReplicatedStorage.Packages.Net)

return Net.new({
	remotes = {
		DataSync = { kind = "event" },
		Notify = { kind = "event", throttle = 10 },                    -- s->c: at most 10/s per player
		Swing = { kind = "event", limit = { burst = 3, perSecond = 2 } }, -- c->s: token bucket per player
		EventStream = { kind = "unreliable" },
		AdminList = { kind = "function" },
	},
	-- layout = "folder" (default: ReplicatedStorage.Remotes) | "byType" (RemoteEvents / RemoteFunctions / UnreliableRemoteEvents)
	-- readyRemote = "NetReady" (default) | false
	-- onLimited = function(player, name) end   -- a refused client request
})

The list form works too: remotes = Net.fromLists({ events = {...}, unreliable = {...}, functions = {...} }).

On the server, requiring the module creates every remote at once, in one replication step. On the client, the accessors wait for them.

Server → client

CallDelivery
net:SendTo(player, name, ...)Queued until that client is ready, then delivered in order; trailing nils are kept. Returns false if the remote's throttle dropped it, or if the name is unknown or is a function remote.
net:SendToAll(name, ...)SendTo for every player.
net:Broadcast(name, ...)Ready clients only, immediately, never queued. Use it for cosmetic cues a loading client should simply miss.
net:QueueGlobal(name, { args }, expiresIn?) / net:ClearGlobal(name)World state replayed to each client when it becomes ready (default lifetime 30 s). Clear it when a newer state replaces it.
net:InvokeOn(player, name, ...)Yields for the client's answer (after it's ready); raises if the client's handler errors.

A client becomes ready in one of two ways:

  • it calls net:SignalReady() once its listeners are connected;
  • the server calls net:MarkReady(player) itself, for example from a data hello.

net:IsReady(player), net:MarkNotReady(player) and net:Queued(player) round this out.

A player's queue, readiness, throttle windows and buckets are dropped when they leave, and a late MarkReady for someone who already left is ignored.

Client → server

net:OnServerEvent("Swing", function(player, targetId) ... end)    -- refused calls never reach the handler
net:OnServerInvoke("AdminList", function(player) return list end)  -- a refused call returns nil

limit is a token bucket per player and remote: up to burst requests at once, refilling at perSecond. For limits keyed by something else, such as per target or per action, use Net.TokenBucket directly.

On the client, use net:OnClientEvent(name, fn), net:FireServer(name, ...) and net:InvokeServer(name, ...). The raw instances are available from net:Event(name), net:Unreliable(name) and net:Function(name); each checks the name and kind.

Remotes that aren't declared

net:Get(name, className, waitForServer?) creates a remote on demand on the server, or waits for it on the client, in the same folders. This is Slime Nebula's RemoteBinder.get. A remote that exists with the wrong class is replaced on the server.

Net.Binder.new({ clientFallback = true }) keeps RemoteBinder's old client behaviour: after a timeout it creates a local stand-in with no server behind it. This is off by default.

Studio Edit mode

RunService:IsServer() is false in Edit mode, so a plain Net.new there acts as a client. Edit-mode tools and plugins pass server = true to create the remotes. Handlers can't be connected in Edit mode: Roblox refuses OnServerEvent and OnServerInvoke there.

Moving the games over

  • Steal an Egg and Magic Neighborhood:
    1. shared/Net.luau becomes return Net.new({ remotes = Net.fromLists({ events = EVENTS, unreliable = UNRELIABLE, functions = FUNCTIONS }) }), with the same Remotes folder.
    2. Rename the calls: Net.event(x) to net:Event(x), Net.unreliable(x) to net:Unreliable(x), Net.func(x) to net:Function(x), Net.markReady(p) to net:MarkReady(p), and Net.fireReady(Net.event(x), ...) to net:Broadcast(x, ...).
    3. Handlers that validate rate by hand (ProductService's prompt cooldown, for example) can move to a limit.
  • Slime Nebula:
    1. EventsConfig entries become remote specs: { type = "RemoteEvent", throttle = n } maps to { kind = "event", throttle = n }.
    2. Use layout = "byType" so instance paths stay as they are. Pass readyRemote = false while GameInit still calls MarkPlayerReady.
    3. EventsManager.SendTo, SendToAll, MarkPlayerReady and InvokeOn map to the same-named net calls.
    4. QueueGlobalEvent(name, ...) maps to net:QueueGlobal(name, { ... }, expiresIn).
    5. RemoteBinder.get (325 call sites) maps to net:Get, or keep RemoteBinder.lua as a one-line shim over a Net.Binder.new({ layout = "byType", clientFallback = true }).
    6. ThrottlingManager goes away (throttle covers it), and TokenBucket is Net.TokenBucket.

Behaviour fixed on the way

  • InvokeOn: Slime Nebula's version, for an already-ready client, resumed its own running coroutine and then yielded forever. It now invokes directly.
  • Trailing nils: queued sends kept only the arguments up to the first trailing nil ({...}). They now keep them all (table.pack).
  • Throttle state: throttle windows were never cleared when a player left. They are now.
  • Debug prints: the prints on every ready and flush are gone.

Developing

lune run tests/run    # specs on hurreman/luau-spec (DevPackages, else ../luau-spec); fake Roblox in tests/fixtures

The Lune specs cover queueing, broadcast, throttle, limits, global replay, InvokeOn, leaving players and the client path, against fake Instances.

In Studio (Slime Nebula's Edit DataModel, under a throwaway folder), these were checked: real remote creation in both layouts, replacement of a wrong-class remote, on-demand Get, and the accessors' classes.

Client-to-server delivery between a real server and client in Play mode has not been run yet.

Package Details

Install command (Click to copy)


Version

0.1.0

License

MIT

check_circle

Safe for commercial use

Automated license review — not legal advice.