Alternatives to bridging the server-client boundary directly

I’ll try to keep things nice and simple while explaining in detail what I’m looking for.

I would like to use BindableEvents or a custom implementation to create a signal, like the ones that Roblox instances have (e.g. BasePart.Touched). Replication would be included with these signals. Below is a table that shows what kind of workflow I’m looking for.

Machine Can connect Can fire
Server Yes Yes
Client Yes No*

*If I can’t restrict firing from the client, restrictions apply. I explain that further down.

In my codebase, usage of the signal would look like this:

-- Server and client method
Signal:Connect(FunctionBody)

-- Server-only, client w/ restrictions
Signal:Fire(Arguments)

To explain the above table in bullet points, this is what I’d like:

  • Server and client can connect to signal
  • Server can fire signals: all connections (server and client) will run
  • Client cannot fire signal
    • If they can, then only client connections should run (I can just use BindableEvents here)

You can’t do all this with vanilla BindableEvents because sending and receiving is restricted to a specific environment. Any events connected by the server will only run when the server fires the bindable, likewise for the client. For a custom signal class it’s the same way because each machine (server and clients) have different copies of a ModuleScript returned on require.

I do not want to resolve this task using either RemoteEvents or ValueObjects Please do not suggest their use. If I have no choice in the matter, I’d like to know why their use is needed and how I can implement them invisibly both as a developer and at run time for scripts in the game.

And I suppose the final thing to include is the why.

  • I would like to create a mock of Roblox’s instances signals for custom objects. This will help out creating replicating events for pseudoinstances (class objects made from a new method).

  • I feel like it’d be easier, cleaner and widely useful to be able to connect to a signal from both environments but limit firing across one environment, or replicate a fired signal from the server across the server as well as to clients.

  • Imagine the joys you could get out of, for example, a ValueObject-less data management system. The client could connect to a signal determing when updates are made to update some Guis and the server could as well to push new data to the cache, replicate or use for other functions.

  • I love code organisation a little too much.

This sounds like something that would involve selective or developer-controlled replication, so if I can’t do it without either of these being a feature, then I’ll be sad and wait until it is. No feature request because this has already been asked for numerous times.

The closest solution I’m looking at is HuotChu’s PubSub. It uses remotes internally which I don’t want. This is also 2 years old, the source can be improved and it has undesirable conventions. It’s my last resort as of right now.

(10/24/19 - 1:03AM) Cleaned up the post a bit. Hopefully it’s easier to follow now.

6 Likes

I managed to recreate a bindable event like script yet only for the server so far. Sadly i don’t see any other method other than using ObjectValues if you really don’t want to use remote events. hopefully this code stack helps a bit:

-- module
local module = {}
module.__index = module

function module.CreateSignal(name)
	if game.Players.LocalPlayer then error("Can't create signal from the client") end
	local signal = {}
	setmetatable(signal, module)
	signal.Name = name
	signal.Fired = false
	module[name] = signal
	module.Arguments = nil
	
	return signal
end

function module:Fire(...)
	if game.Players.LocalPlayer then error("Can't fire from the client") end
	self.Arguments = {...}
	self.Fired = true
end

function module:Connect(signal, func)
		local connection = {}
		local self = module[signal]
		local loop = true
		function connection:Disconnect()
			warn("Disconnected from: "..signal)
			loop = false
		end
		
		local wrap = coroutine.wrap(function()
			repeat wait()
				repeat wait() until self.Fired
				 self.Fired = false
				func(table.unpack(self.Arguments))
			until not loop
		end)
		wrap()
		return connection
end

return module

-- server script 1
local mod = require(game.ReplicatedStorage.ModuleScript)
local signal = mod.CreateSignal("Test")

signal:Fire("test string", "another test")
wait(1)
signal:Fire("test string")

-- server script 2
local SignalModule = require(game.ReplicatedStorage.ModuleScript)
local connection 

local function testfunc(args, args2)
	print(args, args2)
	connection:Disconnect()
end

connection = SignalModule:Connect("Test", testfunc)

I do hope that this base helps to see if there’s a way to emulate the remote event without actually remote events. Yet i am afraid that it’ll require ObjectValues such as booleans and string values that are still visually present as the client would need to access the EXACT same as the server in order to communicate with it.

