How to make module script call bindable event

I wanted my module script to use the Bindable Events within itself as a homemade Changed event.

However, when the module script changes something and fires the bindable event, no local script detects the change. Which is odd because I can make a local script fire the event, AND listen to it and it will work just fine. It would seem like JUST the module script cant call it.

I tried moving the events to replicated storage and yielding to the call. Nothing. Are bindable events just not possible for client-side module scripts?

The bindable event, PlayerChanged, is child to ModuleScript. then i just have a testing script that’s supposed to change the data. The module script makes the adjustment and “fires” the event. but the script thats listening will detect nothing.

here is the code for the testing local script:

local PlayerChanged= script.Parent:WaitForChild("ModuleScript1"):WaitForChild("PlayerChanged")
local Module = require(script.Parent:WaitForChild("ModuleScript1"))
local Testbutton = script.Parent.Parent:WaitForChild("ScreenGui"):WaitForChild("TextButton")
Testbutton.Activated:Connect(function()--I attached the call to a clickable button. just so that i could REALLY spam it
   Module.Player().Offset(3,10)
end)
PlayerChanged.Event:Connect(function(PlayerData)
   print("CHANGE DETECTED!!!") --this is never printed :(
   print(PlayerData)
end)

this is a snippet of the module script:

function Player.SetSettings(Index,Value)
   if not CheckParameters({Index,Value},2) then return{3,"ERROR. MISSING PARAMETERS"}end --ignore this. its just validating that parameters were sent
   ChangeSaveStatus:FireServer("PlayerDataStoreStatus")--ignore this
   PlayerData[1][11][Index]=Value--the actual change taking place. It works
   PlayerChanged:Fire(PlayerData)--never called
   return {1,PlayerData}--code 1 and updated table returned
end

I would REALLY like to keep this approach because the code is so much cleaner than using Attributes to detect changes across the client. Any advice or help would be amazing

1 Like

For the BindableEvent to fire, you need to call the SetSettings() function of your module. In this case it should be called inside the Module.Player() or Module.Player().Offset() function. Please check that you are indeed calling the SetSettings() function in one of these.

If it still does not work, you could post the code of these two functions.

--LOCAL

local replicated = game:GetService("ReplicatedStorage")
local bindable = replicated:WaitForChild("Event")

local module = script:WaitForChild("ModuleScript")
module = require(module)

bindable.Event:Connect(module.onBindableEventFired)
bindable:Fire(math.pi)
--MODULE

local module = {}

function module.onBindableEventFired(...)
	local arguments = {...}
	print(table.concat(arguments, " "))
end

return module

In short, BindableEvents/BindableFunctions do work with ModuleScripts.

2 Likes

Apologies, but it IS being called, just not in any given script. Its being called from a script who’s purpose is to test the module script, hence the return codes.

1 Like

I have no doubt the way you programmed it that a module script could be fired during a callback. However, this is already known and doesn’t address the problem. module scripts cannot FIRE the event. you fired the event within local scripts and handled the event inside as well. which i already stated.

1 Like

Um, first chunk of code, line 2?

--Module script inside ReplicatedStorage.

local module = {}

local replicated = script.Parent --ReplicatedStorage script.
local bindable = replicated:WaitForChild("Event")

function module.fireBindable(...)
	bindable:Fire(...)
end

return module
--Local script inside StarterGui.

local starterGui = script.Parent --StarterGui script.

local replicated = game:GetService("ReplicatedStorage")
local module = replicated:WaitForChild("ModuleScript")
module = require(module)

task.wait(1)
module.fireBindable(math.pi)
--Local script inside StarterPlayerScripts.

local starterPlayer = script.Parent.Parent --StarterPlayer script.

local replicated = game:GetService("ReplicatedStorage")
local bindable = replicated:WaitForChild("Event")

bindable.Event:Connect(function(...)
	print(...)
end)

This, as expected, prints the math.pi constant to the console.

2 Likes

There’s a good chance you’re encountering a racing condition, in which the instance method “Fire()” is called on the BindableEvent before a connection is made to its RBXScriptSignal object named “Event”.

I avoided that in the scripts I provided by yielding for a second before firing the BindableEvent instance.

As @Forummer said, it’s likely a racing condition.

Rather than using a bindable (since it has such a drawback), use a Pure Signal implementation. It should work just about the same. I’ve also figured out a method to retain the signal if required multiple times (although not very ideal…)
Here’s the code: (read notes please)

-- with this implementation, a module can be required in
-- multiple places, and still retain the same Changed event.

local module = {}
local ID = script:GetAttribute("ID")
local Signal = nil
if not ID then -- multiple modules with changed events can be made with an ID
  -- if we don't do this, we're setting a constant key to _G (_G is not recommended but it'll do)
  ID = game:GetService("HttpService"):GenerateGUID(false):sub(1,6)
  -- we only need a limited string (they're unique enough)
  -- and we save memory this way.
  script:SetAttribute("ID", ID)
end
Signal = _G[ID]
if not Signal then
  -- make sure you change this to the proper path
  Signal = require(yoursignalmodulepath).new()
  _G[ID] = Signal
end

module.Changed = Signal

--example
function module:SetParent(Parent)
  assert(typeof(Parent) == "Instance", "Invalid argument #1 to module:SetParent(Instance)")
  script.Parent = Parent
  module.Changed:Fire("Parent", Parent) -- won't just randomly throw errors.
end

return module
Signal module if you need it
local signal = {
	DisconnectYieldMax = 1.5,
	Debug = true
}
local integrated_proto,standalone_proto = {},{}
local Thread = nil

local function ExecThread(Exec, ...)
	local CurrentThread = Thread
	Thread = nil
	Exec(...)
	Thread = CurrentThread
end

local function RunInThread(...)
	ExecThread(...)
	while true do
		ExecThread(coroutine.yield())
	end
end

local function allocate(...)
	local Args = table.pack(...)
	local Argc = table.getn(Args)
	if Argc == 1 and typeof(Args[1]) == "table" then
		local Arg = Args[1]
		local ArgS = table.getn(Args[1])
		local New = table.create(ArgS)
		for Index, Value in ipairs(Arg) do
			New[Index] = Value
		end
		return New
	elseif Argc > 1 then
		local New = table.create(Argc)
		for Index, Value in ipairs(Args) do
			New[Index] = Value
		end
		return New
	end
end

local Connects = {}
Connects.__tostring = function() return "PureConnection" end
Connects.__index = Connects
function Connects:Disconnect()
	assert(self[2], "PureConnection cannot be disconnected twice.")
	self[2]=nil
	if self[1][1] == self then
		self[1][1] = self[3]
	else
		task.spawn(function()
			local Top = self[1][1]
			while Top and Top[3] ~= self do
				Top = Top[3]
			end
			if Top then
				Top[3] = self[3]
			end
		end)
	end
end

local function BuildConnection(Signal, Handle, Queue)
	local Connection = allocate({
		[1] = Signal, -- Main Signal
		[2] = Handle,	-- Connection Handle(r)
		[3] = Queue		-- Next connection in execution stack
	})
	return setmetatable(Connection, Connects)
end

function signal.new()
	local sig = allocate({
		[1] = false
	})
	return setmetatable(sig, {
		__index = integrated_proto,
		__newindex = function(self, k, v)
			if typeof(k) == "number" and k <= 2 then
				error("Attempt to overwrite internal values of PureSignal!", 2)
			else
				self[k] = v
			end
		end,
		__metatable = "Locked!",
		__tostring = function() return "[LuaU] PureSignal" end
	})
end

function integrated_proto:Connect(Handle)
	assert(typeof(Handle) == "function", ("Invalid argument #1 to PureSignal:Connect(), expects function, got %s"):format(typeof(Handle)))
	local Connector = BuildConnection(self, Handle, self[1])
	self[1] = Connector
	return Connector
end

function integrated_proto:Fire(...)
	local StackHead = self[1]
	task.spawn(function(Top, ...)
		while Top do
			if Top[2] then
				Thread = if Thread then Thread else coroutine.create(RunInThread)
				task.spawn(Thread, Top[2], ...)
			end
			Top = Top[3]
		end
	end, StackHead, ...)
end

function integrated_proto:Wait()
	local ThreadParent, Temp = coroutine.running()
	Temp = self:Connect(function(...)
		Temp:Disconnect()
		task.spawn(ThreadParent, ...)
	end)
	return coroutine.yield()
end

return signal

It includes:

  • :Wait() – yields and returns the arguments to the :Fire() call that broke yield.
  • :Fire(Tuple *any) – self-explanatory.
  • :Connect(function Callback) – self-explanatory.
  • Connection:Disconnect() – self-explanatory.
1 Like

Hm, your code might work. But I also think that it only works because it’s a connection between server and client. Like a remote event that uses bindable event. What I was doing was strictly client-sided. I will test this theory and let you know if it matters where it’s called

Looking through your code, I dont see the fire event. Either you forgot it or you’re adding this ontol of what I already have. Going with the latter, I still am confused as to why you want to create UUID for all fire events and set attributes. It would make sense to just use the Attributes.changed event instead to save time, but I was trying to avoid using attributes and globals. (I was told using _g. Is a surefire way of ruining your game)

if the module is required once, you can simply make one signal. That code is for multiple scripts requiring the module. You could remove the ID/_G parts and just keep Signal = require(path).new() if it’s a one-off module.

Both scripts are local scripts, I made that clear in the comments.

Moreover, BindableEvents/BindableFunctions cannot be used to fire the server from the client/fire the client from the server.

But your post says the module script is within replicated storage. Which is just a storage space saved for both server and client. I’m trying to have it done within start player scripts. Could it be done there? Or did you pick replicated storage for a reason?

Sorry, but ya lost me in the complexity of your script. I wanted to use bindable because it sounded so easy, but if it’s this much of a hassel, it sounds like I should just use attribute changed events instead

1 Like

ReplicatedStorage is just the location of the ModuleScript and the BindableEvent, the two local scripts which are communicating are in StarterGui and StarterPlayer.

My method isn’t necessary, and it looks like Forummer is gonna assist you further, I’m a bit too tired now to explain in depth on the signal implementation.

1 Like

I found out the solution to my code was for me to stop being stupid. I didnt realize that in the parameters of what i passed through was less than 0 and more than what the player had. therefor, it would always return “error. value is less than”. but since i never printed this out, i never saw any errors. Module scripts do fire the event with a simple BE:fire(). im giving you the solution tho, because you showed binding the event calls into task.delay or task.wait