Whisper - A remote communication library aiming to reduce remote overhead

A common pattern that some people may run into while coding is something like this:

Input.OnServerEvent:Connect(function(player, key)
      if key == Enum.KeyCode.F then
             ...
      elseif key == Enum.KeyCode.E then
             ...
      end -- repeat 
end)

And, as a person who cares too much about optimisation, this pattern feels sacrilegious, a common way to avert this problem is to make a remote per keycode, so that the keys which are not being pressed are not being sent to the server (or, you send to the client or the client knows the keys which are bound to some function.)

Here’s how a simple ‘equipping’ system may be done using Whisper.

--// SERVER \\--
local Whisper = require(game.ReplicatedStorage.Whisper)

local InputNamespace = Whisper.Namespace("InputDown", {
	FireRoot = false;
})

local ToolNamespace = Whisper.Namespace("Tool", {
	FireRoot = false;
})

local VFX = ToolNamespace:CreateTunnel("SpecialVFX") --// you need to do this to send data to the client

local Equipped = false;

local Tunnel = InputNamespace:ServerTunnel(Enum.KeyCode.F);
local SpecialConn;

InputNamespace:ServerTunnel(Enum.KeyCode.E):Connect(function(a0: Player) 
	Equipped = not Equipped;
	
	if Equipped then
		SpecialConn = Tunnel:Connect(function(a0: Player) 
			print('player used special')	
			ToolNamespace:FireClient(a0, "SpecialVFX")
		end)
	else
		SpecialConn:Disconnect()
	end
end)
--// CLIENT \\--
local Whisper = require(game.ReplicatedStorage.Whisper)
local Input = Whisper.Namespace("InputDown");
local ToolNamespace = Whisper.Namespace("Tool")

game.UserInputService.InputBegan:Connect(function(input: InputObject, gameProcessedEvent: boolean) 
	if not gameProcessedEvent then
		Input:FireServer(input.KeyCode);
	end	
end)

ToolNamespace:ClientTunnel('SpecialVFX'):Connect(function() 
	print('client received vfx.')	
end)

note that the server tunnels become inactive after all connections have been disconnected, this is to stop the client from sending data to remotes which are not being used & also allows for some calls to get sunk (if FireRoot is disabled) to gain performance (this is the recommended option)

Features and Advanced Functionality
  • Propagating signals:
    • Having two tunnels which share the same identifiers such as like this:
    Namespace:ServerTunnel("Type1"):Connect(...)
    Namespace:ServerTunnel("Type1", "Specific"):Connect(...)
    
    These would both fire if the client did :FireServer(“Type1”, “Specific”)
  • Typematched identifiers
    • The module allows for identifiers to follow any type (but not the value of all types, just Instances, numbers, strings, bools and nil), For the time being there are 3 custom data types, they may be used as shown.
    Namespace:ServerTunnel("Type1", Whisper.DataTypes.any(), "other"):Connect(...) -- this works to allow for the middle arg to be anything
    Namespace:ServerTunnel("Type1", Whisper.DataTypes.many(1,""), "other"):Connect(...) -- this would only work if the middle arg was either a number or a string (contents do not matter)
    Namespace:ServerTunnel("Type1", Whisper.DataTypes.ambigious(workspace), "other"):Connect(...) -- this would only work if the middle arg was a Instance, (the contents do not matter again) 
    
  • The ‘Required’ Datatype (Advanced)
    • The custom datatypes / filters can be problematic, for example, if you wanted to have one connection where one value was either a string or a number, but another script expects that same connection tree to have a number (and requires its contents) you will get an ‘omitted’ argument, to fix this, you can label certain fields as ‘required’ (you usually shouldnt need to use this). A scuffed example (I am aware this isnt valid because it calls ping twice, but it is for the sake of showcasing):
local Whisper = require(game.ReplicatedStorage.Whisper);

local Req = Whisper.Namespace('RequiredFieldTests')
Req:CreateTunnel('Pong')
Req:CreateTunnel('PongWithPayload')

Req:ServerTunnel('Ping', Whisper.DataTypes.ambigious(1)):Connect(function(Player: Player, ...: any) 
	Req:FireClient(Player, "Pong");	
end) -- Ping without payload.

Req:ServerTunnel("Ping", Whisper.DataTypes.required()):Connect(function(Player: Player, ...: any) 
	local requiredFields = Whisper.PopRequiredArgs()
	Req:FireClient(Player, "PongWithPayload", requiredFields[1]);
end) -- Ping with payload.
--// Client \\
local Whisper = require(game.ReplicatedStorage.Whisper);

local Req = Whisper.Namespace('RequiredFieldTests')

Req:FireServer('Ping', 1);

Req:ClientTunnel('Pong'):Connect(function(...) 
	print('got pong')
end)

Req:ClientTunnel('PongWithPayload'):Connect(function(...) 
	print('got pong payload', ...)
end)
  • in this case, removing the .required() field and printing … will show: (This value has been omitted due to filters, if this is unintended behaviour, you can specify this argument to be required by using the .required datatype.), upon readding the required field it should print ‘nil’. To actually access the required variables, you need to do, PopRequiredArgs (best practice) or GetRequiredArgs , you can only use poprequiredargs once so saving its result is also good practice. (The reason why it is done like this is due to the fact that it is completely seperate from the arguments of the function, and it is a design decision made by me)

Overall, the best way to use this module is to use it with many namespaces, sticking to just global is fine but namespaces are great for isolating just a few remotes which allows the search algorithm to find the ‘shortest’ payload to send to any of the remotes.

https://create.roblox.com/store/asset/85744814956550/Whisper

1 Like

I think you forgot to make it accessible to everyone?

1 Like

yes i did (forget to), its accessible now.

incredibly roblox took it down for ‘misusing roblox systems’, if anyone wants to use it here is a .rbxm, i have filed an appeal.
Whisper.rbxm (11.0 KB)