Of course you could check manually if e.g. the Boolean is set to true by the server or the client but that would require remote events to match the values (which could be great for exploit detection as the only types of “Remotes” you have are booleans and one remote event to check if it’s correctly enabled by the server only)

As @VoidedBIade said there isn’t really a way without using Remotes. This is what I do:

local function makeEvent(name)
    local event = Instance.new("BindableEvent")
    local clientEvent = Instance.new("RemoteEvent")
    event:Connect(function(...) -- I actually use a custom wrapper function here to make this code much shorter but this is essentially what the final result is:
        clientEvent:FireAllClients(...)
    end)
    event.Name = name
    clientEvent.Nane = name
    clientEvent.Parent = ClientEvents -- A folder in ReplicatedStorage
    return event, clientEvent
end

Example on the client:

local event = Events:WaitForChild(name).OnClientEvent

See though, there’s no real point in sharing that because it completely reinvents the wheel for BindableEvents, just without the actual instance. It has no use for me and it backtracks by removing client capability. I appreciate the effort but it’s not what I’m looking for. My parameters are predefined in the thread.

This emulates BindableEvents, not RemoteEvents. Bindables are environment-local (server-server and client-client), remotes are cross-environment (server-client and vice versa). I’m looking for something falling within the thread’s guides - that is, cross-machine as well as cross-environment connection but server authoritative signal calls and restrictions or no firing for the client.

I’m against the use of ValueObjects because they’re inflate game memory and become a dependency for code, which abstraction and disassociating code from physical instances is part of what I want. I’m aware of how I can settle for ValueObjects and I know how I want it done as well: it’s not what I’m looking for though and if anything, I’d rather use RemoteEvents with a descriptor parameter than ValueObjects. See the GitHub repo at the bottom of the thread.

1 Like

If I’m understanding correctly I think that the function I use does what you want.

  • Server can fire
  • Client can’t fire
  • Both can connect

That depends. I’ve outlined what I’m looking for in the thread as the first table and the first bullet list.

I think I understand. In the case of using a shared module you could use something like this:

