Hello world!
This module allows you to make module scripts that usually would only work on the client or the server, work for both the client and the server. For example, it can be used to make a Datastore module (GetData, UpdateData, etc) be accessible from the client
This is not some kind of unfiltered enabled though, it is perfectly safe, mainly replacing remotes you’d use to get the data from the server, or to update it
This module aims to simplify Client/Server communication. By using this module instead of remote, the client (or server) doesn’t have to use a remote, the client can use the module script as the server would, creating a cohesive structure for the server and the client. The client will also retain autocomplete, unlike a remote
No more hidden connections to remotes scattered across multiple scripts
The way it works is simple. The methods that are marked as being for the server will be overwritten to a function that sends whatever the client gives it, to the server, and calls that method, from the server. In the case of a “RemoteFunction”, the result is sent back to the client. It works the same way for server → client.
It also has a little system for sanity checks. While it is not needed to use it (ie, it can be done in the method itself), it encourages the use of sanity checks
Example code
local ClientServerModuleBridge = require(game.ReplicatedStorage.ClientServerModuleBridge)
local ServerModule = ClientServerModuleBridge:InitializeServerModule(script)
local ClientModule = ClientServerModuleBridge:InitializeClientModule(script)
local TestModule = {}
-- // Server examples // --
local function SanityChecks(Player : Player, Key, Data)
if type(Key) ~= "string" then return end -- When nil or false is returned, that means the sanity check failed, and UpdatePlayerData wont be called
return true
end
function TestModule:UpdatePlayerData(Player : Player, Key, Data)
-- Update the data into the datastore or something
print(Player, Key, Data)
end
ServerModule:SetRemoteEvent(TestModule, TestModule.UpdatePlayerData, true, SanityChecks)
function TestModule:GetPlayerData(Player : Player, Key : string)
local SaveData = {"Hello World"}
-- Do datastore stuff or something
return SaveData
end
ServerModule:SetRemoteFunction(TestModule, TestModule.GetPlayerData, true) -- SanityCheckFunction is optional
-- // Client examples // --
function TestModule:PromptSaveSucceded(Player : Player, Message)
-- Show the player a ScreenGui with message
-- It is probably a good idea to do this in a seperate PromptModule, but this is to showcase that some functions can be clientsided, while others are serversided
end
ClientModule:SetRemoteEvent(TestModule, TestModule.GetClientData, true)
return TestModule
How to use
First up, you need to make either a ClientModule, a ServerModule, or both. You must pass the ModuleScript, and there are two optional arguments, a RemoteEvent and a RemoteFunction, if you want to specify which ones to use, if you don’t want it to automatically make them under the ModuleScript
(The first argument can actually be anything, it’s where the remotes will be parented)
If you specify a RemoteEvent and a RemoteFunction, it MUST be the same for the client and the server
local ClientServerModuleBridge = require(game.ReplicatedStorage.ClientServerModuleBridge)
local ServerModule = ClientServerModuleBridge:InitializeServerModule(script)
local ClientModule = ClientServerModuleBridge:InitializeClientModule(script)
ServerModule is for creating server sided methods or functions. When the client will call the method, a remote will be used behind the scenes
ClientModule is for client sided methods, so the server will use a remote
Next up, you need a function, in this case, I’ll take the example of a Datastore method, though, just a mimic
function TestModule:GetPlayerData(Player : Player, Key : string)
local SaveData = {"Hello World"}
-- Do datastore stuff or something
return SaveData
end
One thing that is very important, every method that will use the Client/Server Bridge Module must have the Player as the first argument, even in cases where it isn’t useful (ie, calling the method above from the client. You can actually send anything for the player argument in this case, anything other than nil)
Now, you can use one of two methods, :SetRemoteEvent()
or :SetRemoteFunction()
. SetRemoteEvent is the equivalent of a RemoteEvent, SetRemoteFunction is the equivalent of a RemoteFunction, for getting data back
function TestModule:GetPlayerData(Player : Player, Key : string)
local SaveData = {"Hello World"}
-- Do datastore stuff or something
return SaveData
end
ServerModule:SetRemoteFunction(TestModule, TestModule.GetPlayerData, true, SanityChecks) -- SanityCheckFunction is optional
The first argument is the table, and the second one is the function
The third argument is a boolean, true if self is passed to the function self
, false if not. Set this to true for the :
notation, and false for the .
notation. (Otherwise, if you know self is, you know what to do)
The fourth argument is optional, it is a function, for sanity checks. If the function returns nil or false, the sanity check failed. If true or any other value is returned, the sanity check succeeded
local function SanityChecks(Player : Player, Key, Data)
if type(Key) ~= "string" then return end -- When nil or false is returned, that means the sanity check failed, and UpdatePlayerData wont be called
return true
end
This is pretty much it :P
Using this module involves putting pretty much every module script inside of replicated storage, so it isn’t ideal for games that already use remotes extensively, that would require a bit of rearranging…
Files & model
ClientServerModuleBridge.lua (13.3 KB) ← Not valid lua, it should be luau, but the forum doesn’t like luau…
TestModule.lua (1.4 KB)
ClientServerModuleBridge.rbxm (3.1 KB)
TestModule.rbxm (1.4 KB)
https://create.roblox.com/store/asset/17277377848
Final thoughts
I tried as much as possible to give this module a cohesive structure. It was a bit challenging given the task it does, one example of that are the arguments for SetRemoteEvent/SetRemoteFunction, the second argument is the function, rather than being the index of the function, which was done purely for usability. Doing it this way involves getting the index by searching for the function in the table, not ideal
I also kind of just realized that there is no way to detect if the function is running from the server, or from a request from the client. I was thinking, ohh you can just use RunService:IsClient(), but no duh… I’ll probably add a variable or something to fix this. Not sure what would be the best way though…?
The sanity check function could be used for that, with some imagination, the sanity check function and the module function both run in the same thread, so coroutine.running() could be used to detect if the sanity check function was ran during the same thread