diff --git a/Test/Client/Client.client.luau b/Test/Client/Client.client.luau index ac89343..78e8650 100644 --- a/Test/Client/Client.client.luau +++ b/Test/Client/Client.client.luau @@ -1,48 +1,94 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage") -local ComplexFunction = require(ReplicatedStorage.ComplexFunction) -local EmptyFunction = require(ReplicatedStorage.EmptyFunction) -local SimpleFunction = require(ReplicatedStorage.SimpleFunction) +function TestEvents() + local ComplexFunction = require(ReplicatedStorage.ComplexFunction) + local EmptyFunction = require(ReplicatedStorage.EmptyFunction) + local SimpleFunction = require(ReplicatedStorage.SimpleFunction) -local ComplexEvent = require(ReplicatedStorage.ComplexEvent):Client() -local SimpleEvent = require(ReplicatedStorage.SimpleEvent):Client() -local EmptyEvent = require(ReplicatedStorage.EmptyEvent):Client() -local ReadyEvent = require(ReplicatedStorage.ReadyEvent):Client() + local ComplexEvent = require(ReplicatedStorage.ComplexEvent):Client() + local SimpleEvent = require(ReplicatedStorage.SimpleEvent):Client() + local EmptyEvent = require(ReplicatedStorage.EmptyEvent):Client() + local ReadyEvent = require(ReplicatedStorage.ReadyEvent):Client() + task.wait(0.5) -task.wait(0.5) + print("Registering Listeners") -print("Registering Listeners") + ComplexEvent:On(function(Value1, Value2, Value3) + print("ComplexEvent", Value1, Value2, Value3) + end) -ComplexEvent:On(function(Value1, Value2, Value3) - print("ComplexEvent", Value1, Value2, Value3) -end) + SimpleEvent:On(function(Value) + print("SimpleEvent", Value) + end) -SimpleEvent:On(function(Value) - print("SimpleEvent", Value) -end) + EmptyEvent:On(function() + print("EmptyEvent") + end) -EmptyEvent:On(function() - print("EmptyEvent") -end) + task.wait(0.5) -task.wait(0.5) + print("Firing Events") -print("Firing Events") + ComplexEvent:Fire({ one = { "String Literal", 123 }, two = { 123, "String Literal" } }, "hello world again", 123) + SimpleEvent:Fire(123) + EmptyEvent:Fire() -ComplexEvent:Fire({ one = { "String Literal", 123 }, two = { 123, "String Literal" } }, "hello world again", 123) -SimpleEvent:Fire(123) -EmptyEvent:Fire() + task.wait(0.5) -task.wait(0.5) + print("Calling Functions") -print("Calling Functions") + print(ComplexFunction:Call({ one = { "String Literal", 123 }, two = { 123, "String Literal" } }, "hi", 12):Await()) + print(SimpleFunction:Call(123):Await()) + print(EmptyFunction:Call():Await()) -print(ComplexFunction:Call({ one = { "String Literal", 123 }, two = { 123, "String Literal" } }, "hi", 12):Await()) -print(SimpleFunction:Call(123):Await()) -print(EmptyFunction:Call():Await()) + task.wait(0.5) -task.wait(0.5) + print("Firing Ready") -print("Firing Ready") + ReadyEvent:Fire() +end -ReadyEvent:Fire() +function TestSharedEvents() + print("Testing Shared Events!") + local SharedEvents = require(ReplicatedStorage.SharedEvents) + + task.wait(0.5) + print("Registering Listeners") + + SharedEvents.Empty:SetClientListener(function() + print("Received empty!") + end) + + SharedEvents.Simple:SetClientListener(function(Value) + print("Received simple!", Value) + end) + + SharedEvents.Signal:OnClient(function(Value) + print("Signal 1 called!", Value) + end) + SharedEvents.Signal:OnClient(function(Value) + print("Signal 2 called!", Value) + end) + + SharedEvents.Complex:SetClientListener(function(Value1, Value2, Value3) + print("Received complex!", Value1, Value2, Value3) + end) + + task.wait(0.5) + print("Firing events") + + SharedEvents.Empty:FireServer() + SharedEvents.Simple:FireServer(5) + SharedEvents.Signal:FireServer(6) + SharedEvents.Complex:FireServer( + { one = { "String Literal", 17 }, two = { 123, "String Literal" } }, + "another string", + 5 + ) + + task.wait(0.5) + print("Firing ready") + SharedEvents.Ready:FireServer() +end + +TestSharedEvents() diff --git a/Test/Server/Server.server.luau b/Test/Server/Server.server.luau index c3c0092..1e12725 100644 --- a/Test/Server/Server.server.luau +++ b/Test/Server/Server.server.luau @@ -1,82 +1,169 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage") -local ComplexFunction = require(ReplicatedStorage.ComplexFunction) -local SimpleFunction = require(ReplicatedStorage.SimpleFunction) -local EmptyFunction = require(ReplicatedStorage.EmptyFunction) +function TestEvents() + local ComplexFunction = require(ReplicatedStorage.ComplexFunction) + local SimpleFunction = require(ReplicatedStorage.SimpleFunction) + local EmptyFunction = require(ReplicatedStorage.EmptyFunction) -local ComplexEvent = require(ReplicatedStorage.ComplexEvent):Server() -local SimpleEvent = require(ReplicatedStorage.SimpleEvent):Server() -local EmptyEvent = require(ReplicatedStorage.EmptyEvent):Server() -local ReadyEvent = require(ReplicatedStorage.ReadyEvent):Server() + local ComplexEvent = require(ReplicatedStorage.ComplexEvent):Server() + local SimpleEvent = require(ReplicatedStorage.SimpleEvent):Server() + local EmptyEvent = require(ReplicatedStorage.EmptyEvent):Server() + local ReadyEvent = require(ReplicatedStorage.ReadyEvent):Server() -print("Registering Function Callbacks") + print("Registering Function Callbacks") -ComplexFunction:SetCallback(function(Player, Value1, Value2, Value3) - print("ComplexFunction", Player, Value1, Value2, Value3) - task.wait(0.2) + ComplexFunction:SetCallback(function(Player, Value1, Value2, Value3) + print("ComplexFunction", Player, Value1, Value2, Value3) + task.wait(0.2) - return Value1, Value2, Value3 -end) + return Value1, Value2, Value3 + end) + + SimpleFunction:SetCallback(function(Player, Value) + print("SimpleFunction", Player, Value) -SimpleFunction:SetCallback(function(Player, Value) - print("SimpleFunction", Player, Value) + return tostring(Value) + end) - return tostring(Value) -end) + EmptyFunction:SetCallback(function(Player) + print("EmptyFunction", Player) -EmptyFunction:SetCallback(function(Player) - print("EmptyFunction", Player) + return + end) - return -end) + print("Registering Event Listeners") -print("Registering Event Listeners") + ComplexEvent:On(function(Player, Value1, Value2, Value3) + print("ComplexEvent", Player, Value1, Value2, Value3) + end) + + SimpleEvent:On(function(Player, Value) + print("SimpleEvent", Player, Value) + end) -ComplexEvent:On(function(Player, Value1, Value2, Value3) - print("ComplexEvent", Player, Value1, Value2, Value3) -end) + EmptyEvent:On(function(Player) + print("EmptyEvent", Player) + end) -SimpleEvent:On(function(Player, Value) - print("SimpleEvent", Player, Value) -end) + ReadyEvent:On(function(Player) + print(`[{Player.Name}] Ready, firing events!`) + + print(`[{Player.Name}] :Fire`) + SimpleEvent:Fire(Player, 1) + EmptyEvent:Fire(Player) + ComplexEvent:Fire(Player, nil, "hello world again", 1) + + print(`[{Player.Name}] :FireAll`) + SimpleEvent:FireAll(2) + EmptyEvent:FireAll() + ComplexEvent:FireAll( + { one = { "String Literal", 123 }, two = { 123, "String Literal" } }, + "hello world again", + 2 + ) + + print(`[{Player.Name}] :FireAllExcept`) + SimpleEvent:FireAllExcept(Player, 3) + EmptyEvent:FireAllExcept(Player) + ComplexEvent:FireAllExcept(Player, nil, "this should be skipped", 3) + + print(`[{Player.Name}] :FireList`) + SimpleEvent:FireList({ Player }, 4) + EmptyEvent:FireList({ Player }) + ComplexEvent:FireList({ Player }, { one = { "String Literal", 8 }, two = { 1, "String Literal" } }, "hi", 4) + + print(`[{Player.Name}] :FireWithFilter`) + SimpleEvent:FireWithFilter(function(OtherPlayer) + return OtherPlayer.Name == Player.Name + end, 5) + EmptyEvent:FireWithFilter(function(OtherPlayer) + return OtherPlayer.Name == Player.Name + end) + ComplexEvent:FireWithFilter(function(OtherPlayer) + return OtherPlayer.Name == Player.Name + end, { one = { "String Literal", 17 }, two = { 123, "String Literal" } }, "another string", 5) + + print(`[{Player.Name}] Done!`) + end) +end -EmptyEvent:On(function(Player) - print("EmptyEvent", Player) -end) +function TestSharedEvents() + print("Testing Shared Events") + local SharedEvents = require(ReplicatedStorage.SharedEvents) -ReadyEvent:On(function(Player) - print(`[{Player.Name}] Ready, firing events!`) + print("Registering Event Listeners") - print(`[{Player.Name}] :Fire`) - SimpleEvent:Fire(Player, 1) - EmptyEvent:Fire(Player) - ComplexEvent:Fire(Player, nil, "hello world again", 1) + SharedEvents.Complex:SetServerListener(function(Player, Value1, Value2, Value3) + print("ComplexEvent", Player, Value1, Value2, Value3) + end) - print(`[{Player.Name}] :FireAll`) - SimpleEvent:FireAll(2) - EmptyEvent:FireAll() - ComplexEvent:FireAll({ one = { "String Literal", 123 }, two = { 123, "String Literal" } }, "hello world again", 2) + SharedEvents.Simple:SetServerListener(function(Player, Value) + print("SimpleEvent", Player, Value) + end) - print(`[{Player.Name}] :FireAllExcept`) - SimpleEvent:FireAllExcept(Player, 3) - EmptyEvent:FireAllExcept(Player) - ComplexEvent:FireAllExcept(Player, nil, "this should be skipped", 3) + SharedEvents.Empty:SetServerListener(function(Player) + print("EmptyEvent", Player) + end) - print(`[{Player.Name}] :FireList`) - SimpleEvent:FireList({ Player }, 4) - EmptyEvent:FireList({ Player }) - ComplexEvent:FireList({ Player }, { one = { "String Literal", 8 }, two = { 1, "String Literal" } }, "hi", 4) + SharedEvents.Signal:OnServer(function(Player, Value) + print("SignalEvent1", Player, Value) + end) + SharedEvents.Signal:OnServer(function(Player, Value) + print("SignalEvent2", Player, Value) + end) - print(`[{Player.Name}] :FireWithFilter`) - SimpleEvent:FireWithFilter(function(OtherPlayer) - return OtherPlayer.Name == Player.Name - end, 5) - EmptyEvent:FireWithFilter(function(OtherPlayer) - return OtherPlayer.Name == Player.Name + SharedEvents.Ready:SetServerListener(function(Player) + print(`[{Player.Name}] Ready, firing events!`) + + print(`[{Player.Name}] :FireClient`) + SharedEvents.Empty:FireClient(Player) + SharedEvents.Simple:FireClient(Player, 1) + SharedEvents.Signal:FireClient(Player, 1) + SharedEvents.Complex:FireClient(Player, nil, "hello world again", 1) + + print(`[{Player.Name}] :FireAllClients`) + SharedEvents.Empty:FireAllClients() + SharedEvents.Simple:FireAllClients(2) + SharedEvents.Signal:FireAllClients(2) + SharedEvents.Complex:FireAllClients( + { one = { "String Literal", 123 }, two = { 123, "String Literal" } }, + "hello world again", + 2 + ) + + print(`[{Player.Name}] :FireAllClientsExcept`) + SharedEvents.Empty:FireAllClientsExcept(Player) + SharedEvents.Simple:FireAllClientsExcept(Player, 3) + SharedEvents.Signal:FireAllClientsExcept(Player, 3) + SharedEvents.Complex:FireAllClientsExcept(Player, nil, "this should be skipped", 3) + + print(`[{Player.Name}] :FireClients`) + SharedEvents.Empty:FireClients({ Player }) + SharedEvents.Simple:FireClients({ Player }, 4) + SharedEvents.Signal:FireClients({ Player }, 4) + SharedEvents.Complex:FireClients( + { Player }, + { one = { "String Literal", 8 }, two = { 1, "String Literal" } }, + "hi", + 4 + ) + + print(`[{Player.Name}] :FireFilteredClients`) + SharedEvents.Empty:FireFilteredClients(function(OtherPlayer) + return OtherPlayer.Name == Player.Name + end) + SharedEvents.Simple:FireFilteredClients(function(OtherPlayer) + return OtherPlayer.Name == Player.Name + end, 5) + SharedEvents.Signal:FireFilteredClients(function(OtherPlayer) + return OtherPlayer.Name == Player.Name + end, 5) + SharedEvents.Complex:FireFilteredClients(function(OtherPlayer) + return OtherPlayer.Name == Player.Name + end, { one = { "String Literal", 17 }, two = { 123, "String Literal" } }, "another string", 5) + + print(`[{Player.Name}] Done!`) end) - ComplexEvent:FireWithFilter(function(OtherPlayer) - return OtherPlayer.Name == Player.Name - end, { one = { "String Literal", 17 }, two = { 123, "String Literal" } }, "another string", 5) +end - print(`[{Player.Name}] Done!`) -end) +TestSharedEvents() diff --git a/Test/Shared/SharedEvents.lua b/Test/Shared/SharedEvents.lua new file mode 100644 index 0000000..4adf5d4 --- /dev/null +++ b/Test/Shared/SharedEvents.lua @@ -0,0 +1,24 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local Guard = require(ReplicatedStorage.Packages.Guard) +local Red = require(ReplicatedStorage.Packages.Red) + +local ValueCheck = + Guard.Optional(Guard.Map(Guard.String, Guard.List(Guard.Or(Guard.Literal("String Literal"), Guard.Number)))) + +return { + Empty = Red.SharedEvent("Empty", function() end), + + Ready = Red.SharedEvent("Ready", function() end), + + Simple = Red.SharedEvent("Simple", function(Number) + return Guard.Number(Number) + end), + + Signal = Red.SharedSignalEvent("Signal", function(Number) + return Guard.Number(Number) + end), + + Complex = Red.SharedEvent("Complex", function(Value1, Value2, Value3) + return ValueCheck(Value1), Guard.String(Value2), Guard.Number(Value3) + end), +} diff --git a/lib/SharedEvent.luau b/lib/SharedEvent.luau new file mode 100644 index 0000000..f6ee9e1 --- /dev/null +++ b/lib/SharedEvent.luau @@ -0,0 +1,266 @@ +-- For implementation discussion, see: https://github.com/red-blox/Red/issues/8 + +local Players = game:GetService("Players") +local RunService = game:GetService("RunService") + +local Identifier = require(script.Parent.Identifier) +local Spawn = require(script.Parent.Parent.Spawn) +local Signal = require(script.Parent.Parent.Signal) + +local Net = require(script.Parent.Net) + +type SharedBaseEvent = { + Id: string, + Unreliable: boolean, + + FireClient: (self: SharedBaseEvent, Player: Player, T...) -> (), + FireAllClients: (self: SharedBaseEvent, T...) -> (), + FireAllClientsExcept: (self: SharedBaseEvent, Player: Player, T...) -> (), + FireClients: (self: SharedBaseEvent, Players: { Player }, T...) -> (), + FireFilteredClients: (self: SharedBaseEvent, Filter: (Player) -> boolean, T...) -> (), + + FireServer: (self: SharedBaseEvent, T...) -> (), +} + +export type SharedCallEvent = SharedBaseEvent & { + CallMode: "Call", + + Listener: ((...any) -> ())?, + + SetServerListener: (self: SharedCallEvent, Listener: (Player: Player, T...) -> ()) -> (), + SetClientListener: (self: SharedCallEvent, Listener: (T...) -> ()) -> (), +} + +export type SharedSignalEvent = SharedBaseEvent & { + CallMode: "Signal", + + Signal: Signal.Signal<...any>, + + OnServer: (self: SharedSignalEvent, Listener: (Player: Player, T...) -> ()) -> () -> (), + OnClient: (self: SharedSignalEvent, Listener: (T...) -> ()) -> () -> (), +} + +local function FireClient(self: SharedBaseEvent, Player: Player, ...) + assert(RunService:IsServer(), "FireClient can only be called from the server") + + if self.Unreliable then + Net.Server.SendUnreliableEvent(Player, self.Id, table.pack(...)) + else + Net.Server.SendReliableEvent(Player, self.Id, table.pack(...)) + end +end + +local function FireAllClients(self: SharedBaseEvent, ...) + assert(RunService:IsServer(), "FireAllClients can only be called from the server") + + local Args = table.pack(...) + + if self.Unreliable then + for _, Player in Players:GetPlayers() do + Net.Server.SendUnreliableEvent(Player, self.Id, Args) + end + else + for _, Player in Players:GetPlayers() do + Net.Server.SendReliableEvent(Player, self.Id, Args) + end + end +end + +local function FireAllClientsExcept(self: SharedBaseEvent, Player: Player, ...) + assert(RunService:IsServer(), "FireAllClientsExcept can only be called from the server") + + local Args = table.pack(...) + + if self.Unreliable then + for _, OtherPlayer in Players:GetPlayers() do + if OtherPlayer ~= Player then + Net.Server.SendUnreliableEvent(OtherPlayer, self.Id, Args) + end + end + else + for _, OtherPlayer in Players:GetPlayers() do + if OtherPlayer ~= Player then + Net.Server.SendReliableEvent(OtherPlayer, self.Id, Args) + end + end + end +end + +local function FireClients(self: SharedBaseEvent, Players: { Player }, ...) + assert(RunService:IsServer(), "FireClients can only be called from the server") + + local Args = table.pack(...) + + if self.Unreliable then + for _, Player in Players do + Net.Server.SendUnreliableEvent(Player, self.Id, Args) + end + else + for _, Player in Players do + Net.Server.SendReliableEvent(Player, self.Id, Args) + end + end +end + +local function FireFilteredClients(self: SharedBaseEvent, Filter: (Player) -> boolean, ...) + assert(RunService:IsServer(), "FireFilteredClients can only be called from the server") + + local Args = table.pack(...) + + for _, Player in Players:GetPlayers() do + if Filter(Player) then + if self.Unreliable then + Net.Server.SendUnreliableEvent(Player, self.Id, Args) + else + Net.Server.SendReliableEvent(Player, self.Id, Args) + end + end + end +end + +local function FireServer(self: SharedBaseEvent, ...) + assert(RunService:IsClient(), "FireServer can only be called from the client") + + local Args = table.pack(...) + + if self.Unreliable then + Net.Client.SendUnreliableEvent(self.Id, Args) + else + Net.Client.SendReliableEvent(self.Id, Args) + end +end + +local function SetServerListener(self: SharedCallEvent, Listener: (Player: Player, T...) -> ()) + assert(RunService:IsServer(), "SetServerListener can only be called from the server") + + self.Listener = Listener :: any +end + +local function SetClientListener(self: SharedCallEvent, Listener: (T...) -> ()) + assert(RunService:IsClient(), "SetClientListener can only be called from the client") + + self.Listener = Listener :: any +end + +local function OnServer(self: SharedSignalEvent, Listener: (Player: Player, T...) -> ()): () -> () + assert(RunService:IsServer(), "OnServer can only be called from the server") + + return self.Signal:Connect(Listener :: any) +end + +local function OnClient(self: SharedSignalEvent, Listener: (T...) -> ()): () -> () + assert(RunService:IsClient(), "OnClient can only be called from the client") + + return self.Signal:Connect(Listener :: any) +end + +type SharedEventOptions = { + Name: string, + Unreliable: boolean?, +} + +type ValidateFunction = (...unknown) -> T... + +local function SharedBaseEvent(Name: string, Unreliable: boolean): SharedBaseEvent + local self = { + Id = Identifier.Shared(Name):Await(), + Unreliable = Unreliable, + + FireClient = FireClient, + FireAllClients = FireAllClients, + FireAllClientsExcept = FireAllClientsExcept, + FireClients = FireClients, + FireFilteredClients = FireFilteredClients, + + FireServer = FireServer, + } + + return self :: any +end + +local function SharedCallEvent( + Options: string | SharedEventOptions, + Validate: ValidateFunction +): SharedCallEvent + local ParsedOptions = if typeof(Options) ~= "string" + then Options + else { + Name = Options, + Unreliable = false, + } + + local self: any = SharedBaseEvent(ParsedOptions.Name, ParsedOptions.Unreliable or false) + + self.CallMode = "Call" + + self.SetServerListener = SetServerListener + self.SetClientListener = SetClientListener + + self.Listener = nil :: (...any) -> ()? + + if RunService:IsServer() then + Net.Server.SetListener(self.Id, function(Player, Args) + Spawn(function(Player: Player, ...: any) + if self.Listener and pcall(Validate, ...) then + self.Listener(Player, ...) + end + end, Player, table.unpack(Args)) + end) + else + Net.Client.SetListener(self.Id, function(Args) + Spawn(function(...: any) + if self.Listener and pcall(Validate, ...) then + self.Listener(...) + end + end, table.unpack(Args)) + end) + end + + return self +end + +local function SharedSignalEvent( + Options: string | SharedEventOptions, + Validate: ValidateFunction +): SharedSignalEvent + local ParsedOptions = if typeof(Options) ~= "string" + then Options + else { + Name = Options, + Unreliable = false, + } + + local self: any = SharedBaseEvent(ParsedOptions.Name, ParsedOptions.Unreliable or false) + + self.CallMode = "Signal" + + self.Signal = Signal() + + self.OnServer = OnServer + self.OnClient = OnClient + + if RunService:IsServer() then + Net.Server.SetListener(self.Id, function(Player, Args) + Spawn(function(Player: Player, ...: any) + if pcall(Validate, ...) then + self.Signal:Fire(Player, ...) + end + end, Player, table.unpack(Args)) + end) + else + Net.Client.SetListener(self.Id, function(Args) + Spawn(function(...: any) + if pcall(Validate, ...) then + self.Signal:Fire(...) + end + end, table.unpack(Args)) + end) + end + + return self :: SharedSignalEvent +end + +return { + SharedCallEvent = SharedCallEvent, + SharedSignalEvent = SharedSignalEvent, +} diff --git a/lib/init.luau b/lib/init.luau index 453dae4..61cb76c 100644 --- a/lib/init.luau +++ b/lib/init.luau @@ -22,7 +22,11 @@ else require(script.Net).Client.Start() end +local SharedEvents = require(script.SharedEvent) + return { Event = require(script.Event), Function = require(script.Function), + SharedEvent = SharedEvents.SharedCallEvent, + SharedSignalEvent = SharedEvents.SharedSignalEvent, }