From 1247fc58a891d0504fbd62237a45750e72357d9a Mon Sep 17 00:00:00 2001 From: Javi <4343867+dig1t@users.noreply.github.com> Date: Thu, 16 Jan 2025 16:30:23 +0000 Subject: [PATCH] cleanup --- README.md | 9 +- package.json | 3 +- src/Controller.luau | 240 -------------------------------------- src/Server.luau | 277 -------------------------------------------- src/Store.luau | 162 ++++++++++++++------------ src/init.luau | 11 -- 6 files changed, 89 insertions(+), 613 deletions(-) delete mode 100644 src/Controller.luau delete mode 100644 src/Server.luau diff --git a/README.md b/README.md index d90c017..daf596a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## What is red? red is a lightweight, easy-to-use, and efficient event-driven library for Roblox. -red provides a simple API that allows you to create and manage events, actions, and state in your game, making it easy to build complex systems and features. +red provides a simple API that allows you to create and manage events, actions, and state in your game. ## Benefits - **Simple API**: red is designed to be easy to use and understand, making it perfect for developers of all skill levels. @@ -26,10 +26,3 @@ red = "dig1t/red@2.0.0" Download the rbxl file from the [releases](https://github.com/dig1t/red/releases) tab. Once the place file is open, you can find the package inside `ReplicatedStorage.Packages`. - -## Games powered by red - - - - -Add your game here! Create a Pull Request to add your game to the list. diff --git a/package.json b/package.json index 1170725..42fb66d 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "build:rojo": "rojo build -o dev-build.rbxl build.project.json", "build:docs": "moonwave build", "build": "npm run build:dependencies && npm run build:rojo", - "dev": "npm run build && dev-build.rbxl && npm start", "start": "rojo serve build.project.json" }, @@ -23,6 +22,6 @@ }, "homepage": "https://github.com/dig1t/red#readme", "dependencies": { - "moonwave": "^1.1.3" + "moonwave": "^1.2.1" } } diff --git a/src/Controller.luau b/src/Controller.luau deleted file mode 100644 index 6261706..0000000 --- a/src/Controller.luau +++ /dev/null @@ -1,240 +0,0 @@ ---[=[ -@class Controller - -Controller is a class that is used to manage different features of the game. -It can be ran on the client and server. - -```lua -local controller = red.Controller.new({ - name = "test"; -}) - -controller:init(function() - print("init") -end) - --- You can subscribe to red actions through the controller -controller:subscribe(function(action: red.Action) - print(action.type) -end) - --- You can also listen to the stepped event -controller:stepped(function(deltaTime: number) - self.timeElapsed = self.time + deltaTime - print(self.timeElapsed) -end) -``` -]=] - ---!strict - -local RunService = game:GetService("RunService") - -local Store = require(script.Parent.Store) -local Types = require(script.Parent.Types) -local Util = require(script.Parent.Parent.Util) - -local Controller = {} -Controller.__index = Controller - --- Controller -export type ControllerConfig = { - name: string, - [any]: any, -} - -export type ControllerType = typeof(setmetatable( - {} :: { - name: string, - controllerId: string, - redController: boolean, - [any]: any, - }, - Controller -)) - -local store = Store.new() - -local controllers: { [string]: ControllerType } = {} -local storeSubscriptions: { [number]: string } = {} - -local _subscriptionId: string = store:subscribe(function(action: Types.Action) - for _, controllerId: string in pairs(storeSubscriptions) do - local controller: ControllerType = controllers[controllerId] - - if controller and controller.subscribe then - controller:subscribe(action) - end - end -end) - -RunService.Stepped:Connect(function(deltaTime: number) - for _, controller: ControllerType in pairs(controllers) do - if controller.stepped then - controller:stepped(deltaTime) - end - end -end) - ---[=[ -Creates a new controller instance - -@param config ControllerConfig -@return ControllerType -]=] -function Controller.new(config: ControllerConfig): ControllerType - assert(typeof(config) == "table", "Expected config to be a table") - assert(typeof(config.name) == "string", "Expected config.name to be a string") - assert( - not controllers[config.name], - "Controller with name " .. config.name .. " already exists" - ) - - if config.init then - assert(typeof(config.init) == "function", "Expected config.init to be a function") - end - - if config.subscribe then - assert( - typeof(config.subscribe) == "function", - "Expected config.subscribe to be a function" - ) - end - - local self = setmetatable({}, Controller) - - self.name = config.name - self.controllerId = Util.randomString() - self.redController = true - - return self -end - ---[=[ -Called when the controller is initialized - -@within Controller -@method Destroy -]=] -function Controller.Destroy(self: ControllerType) - controllers[self.name] = nil - - local subscriptionIndex = table.find(storeSubscriptions, self.controllerId) - - if subscriptionIndex then - table.remove(storeSubscriptions, subscriptionIndex) - end -end - ---[=[ -Adds controller ModuleScripts from a folder or table - -Setting a requiredSuffix will only add modules that end with the suffix. -(e.g. "Controller" will only add modules that end with "Controller") - -This will call the ```init``` method on all controllers once they are added. - -Call ```Controller.start()``` to call the ```ready``` method on all controllers. - -@param _controllers Instance | { ModuleScript } -@param requiredSuffix string? -]=] -function Controller.addModules( - _controllers: Instance | { ModuleScript }, - requiredSuffix: string? -) - local modules: { ModuleScript } = {} - - if typeof(_controllers) == "Instance" then - if requiredSuffix then - local children: { Instance } = _controllers:GetDescendants() - - _controllers = {} - - for _, module: Instance in ipairs(children) do - if - module - and module:IsA("ModuleScript") - and module.Name:sub(-#requiredSuffix) == requiredSuffix - then - local _module: ModuleScript = module - modules[#modules + 1] = _module - end - end - else - for _, child: Instance in ipairs(_controllers:GetChildren()) do - if child and child:IsA("ModuleScript") then - modules[#modules + 1] = child - end - end - end - elseif typeof(_controllers) == "table" then - for _, module: ModuleScript in ipairs(_controllers) do - assert(typeof(module) == "Instance", "Expected module to be an Instance") - end - end - - for _, module: ModuleScript in pairs(modules) do - local newController: ControllerType = require(module) :: any - - if not newController.name then - warn(`{module.Name} does not have a name`) - continue - end - - if newController.init then - assert( - typeof(newController.init) == "function", - "Expected controller.init to be a function" - ) - - task.spawn(function() - debug.setmemorycategory(newController.name) - newController:init() - end) - end - - if newController.subscribe then - storeSubscriptions[#storeSubscriptions + 1] = newController.controllerId - end - - controllers[newController.name] = newController - end -end - ---- Calls the ```ready``` method on all controllers. -function Controller.start() - for _, controller: ControllerType in pairs(controllers) do - if not controller.ready then - continue - end - - assert( - typeof(controller.ready) == "function", - "Expected controller.ready to be a function" - ) - - task.spawn(function() - debug.setmemorycategory(controller.name) - controller:ready() - end) - end -end - ---[=[ -Gets a controller by name - -@param name string -@return ControllerType? -]=] -function Controller.get(name: string): ControllerType? - assert(typeof(name) == "string", "Expected name to be a string") - - if not controllers[name] then - warn(`Controller with name {name} does not exist`) - end - - return controllers[name] -end - -return Controller diff --git a/src/Server.luau b/src/Server.luau deleted file mode 100644 index e11e7bc..0000000 --- a/src/Server.luau +++ /dev/null @@ -1,277 +0,0 @@ ---[=[ -@class Server - -The Server class is used to create a new server instance. - -#### Setup -```lua -local ReplicatedStorage = game:GetService("ReplicatedStorage") - -local red = require(ReplicatedStorage.Packages.red) - -local server: red.ServerType = red.Server.new() -- Constructs a new server - -server:init() -- Starts listening to dispatches -``` -]=] - ---!strict - -local Store = require(script.Parent.Store) -local Types = require(script.Parent.Types) -local redUtil = require(script.Parent.redUtil) - -local remotes = redUtil.getRemotes() -local store = Store.new() - -local Server = {} -Server.__index = Server - -export type ServerType = typeof(setmetatable( - {} :: { - _reducers: { - [string]: (player: Player, payload: Types.ActionPayload?) -> any, - }, - started: boolean, - }, - Server -)) - ---[=[ -Creates a new server instance - -@return ServerType -]=] -function Server.new(): ServerType - return setmetatable({ - _reducers = {}, - started = false, - store = store, - }, Server) :: ServerType -end - ---[=[ -Use this method to bind a reducer to the server. - -Examples: -```lua -server:bind("ACTION_TYPE", function(player: Player, payload: Types.ActionPayload?) - -- Do something -end) -``` - -```lua -server:bind("PLAYER_KILL", function(player) - if player and player.Character then - player.Character:BreakJoints() - end -end, true) -``` - -@within Server -@method bind -@param actionType string -@param reducer () -> () -]=] -function Server.bind( - self: ServerType, - actionType: string, - reducer: (player: Player, payload: Types.ActionPayload) -> () -) - assert(typeof(actionType) == "string", "Action type must be a string") - assert(typeof(reducer) == "function", "Action must be a function") - - self._reducers[actionType] = reducer -end - ---[=[ -Use this method to unbind a handler from the server. - -Example: -```lua -server:unbind("ACTION_TYPE") -``` - -@within Server -@method unbind -@param id string -]=] -function Server.unbind(self: ServerType, id: string) - assert(self._reducers[id], "Action reducer does not exist") - - self._reducers[id] = nil -end - ---[=[ -Use this method to load a handler into the server. - -Example: -```lua -server:useHandler(game.ServerScriptService.Server.ActionHandlers.Players) -``` - -@within Server -@method useHandler -@param moduleInstance ModuleScript -]=] -function Server.useHandler(self: ServerType, moduleInstance: ModuleScript) - assert( - typeof(moduleInstance) == "Instance" and moduleInstance:IsA("ModuleScript"), - "Path is not a ModuleScript" - ) - - local handler = require(moduleInstance) :: any - - assert(typeof(handler) == "function", "Handler must be a function") - - debug.setmemorycategory(`Action Handler: {moduleInstance.Name}`) - handler(self) -end - ---[=[ -Use this method to load multiple handlers modules into the server. - -Example: -```lua -server:useHandlers(game.ServerScriptService.Server.Handlers:GetChildren()) -``` - -@within Server -@method useHandlers -@param handlers { ModuleScript } -]=] -function Server.useHandlers(self: ServerType, handlers: { ModuleScript } | Instance) - for _, handler: ModuleScript in - pairs( - typeof(handlers) == "Instance" and handlers:GetChildren() - or handlers :: { ModuleScript } - ) - do - self:useHandler(handler) - end -end - ---[=[ -Use this method to call an action. - -Example: -```lua -server:_call(action, player) -``` - -@within Server -@method _call -@param action Types.Action -@param player Player? -@return Types.Action -]=] -function Server._call( - self: ServerType, - action: Types.Action, - player: Player -): Types.Action - assert(action and typeof(action.type) == "string", "Action type must be a string") - - if not self._reducers[action.type] then - return { - type = action.type, - err = "Action does not exist", - success = false, - } :: Types.Action - end - - local success: boolean, res: Types.Action | string = - pcall(self._reducers[action.type], player, action.payload) - - if not success then - task.spawn(function() - local userId: number = player.UserId - - if typeof(res) == "string" then - error(res .. (userId and ` - UserId: {userId}` or ""), 2) - end - end) - - return { - type = action.type, - err = typeof(res) == "string" and res or nil, - success = false, - } :: Types.Action - end - - -- Add a success property to the action - if typeof(res) == "table" and not res.err and res.success == nil then - res.success = true - elseif not res then - res = { - type = action.type, - success = true, - } - end - - return res :: Types.Action -end - ---[=[ -Initializes the server. - -@within Server -@method listen -]=] -function Server.listen(self: ServerType) - -- store:dispatch() called from client - remotes.Client.OnServerEvent:Connect( - function(player: Player, action: Types.Action) - if action.method and action.method == "get_result" then - return - end - - local res: Types.Action = self:_call(action, player) - - -- If the action's method is "get" fire it back - -- to the sender - if action.method and action.method == "get" then - assert( - typeof(res) == "table", - action.type - .. " - Cannot return a " - .. typeof(res) - .. " type to a client." - ) - - -- Change method to get result since - -- the server is now firing the result - -- to the client. - res.method = "get_result" - res.uid = action.uid - - remotes.Client:FireClient(player, res) - end - end - ) - - self.started = true -end - ---[=[ -Use this method to dispatch an action to the store from a handler - -Example: -```lua -server:dispatch({ - type = "ACTION_TYPE", - payload = { - -- Payload - } -}) -``` - -@within Server -@method dispatch -@param ... any -]=] -function Server.dispatch(_, ...) - store:dispatch(...) -end - -return Server diff --git a/src/Store.luau b/src/Store.luau index 4c4da56..5eaeaed 100644 --- a/src/Store.luau +++ b/src/Store.luau @@ -1,6 +1,6 @@ --[=[ @class Store - + Dispatcher for actions to be dispatched to the server or clients. #### Setup @@ -9,32 +9,30 @@ local red = require(ReplicatedStorage.Packages.red) - local store: red.StoreType = red.Store.new() -- Constructs a new store - -- Client - store:subscribe(function(action: red.Action) + red.Store.subscribe(function(action: red.Action) print(action.type) end) - - store:dispatch({ + + red.Store.dispatch({ type = "HELLO_WORLD", payload = { message = "Hello World" } }) - + -- Server - + -- Dispatch to all clients - store:dispatch(true, { + red.Store.dispatch(true, { type = "HELLO_WORLD", payload = { message = "Hello World" } }) - + -- Dispatch to one client - store:dispatch(Players.Player1, { + red.Store.dispatch(Players.Player1, { type = "HELLO_WORLD", payload = { message = "Hello World" @@ -61,13 +59,8 @@ Store.__index = Store local _middleware: { (action: Types.Action) -> () } = {} -export type StoreType = typeof(setmetatable( - {} :: { - _connections: { [number]: RBXScriptConnection }, - _subscribers: { [Types.SubscriptionId]: (action: Types.Action) -> nil }, - }, - Store -)) +local _connections: { [number]: RBXScriptConnection } = {} +local _subscribers: { [Types.SubscriptionId]: (action: Types.Action) -> nil } = {} --[=[ Applies middleware to the store @@ -75,13 +68,13 @@ export type StoreType = typeof(setmetatable( The middleware function will be called before the action is dispatched. ```lua - store:use(function(action: red.Action) + store.use(function(action: red.Action) if action.type == "HELLO_WORLD" then action.payload.message = action.payload.message .. "!" end end) - store:dispatch({ + store.dispatch({ type = "HELLO_WORLD", payload = { message = "Hello World" @@ -99,20 +92,6 @@ function Store.use(middleware: (action: Types.Action) -> ()) _middleware[#_middleware + 1] = middleware end ---[=[ - Creates a new store instance - - @return StoreType -]=] -function Store.new(): StoreType - local self = setmetatable({}, Store) :: StoreType - - self._connections = {} - self._subscribers = {} - - return self -end - --[=[ Dispatches an action @@ -126,12 +105,12 @@ end local Players = game:GetService("Players") -- Dispatch to the server - store:dispatch({ + store.dispatch({ type = "PLAYER_KILL", player = Players.Player1 -- The first argument in the binded action. }) - store:dispatch({ + store.dispatch({ type = "PLAYER_DAMAGE", player = { Players.Player2, Players.Player3 } payload = { -- The second argument in the binded action. @@ -139,7 +118,7 @@ end } }) - store:dispatch({ -- Called from the server + store.dispatch({ -- Called from the server type = "GAME_STOP", payload = { -- This would be the first argument since there is no reason to include a player parameter. message = "Game over" @@ -147,7 +126,7 @@ end }) -- Dispatch to all clients - store:dispatch(true, { + store.dispatch(true, { type = "UI_NOTIFICATION", payload = { text = "Hello World!" @@ -155,12 +134,12 @@ end }) -- Dispatch to one client - store:dispatch(Players.Player1, { + store.dispatch(Players.Player1, { type = "UI_SPECTATE_START" }) -- Dispatch to multiple clients - store:dispatch({ Players.Player2, Players.Player3 }, { + store.dispatch({ Players.Player2, Players.Player3 }, { type = "UI_GAME_TIMER", payload = { duration = 60 -- Show a countdown timer lasting 60 seconds @@ -173,7 +152,7 @@ end @param target Player | { Player } | true @param action Types.Action ]=] -function Store.dispatch(_self: StoreType, ...: any) +function Store.dispatch(...: any) local args: { any } = { ... } local action: Types.Action = #args == 1 and args[1] or args[2] @@ -242,7 +221,7 @@ end ```lua -- Client - local fetch = store:get({ -- Fetch player stats from the server + local fetch = store.get({ -- Fetch player stats from the server type = "PLAYER_STATS", }) local stats = fetch.success and fetch.payload.stats @@ -254,14 +233,13 @@ end @within Store @method get - @param self StoreType @param action Types.Action @return Types.Action ]=] -function Store.get(self: StoreType, action: Types.Action): Types.Action +function Store.get(action: Types.Action): Types.Action assert(typeof(action) == "table", "Action must be a table") assert(action.type, "Action must have an action type") - assert(not isServer, "Store:get() must be called from the client") + assert(not isServer, "Store.get() must be called from the client") local uid: string = Util.randomString(8) @@ -281,7 +259,7 @@ function Store.get(self: StoreType, action: Types.Action): Types.Action) res = result @@ -315,18 +293,14 @@ function Store.get(self: StoreType, action: Types.Action): Types.Action end -function Store._callSubscribers( - self: StoreType, - action: Types.Action, - safeCall: boolean? -) +function Store._callSubscribers(action: Types.Action, safeCall: boolean?) if not action.type or action.method == "get" or action.method == "get_result" then -- Ignore get method actions, these - -- will be processed by store:get() + -- will be processed by store.get() return end - for _, callback in pairs(self._subscribers) do + for _, callback in pairs(_subscribers) do if safeCall then pcall(callback, action) else @@ -346,7 +320,6 @@ end @return Types.SubscriptionId ]=] function Store.subscribe( - self: StoreType, callback: (action: Types.Action) -> () ): Types.SubscriptionId assert( @@ -356,71 +329,110 @@ function Store.subscribe( local subscriptionId: Types.SubscriptionId = Util.randomString(8) -- Unique reference ID used for unsubscribing - if Util.tableLength(self._subscribers) == 0 then + if Util.tableLength(_subscribers) == 0 then -- Hook connections to start receiving events if isServer then - -- store:dispatch() called from client - self._connections[#self._connections + 1] = remotes.Client.OnServerEvent:Connect( + -- store.dispatch() called from client + _connections[#_connections + 1] = remotes.Client.OnServerEvent:Connect( function(player: Player, action: Types.Action) action.player = player - self:_callSubscribers(action, true) + Store._callSubscribers(action, true) end ) else - -- store:dispatch() called from server - self._connections[#self._connections + 1] = remotes.Client.OnClientEvent:Connect( + -- store.dispatch() called from server + _connections[#_connections + 1] = remotes.Client.OnClientEvent:Connect( function(action: Types.Action) - self:_callSubscribers(action, false) + Store._callSubscribers(action, false) end ) end end - self._subscribers[subscriptionId] = callback + _subscribers[subscriptionId] = callback return subscriptionId end --[=[ - Unsubscribes a listener from the store using the subscription id returned from ```Store:subscribe()```. + Subscribes to specific action types + + ```lua + store.subscribeTo("HELLO_WORLD", function(action: red.Action) + print(action.type) + end) + ``` + + ```lua + store.subscribeTo({ "HELLO_WORLD", "PING" }, function(action: red.Action) + print(action.type) + end) + ``` + + @within Store + @method subscribeTo + @param actionType { string } | string + @param callback (action: Types.Action) -> () + @return Types.SubscriptionId +]=] +function Store.subscribeTo( + actionType: { string } | string, + callback: (action: Types.Action) -> () +) + if typeof(actionType) == "string" then + actionType = { actionType } + end + + local actionTypes = actionType :: { string } + + local subscriptionId: Types.SubscriptionId = Store.subscribe( + function(action: Types.Action) + if table.find(actionTypes, action.type) then + callback(action) + end + end + ) + + return subscriptionId +end + +--[=[ + Unsubscribes a listener from the store using the subscription id returned from ```Store.subscribe()```. ```lua -- Setup the listener local connectionId - connectionId = store:subscribe(function(action: red.Action) + connectionId = store.subscribe(function(action: red.Action) if action.type == "HELLO_WORLD" then print(action.type) - store:unsubscribe(connectionId) -- Stop receiving actions + store.unsubscribe(connectionId) -- Stop receiving actions end end) task.wait(2) - store:unsubscribe(connectionId) -- Stop receiving actions + store.unsubscribe(connectionId) -- Stop receiving actions ``` @within Store @method unsubscribe @param id Types.SubscriptionId ]=] -function Store.unsubscribe(self: StoreType, id: Types.SubscriptionId) - assert( - self._subscribers[id], - "Store:unsubscribe() - Event listener id does not exist" - ) +function Store.unsubscribe(id: Types.SubscriptionId) + assert(_subscribers[id], "Store.unsubscribe() - Event listener id does not exist") - self._subscribers[id] = nil + _subscribers[id] = nil - if Util.tableLength(self._subscribers) == 0 then + if Util.tableLength(_subscribers) == 0 then -- Disconnect all connections to prevent -- unneeded events from being received - for index: number, connection: RBXScriptConnection in pairs(self._connections) do + for index: number, connection: RBXScriptConnection in pairs(_connections) do connection:Disconnect() - self._connections[index] = nil + _connections[index] = nil end end end diff --git a/src/init.luau b/src/init.luau index 814eaa1..efcb31b 100644 --- a/src/init.luau +++ b/src/init.luau @@ -7,15 +7,11 @@ local red = {} red.remotes = redUtil.getRemotes() -local Controller = require(script.Controller) -local Server = require(script.Server) local State = require(script.State) local Store = require(script.Store) -red.Server = Server red.State = State red.Store = Store -red.Controller = Controller export type SubscriptionId = string @@ -24,11 +20,4 @@ export type Action = Types.Action export type StateType = State.StateType -export type ServerType = Server.ServerType - -export type ControllerConfig = Controller.ControllerConfig -export type ControllerType = Controller.ControllerType - -export type StoreType = Store.StoreType - return red