local RunService = game:GetService("RunService)
")
local Events = script:FindFirstChild("Events") or Instance.new("Folder")
if Events.Parent ~= script then -- Just created
    Events.Name = "Events"
    Events.Parent = script
end
local eventList = {}
local function getEvent(name)
    local function makeEvent(name)
        local event = Instance.new("BindableEvent")
        local clientEvent = Instance.new("RemoteEvent")
        event:Connect(function(...)
            clientEvent:FireAllClients(...)
        end)
        event.Name = name
        clientEvent.Nane = name
        clientEvent.Parent = ClientEvents -- A folder in ReplicatedStorage
        return event, clientEvent
    end
    if RunService:IsClient() then
        return {Event = Events:WaitForChild(name).OnClientEvent}
    else
        eventList[name] = eventList[name] or makeEvent(name)
        return eventList[name]
    end
end

local TheClass = {}
TheClass._AnEvent = getEvent("AnEvent")
TheClass.AnEvent = TheClass._AnEvent.Event

return TheClass

Yeah… this uses RemoteEvents though. I’m looking for a solution that doesn’t use them or ValueObjects, similarly to events of Roblox objects. Is that not possible? I’m looking to directly bridge environments. This module hides that rerouting internally as far as API goes and leaves instances in ReplicatedStorage.

There’s no direct access methods either. It returns OnClientEvent from the RemoteEvent. I’d like the server and the client to connect the same way but only allow one to fire.

so if i understand correctly from both the github repo and the whole conversation in this post. you want something that emulates the RemoteEvent. aka bring over values from the server to the client but not fire the event from the client to the server? (just trying to summarize it).

so if i did understand it correctly this could help althrough it doesn’t make use of ValueObjects nor RemoteEvents. it does make use of object instancing to translate the values from the server to the client in a split second.

local run = game:GetService("RunService")
local instanceType = "HumanoidController" -- can be anything i guess.


local module = {}
module.__index = module

function module.CreateSignal(name)
	if game.Players.LocalPlayer then error("Can't Create Signal from the client!") end
	local signal = {}
	setmetatable(signal, module)
	signal.Name = name
	module[name] = signal
	return signal
end

function module:Fire(...)
        if game.Players.LocalPlayer then error("Can't Fire Signal from the client!") end
	local signal = Instance.new(instanceType)
	local pack = table.pack(...)[1]
	signal.Name = (self) and self.Name or (module[pack[1]]) and pack[1]
	
	for i,v in pairs({...})do
		if (not self and i ~= i) or self then 
			local parameter = Instance.new(instanceType)
			parameter.Name = v
			parameter.Parent = signal
		end
	end
	signal.Parent = game:GetService("ReplicatedStorage").Folder -- folder for events
	
	signal:Destroy()
end

function module:Connect(name, func)
	local signal = {}
	signal.Stopped = false
	function signal:Disconnect()
		self.Stopped = true
	end
	
	local wrap = coroutine.wrap(function()
		repeat run.RenderStepped:Wait()
			local event = game.ReplicatedStorage:WaitForChild(name)
			local parameters = event:GetChildren()
			local actualParameters = {}
			
			for i,v in pairs(parameters)do
				table.insert(actualParameters, v.Name)
			end
			
			func(table.unpack(actualParameters))
		until signal.Stopped
		print("Disconnected")
	end)
	wrap()
	return signal
end

return module

whilst you only have to register the event with the next in a script

local mod = require(game.ReplicatedStorage.ModuleScript)

local signal = mod.CreateSignal("Test")
wait(3) -- ignore this. roblox has some weird issues with pressing play in studio and not registering the client correctly in time
signal:Fire("Test","Test2")

and can be connected in a local script like so:

local mod = require(game.ReplicatedStorage.ModuleScript)
local connection

connection = mod:Connect("Test", function(args1, args2) 
	print(args1, args2) 
	connection:Disconnect()
end)

tell me if i misunderstood again but this seems to be the closest sollution to a one way custom remote event for Server -> Server/Client that doesn’t use either of both options you wanted to avoid

I’m not trying to emulate RemoteEvents, I’m trying to create signals that follow standard replication rules and work similarly to events on Roblox objects. I’ve clearly defined my requests within the OP in the simplest and most summarised terms I could think of.

Again, appreciate the effort, but I don’t really like this code and its not what I’m looking for either. It’s also very inefficient and borderline hacky.

  • The creation method is really ugly and invites dependencies as well as physical instances into the DataModel. It creates an instance of type x and does the same as children for the arguments.

  • Redundancy in marshalling arguments. There is use of both table.pack and {…}. Changing the script to select either or is a simple matter though.

  • Immediate parenting and destroying will spawn race conditions, which is an anti-pattern in this scenario. It’d be better if I could not rely on parenting instances.

  • RenderStepped shouldn’t ever be used except for very small operations you need running before frames render, such as camera updates.

Like yes, it is a possible solution, but if it gets to this point I might as well use RemoteEvents with a descriptor parameter and save myself unnecessary overhead, bottlenecked only by network traffic.

I would still like to know if what I’ve outlined in the thread is within the realm of reality or know why it isn’t possible to achieve before I start jumping to workarounds or alternatives. None of my original thread has actually been addressed yet, I’m just getting workarounds.

Why don’t Remotes work? They are made for the purpose you are looking for. I don’t know any alternatives to remotes other than property replication.

1 Like

Remotes do work for my case, but I’d like to see if I’m able to do them without it first. I did note that if I’m not able to get a solution I like either from replies or from my own testing, that I’ll be using the linked project (PubSub) or something similar to the concepts it follows.

I believe there are only two options. One is instance/property replication, the other is remotes. I suppose if you really want a third option you could count physics replication as one but I don’t know why anyone would ever use physics to transmit any kind of data.

Had a bit of a long discussion with some other users via DevForum Discord but ultimately it’s come down to me not being able to achieve this without RemoteEvents (I’m not a fan of throwaway object solutions).

What I’m probably going to do is set up a signal class and have the Connect method behave differently depending on which environment is requiring the module. So essentially, I’m going to my fallback which is PubSub or something designed similarly to it.

Thanks for the replies, closing up shop. I’d like to delve into this matter again soon, preferably if Roblox ever decides to give us selective replication.

1 Like

Have you tried setting Tags onto the player object, using collection service, as JSON tables? FE only replicates server to client tag changes.

This doesn’t accomplish what I want and is a waste of memory. Tags are not an alternative to anything that’s in the OP and mind you, custom classes do not have physical instances that tags can be attached to. ModuleScripts are completely environment independent.

This is also a 6-month old post for which I have dropped attempting and just learned to live because productivity is better than fussing over how the code looks. I already have a solution as marked.

1 Like