Cucumber, A Remote Communication Module

Over the past two days I’ve been working on a module that’d make communication simple and easy, so I’ve come up with Cucumber. The module’s syntax is easy to work with, (though, something to note, I was quite lazy to add type-checking so studio may not show you all the methods and properties) and versatile.

The module can be found here: Cucumber

To create a connection, simply do:

local Connection = Cucumber.Connection('TestCase') --> connectionName should be unique if you do not want to get the same connection object

(This works on both client and server implementations, we will assume the connection is on the server for the next snippet)
And then to detect for when Connection has been fired, simply :Connect() to the Connection, like so:

Connection:Connect(function(player : Player, ...) --> server has first arg as player always.
       print('Hello from Client!')
end)

To fire our connection, you need to create a new Bridge instance with the same name as the connection from the client, if the connection is on the server, or from the server, if the connection is on the client.

local Bridge = Cucumber.Bridge('TestCase')

And, to fire our bridge, you’d do:

Bridge:Fire(...)

which returns nothing, and does not yield. If you’ve properly set this up, you should now see the server printing: '‘Hello from Client!’ (please note that any fires for a bridge’s name that does not have a connection set-up nor a receiver (via :Connect or other methods) will be lingering until one is added)

Some other useful functions of this module are ports, ports are special connections which act like remotefunctions, where the returns of the connections are replicated to the client, this can be used like the following

-- Server
local Connection = Cucumber.Connection('GetStatus')
Connection:OpenPort('GET'):Connect(function(player)
       return 'Online'
end, 'Status') --:Connect(() -> (...any), resolveName : string?)
-- Client
local Bridge = Cucumber.Bridge('GetStatus')
print(Bridge:FirePort('GET')) --> this function will yield, the output should look like this:
--[[
{
     Status = 'Online',
}
(single variable returns / non-tuple returns will be automatically cast into one value, otherwise it'd look something more like Status = {'Online', 'Ingame'} with return 'Online', 'Ingame'.
]]--

this can also be done vice-versa (Server → Client), but note that FirePort needs 2nd argument as Player for this, and it may be insecure due to exploiters yielding forever.

I hope my first tutorial and my first community resource I have every written, please report any bugs, I will try to maintain this module as much as motivation lets me :pray:

8 Likes

For the first update to this module, I went and made a serializer for it so all data sent through cucumber can be encrypted and compressed depending on the circumstances. It is used using the Cucumer.Serializer module. Examples are below,

local Cucumber = require(game.ReplicatedStorage:WaitForChild('Cucumber'))
local Connection = Cucumber.Connection('TypeTest', {
    Cucumber.Serializer.CFrame()
}); --> the second field here shows the way that this remote will be serialized. (here, the 1st argument must be a CFrame and it'll be serialized.)

Connection:Connect(function(...)
    print(...) --> will print a cframe ALWAYS. this is a strict datatype and if the serializer can not deserialize it will just error with wrong args.
end)

this can also be done with ports as the 2nd argument to :OpenPort.
furthermore, if you have multiple datatypes that may be put into the first argument, use Cucumber.Serializer.many(Cucumber.Serializer.CFrame(), ...)
^ note that the amount of space that this takes up is dependent on the amount of datatypes passed, it’ll take exactly floor(log2(#serializers)+1) bits to store all of the data.
Cucumber.Serializer.optional(Cucumber.Serializer.CFrame()) → means that this value will be nil-able, or can be nil. Shorthand for Cucumber.Serializer.many(Cucumber.Serializer.None(), ...)
if you have a strict set of arguments to expect as an argument, use:
Cucumber.Serializer.CustomEnum({Enum.UserInputType.MouseButton1, Enum.KeyCode.F}) this will compress the data into just a few bits, making it very efficient.
Some custom-type serializers include:
CFrame16
Vector3float16
(note: these values are likely to have high amounts of corruption if the values serialized are too big.)

Furthermore, the module also has the ability to serialize Instances, they will be serialized to only occupy 5+floor(log2(amountOfInstances)+1) bits.

Please let me know of any bugs and other stuffs that arise :smile: !

4 Likes

Fixed certain bugs, and made some improvements to many serialization.
many will now pick the option whicch can serialize with the LEAST amount of space taken, e.g int8 > int64 whenever possible.
a new serialization type called any has been added, like the name suggests it will basically encrypt any type. (or most of them, which are implemented.)

2 Likes

New feature to Cucumber called RollbackReliantObject. This is something you can create where you can manage data on both client and server, and predict/extrapolate data however you wish, when it is wrong. You can set it to revert back. this is how its used:

local RRO = Cucumber.RollbackReliantObject(game.ReplicatedStorage.NetData, {
	Position = Vector3.new(10,10,10),
	Velocity = Vector3.new(10,10,10),
	Time = 0;
})
-- RRO.Position, RRO.Velocity, RRO.Time can all be accessed and editted using this

NetData:

local Cucumber = require(script.Parent.Modules.Cucumber)
local RollbackObject = {
	SyncedVariables = {
		["Position"] = Cucumber.Serializer.Vector3float16();
		["Velocity"] = Cucumber.Serializer.Vector3float16();
		["Time"] = Cucumber.Serializer.Float64();
	};
	SyncRate = 1/5;
}

function RollbackObject:Init()
	self.trove:Add(game:GetService("RunService").Heartbeat:Connect(function(dt)
		local d = (self.Time+dt)^2 - self.Time^2
		self.Velocity-=Vector3.new(0,0.8*d, 0);
		self.Position += self.Velocity;
		self.Time += dt;

		if self.Time>2 then self:Destroy() end;
	end))
end

function RollbackObject:ClientStart()
	local Object = Instance.new('Part', workspace);
	Object.Anchored = true;
	self.Object = Object;
	self.trove:Add(Object)
	self.Changed:Connect(function(Name, Value, ServerSync)
		self.Object.Position = self.Position;
	end)
	print('ClientStarted', self.Object)
end

return RollbackObject;

if you set SyncRate to -1, it will not automatically sync. you’d have to call :Sync() manually.
to destroy the object, you can call RollbackObject:Destroy(), which will clean all the things within the trove.