├── .eslintrc.json ├── .gitignore ├── .moonwave └── static │ ├── favicon.png │ └── logo.png ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── aftman.toml ├── default.project.json ├── docs ├── Client-Server Example.md ├── Getting Started.md ├── Installation.md ├── Server-Client Example.md └── intro.md ├── moonwave.toml ├── package-lock.json ├── package.json ├── publish.project.json ├── selene.toml ├── src ├── Bridge.lua ├── ClientBridge.lua ├── CreateBridgeTree.lua ├── SerdesLayer.lua ├── ServerBridge.lua ├── index.d.ts └── init.lua ├── test.project.json ├── tests ├── client │ └── ClientTest2.client.lua ├── framework │ ├── bootstrapper.lua │ ├── expect.lua │ └── init.lua └── server │ └── ServerTest2.server.lua ├── tsconfig.json ├── wally.lock └── wally.toml /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/eslint-recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:@typescript-eslint/recommended-requiring-type-checking" 11 | ], 12 | "globals": { 13 | "Atomics": "readonly", 14 | "SharedArrayBuffer": "readonly" 15 | }, 16 | "parser": "@typescript-eslint/parser", 17 | "parserOptions": { 18 | "project": "./tsconfig.json", 19 | "ecmaFeatures": { 20 | "jsx": true 21 | }, 22 | "ecmaVersion": 2018, 23 | "sourceType": "module" 24 | }, 25 | "plugins": [ 26 | "@typescript-eslint" 27 | ], 28 | "rules": { 29 | "linebreak-style": "off", 30 | "indent": [ 31 | "off" 32 | ], 33 | "quotes": [ 34 | "error", 35 | "single", 36 | { 37 | "avoidEscape": true 38 | } 39 | ], 40 | "semi": [ 41 | "error", 42 | "always" 43 | ], 44 | "no-empty-function": "off", 45 | "@typescript-eslint/no-empty-function": "off", 46 | "@typescript-eslint/no-explicit-any": "off", 47 | "@typescript-eslint/no-unsafe-argument": "error" 48 | } 49 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project place file 2 | /BridgeNet.rbxlx 3 | 4 | # Roblox Studio lock files 5 | /*.rbxlx.lock 6 | /*.rbxl.lock 7 | node_modules 8 | Packages 9 | /*.lock 10 | build -------------------------------------------------------------------------------- /.moonwave/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ffrostfall/BridgeNet/89d2b26ec1793a79e845127d904964b5dd0c45c5/.moonwave/static/favicon.png -------------------------------------------------------------------------------- /.moonwave/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ffrostfall/BridgeNet/89d2b26ec1793a79e845127d904964b5dd0c45c5/.moonwave/static/logo.png -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "roblox-ts.vscode-roblox-ts", 4 | "dbaeumer.vscode-eslint" 5 | ] 6 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "files.eol": "\n", 4 | "[typescript]": { 5 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 6 | "editor.formatOnSave": true 7 | }, 8 | "[typescriptreact]": { 9 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 10 | "editor.formatOnSave": true 11 | }, 12 | "eslint.run": "onType", 13 | "eslint.format.enable": true 14 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | As of v2.0.0, this project now adheres to semver. 3 | 4 | ## 2.0.0 5 | - Added the separation of client/server middleware in CreateBridgeTree 6 | - Heavy optimization 7 | - Thread reuse has been added (client-receive only atm) 8 | - New property for Bridge objects: NilAllowed. 9 | - Documentation! 10 | - Typescript port has been updated 11 | - SerdeLayer now uses attributes rather than StringValue instances 12 | 13 | ## 2.0.0-rc4 14 | - Unpacked arguments on server receive (Thank you @MELON-Om4r) 15 | - Fixed numerous queue-related bugs 16 | - Invoke UUIDs are now packed for less network usage (34 bytes -> 18 bytes) 17 | - The Identifiers function is a closure again 18 | - Added outbound middleware 19 | - Added middleware to the client 20 | - Middleware now passes in the ``plr`` argument on the server 21 | - Overall middleware improvements 22 | - Client-sided improvements w/ connections 23 | - Added .GetQueue() for debugging purposes 24 | - General improvements to client receive 25 | - Temporarily removed warning signals until I can figure out a better way to add them, they're kind of a mess right now. 26 | - Removed config symbols 27 | - Removed logging features- it turns out I forgot to fully implement them, plus nobody used them. 28 | - **Removed BridgeNet.Start(), the module now runs when you require it for the first time.** 29 | - Removed :InvokeServer() 30 | - Removed both dependencies 31 | - Updated typescript port 32 | - Fixed Docusaurus dependency- oops. 33 | 34 | ## 2.0.0-rc3 35 | - Multiple :Fire()s can be sent in the same frame 36 | - Performance improvements 37 | - Bugfixes w/ SerdesLayer & replication 38 | - Added more test cases- 2.0.0 should be usable and more stable. 39 | - Fixed invokes 40 | 41 | ## 2.0.0-rc2 42 | - Middleware now is defaulted off if there's nothing in the table 43 | - Some small improvements 44 | - Renamed ``Declare`` to ``CreateBridgeTree`` 45 | - Exposed the typings ``Bridge``, ``ClientBridge`` and ``ServerBridge`` to the user. 46 | - Added ``Bridge:SetReplicationRate()`` 47 | - Started on a better way of doing releases for wally and non-wally. Kinda experimenting right now! 48 | 49 | ## 2.0.0-rc1 50 | - Removed rateManager entirely 51 | - Removed .CreateIdentifiersFromDictionary() 52 | - Removed .CreateBridgesFromDictionary() 53 | - Removed .WhatIsThis() 54 | - Removed .WaitForBridge() 55 | - Removed PrintRemotes symbol- it was useless. 56 | - Added GetCompressedIdentifier 57 | - Added .Declare() 58 | - Added .Identifiers() 59 | - Added .GetFromCompressed() 60 | - Added .GetFromIdentifier() 61 | - Each BridgeObject now has a variable rate it sends information at. This is by default 60. 62 | - A lot of functions are now modules that return a function 63 | - Repeat loops are now while loops 64 | - Renamed serdeLayer to SerdesLayer 65 | - Optimizations 66 | - Symbols are now loaded in via a module 67 | - Added hot reloading support(?) 68 | - Rewrote test code 69 | 70 | ### Changes to be done 71 | - Remove receive queueing 72 | - Typings should use ``never`` and ``unknown`` types 73 | - Add :SetReplicationRate(). There should be a partial implementation already there 74 | - Finish polishing and testing, then do the full release. 75 | 76 | ## 1.9.9-beta 77 | - Functions that rely on .Start will yield until started 78 | 79 | ## 1.9.8-beta 80 | - Switched for loops to be generics for consistency. This should help performance. 81 | - Switched time limit to be .5 milliseconds 82 | - Fixed Bridge:Destroy()? 83 | - Type improvements 84 | 85 | ## 1.9.7-beta 86 | - BridgeNet.Started has been added 87 | - Middleware fixes. Should be stable now 88 | 89 | ## 1.8.7-beta 90 | - Hotfix for ClientObject:Fire() 91 | 92 | ## 1.8.6-beta 93 | - You can now pass nil as parameters 94 | 95 | ## 1.8.5-beta 96 | - Added better performance profiling 97 | - Added ExceededTimeLimit signal 98 | - Added InternalError signal (Unused for now) 99 | - Added server-sided middleware (no typescript support yet, sorry ): [UNSTABLE] 100 | - Added :SetMiddleware() 101 | - Added :AddMiddleware() 102 | - Middleware will be added to the client soon enough 103 | - Added .CreateIdentifiersFromDictionary 104 | - Added .WaitForIdentifier, client-sided only. 105 | - ReceiveLogFunction and SendLogFunction are now stable and ready to be used 106 | - Fixed symbols for roblox-ts(?) 107 | - Improved typings for Luau 108 | - Better error handling 109 | 110 | ## 1.7.5-beta 111 | - Improved typings for both ts and luau 112 | - Re-added :Once() 113 | - Updated dependency versions 114 | 115 | ## 1.6.5-beta 116 | - Fixed invokes 117 | - Added documentation for invokes 118 | - Added .CreateBridgesFromDictionary() 119 | - Significantly improved / fixed roblox-ts typings 120 | 121 | ## 1.5.5-beta 122 | - Added "RemoteFunction"-type API 123 | - Added ServerBridge:OnInvoke(function() end) 124 | - Added ClientBridge:InvokeServerAsync(), yields. 125 | - Added ClientBridge:InvokeServer(), returns a promise instead of yielding. 126 | - Refactored some code to be better-organized. 127 | - Refactored project structure / testing code to allow for dependencies 128 | - Added Promise as a dependency 129 | - Added GoodSignal as a dependency 130 | 131 | ## 1.4.5-beta 132 | - Ported to typescript! 133 | 134 | ## 1.4.4-beta 135 | - You no longer need to declare DefaultReceive and DefaultSend- they default to 60. 136 | - Fixed ServerBridge:Destroy() 137 | - Added print message while waiting for the ClientBridge to be replicated 138 | - Removed the print statement in OnClientEvent. oops! 139 | 140 | ## 1.4.3-beta 141 | - Removed .FromBridge, use .WaitForBridge or .CreateBridge (createbridge returns the existing bridge object if it exists) 142 | - Configuration object now uses symbols instead of regular strings 143 | - Added global custom logging support. UNSTABLE, DONT USE IN PRODUCTION 144 | - Changed some loops to use ipairs instead of pairs 145 | - Used table.clear instead of tbl = {} for better efficiency 146 | - Fixed Disconnect 147 | - Optimizations (thank you @Baileyeatspizza) 148 | - Fixed ClientBridge breaking if the client's bridge was created before the server created the bridge (thank you evanchan0819) 149 | - Fixed client-to-server communication only sending the first argument 150 | 151 | ## 0.4.3-alpha 152 | - Connections now spawn a thread, making them yield-safe and error-proof. 153 | - Added .WaitForBridge() 154 | - Added Roact's Symbol class- not used for now, will be used for .Start configuration in the future. 155 | - .CreateBridge() now has the same functionality of .FromBridge() 156 | - Server now checks for the BridgeObject to exist before trying to run connections. If it doesn't exist, nothing happens. 157 | 158 | ## 0.3.3-alpha 159 | - Hotfix for .CreateIdentifier() 160 | 161 | ## 0.3.2-alpha 162 | - Connections now use pairs instead of ipairs 163 | 164 | ## 0.3.1-alpha 165 | - Better error handling / messages 166 | - Removed unused function in ServerBridge/ClientBridge. 167 | - Added .CreateUUID(), .PackUUID(), .UnpackUUID(). (ty Pyseph!) 168 | - Added .DictionaryToTable(), which converts a dictionary into an alphabetically-ordered table. 169 | - Switched .ChildAdded for the client's serdeLayer to be in serdeLayer._start() 170 | - Switched "Network" documentation to be "BridgeNet"- Network was a working title. 171 | - Removed one_remote_event from config. 172 | 173 | ## 0.2.1-alpha 174 | - Better error handling and messages 175 | - Errors during send/receive will not repeat due to failure to clear queue 176 | - If the queue is blank, it will not send. 177 | 178 | ## 0.2.0-alpha 179 | - Some optimizations and polishing 180 | - Added .FromBridge(), which lets you get a Bridge object from wherever. 181 | - Fixed an issue where an unused artifact was being sent, increasing size drastically 182 | - Fixed multiple documentation mistakes 183 | 184 | ## 0.1.0-alpha 185 | - Initial release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ffrostflame 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # BridgeNet 4 | 5 | Insanely optimized networking library for Roblox, with roblox-ts support. 6 | 7 | * [Documentation](https://ffrostflame.github.io/BridgeNet/) 8 | * [Latest release](https://github.com/ffrostflame/BridgeNet/releases) (v1.9.9-beta) 9 | * [BridgeNet on Roblox Marketplace](https://www.roblox.com/library/10494533012/BridgeNet-v1-8-7-beta) (v1.9.9-beta) 10 | 11 | BridgeNet is a networking library bundled with features to make optimizations easier, alongside optimizing remote events itself. It also has numerous features such as `:FireAllInRange` and `:FireToMultiple`. 12 | 13 | ## Features 14 | * Easy-to-use utility features such as `:FireAllInRange()`, `:FireAllExcept()`, `:FireMultiple()` 15 | * Dynamically create/destroy “remote events” with ease 16 | * Configure the rate of which remotes send and receive information 17 | * Serialization of RemoteEvents for optimization 18 | * Using compressed ID strings to reduce the amount of data each remote call takes, mimicking multiple remote calls. 19 | * Utilities to compress the data you’re sending over such as `.DictionaryToTable`, and `.PackUUID` 20 | * Incredibly easy optimization beyond what’s already provided 21 | * No direct interaction with instances, that’s all abstracted and wrapped away. 22 | * `InvokeServer` (returns a Promise) and `InvokeServerAsync` allow for `RemoteFunction` usability with promises 23 | * roblox-ts support! 24 | 25 | ## Goals 26 | * Make optimization easy, but manual. Don’t intrude on the developer where they don’t expect it, but give them the tools to optimize. 27 | * Keep a simple and human-readable API while still retaining functionality of other net tools. 28 | * Make all functionality extra - you don’t need to understand middleware in order to use the module, but if you do, you can use middleware. 29 | 30 | ## Upcoming features 31 | * Middleware and rate limiting. There's internal support right now, but I delayed it for a later date due to some architecture concerns. 32 | * Typechecking 33 | 34 | ## For contributors 35 | Please add your changes to CHANGELOG.md when you make a PR, it makes it a lot easier on me to make new releases and prevents a lot of confusion. 36 | 37 | ## Performance 38 | 39 | This test was run with 200 blank remote calls sent per frame. In this case, BridgeNet used 42.7% less bandwidth. 40 | 41 | **BridgeNet** 63 KB/s average: 42 | 43 | 44 | 45 | **Roblox** 110 KB/s average: 46 | 47 | 48 | 49 | ## Why should I care about the amount of data being sent/received? 50 | 51 | Because it's integral to your game's performance. Less data and fewer calls means lower frame times, lower ping, easier for players with packet loss and bad connections, and overall a better experience. 52 | 53 | If you have a player cap on your game of 50, and each player is receiving 100 kilobytes per second, that means your server is sending 5,000 kilobytes per second. 5,000/1,000 (kilobytes in a megabyte) is 5, which means you have 5 megabytes being sent out per second. Now, we all know Roblox servers are suboptimal compared to your average dedicated game server. And 5 megabytes... isn’t that much nowadays, right? 54 | 55 | In the networking world, megabits are used to measure things like speed. One megabyte is 8 megabits, and things like speedtest.net use megabits (abbreviated as Mb). So, if your internet upload speed is 40 megabits per second, that means running your computer as a server for your Roblox game would result in your entire bandwith being taken up. 56 | 57 | ## On the topic of networking, reliablity types are a must 58 | 59 | > As a Roblox developer, it is currently impossible to send network messages over anything other than a reliable ordered channel. This is a huge problem for networked game state that needs to be sent very frequently. Re-transmissions and acknowledgements are nice for data that must get there eventually, but it really blows for state that is just going to be immediately sent again in the next network step. 60 | > 61 | >The biggest use case for something like this is custom character replication. We only care about the most recent position and orientation of a replicated character, and we don’t want old stale state to be re-transmitted to us. 62 | > 63 | >I propose adding the property RemoteEvent.Reliability which determines the reliability type of network messages sent using it. This property would be an enum, and could include ReliableOrdered (the way it works now), Unreliable (packets may be dropped), and UnreliableSequenced (same as unreliable, but only the most recent message is accepted). 64 | > 65 | >Being able to send unreliable messages is a bare necessity for creating low-latency multiplayer games. In my opinion this would allow developers to fine tune their games’ networking in order to deliver a more consistent experience. 66 | > 67 | > — [HaxHelper](https://devforum.roblox.com/t/reliability-types-for-remoteevent/308510) 68 | 69 | Due to Roblox’s RemoteEvents being reliable and ordered, it can make systems like head rotation systems cause tons of lag and take up a bunch of unused bandwith. It also means any custom humanoid system like Chickynoid will be less effective than if this feature existed. 70 | 71 | This feature would improve the Roblox platform tremendously if added, and I ask you all to support this feature. If this is added, I can assure you BridgeNet will immediately update to have streaming data support. 72 | 73 | The lack of this feature is a direct roadblock to many MMOs. It’s one of the biggest roadblocks on the platform right now. Please- take your time and show your support. 74 | 75 | -------------------------------------------------------------------------------- /aftman.toml: -------------------------------------------------------------------------------- 1 | # This file lists tools managed by Aftman, a cross-platform toolchain manager. 2 | # For more information, see https://github.com/LPGhatguy/aftman 3 | 4 | # To add a new tool, add an entry to this table. 5 | [tools] 6 | rojo = "rojo-rbx/rojo@7.2.1" 7 | wally = "UpliftGames/wally@0.3.1" 8 | # rojo = "rojo-rbx/rojo@6.2.0" -------------------------------------------------------------------------------- /default.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bridgenet", 3 | "tree": { 4 | "$path": "src" 5 | } 6 | } -------------------------------------------------------------------------------- /docs/Client-Server Example.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | --- 4 | 5 | # Client-Server Example 6 | 7 | ## Server 8 | ```lua name="example.server.lua" 9 | local ReplicatedStorage = game:GetService("ReplicatedStorage") 10 | 11 | local BridgeNet = require(ReplicatedStorage.Packages.BridgeNet) 12 | 13 | local Remote = BridgeNet.CreateBridge("Remote") 14 | 15 | Remote:Connect(function(plr, stringA, stringB) 16 | print(stringA .. stringB) -- Prints "Hello, server!" 17 | end) 18 | ``` 19 | 20 | ## Client 21 | ```lua name="example.client.lua" 22 | local ReplicatedStorage = game:GetService("ReplicatedStorage") 23 | 24 | local BridgeNet = require(ReplicatedStorage.Packages.BridgeNet) 25 | 26 | local Remote = BridgeNet.CreateBridge("Remote") 27 | 28 | while true do 29 | Remote:Fire("Hello, ", "server!") 30 | 31 | task.wait(1) 32 | end 33 | ``` -------------------------------------------------------------------------------- /docs/Getting Started.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # Getting Started 6 | BridgeNet uses objects known as "bridges". These objects are the equivalent of RemoteEvents in normal Roblox. They are created as such: 7 | ```lua title="init.lua" 8 | local Bridge = BridgeNet.CreateBridge("RemoteNameHere") 9 | Bridge:FireAll("Firing all players") 10 | ``` 11 | All the optimizations are handled for you! These are packaged, sent out with a compressed string ID, and received on the client. 12 | 13 | ## Using the identifier system 14 | A common pattern in Roblox are string constants that are sent over the wire as their full identity, which wastes data- they never change. 15 | Identifier strings are 1-2 character strings that represent constant strings which you define. This saves on bandwith because sending shorter strings 16 | instead of longer strings saves on data. These are typically static, and can depict things like action requests, item names, all of that. 17 | This library provides an easy system to optimize these: the 2 functions ``CreateIdentifier`` and ``DestroyIdentifier``. They are used as such: 18 | ```lua title="spellHandler.client.lua" 19 | local SpellCaster = BridgeNet.CreateBridge("SpellCaster") 20 | 21 | local Fireball = BridgeNet.CreateIdentifier("Fireball") 22 | 23 | SomeUserInputSignalHere:Connect(function() 24 | SpellCaster:Fire(Fireball) -- Fires a 1 or 2 character string, much smaller than an 8-character string. 25 | end) 26 | ``` -------------------------------------------------------------------------------- /docs/Installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Installation 6 | 7 | ## With Wally 8 | 9 | 1. Install [Wally](https://wally.run) 10 | 2. Put BridgeNet in the ``wally.toml`` file under ``[dependencies]`` 11 | ```toml title="wally.toml" 12 | [dependencies] 13 | BridgeNet = ffrostflame/bridgenet@0.1.0 14 | ``` 15 | 3. Run ``wally install`` 16 | 17 | ## Without Wally 18 | 1. Get the ``.rbxm`` file from the latest [release](https://github.com/ffrostflame/BridgeNet/releases). 19 | 2. Sync manually or drop into studio manually -------------------------------------------------------------------------------- /docs/Server-Client Example.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # Server-Client Example 6 | 7 | ## Server 8 | ```lua name="example.server.lua" 9 | local ReplicatedStorage = game:GetService("ReplicatedStorage") 10 | 11 | local BridgeNet = require(ReplicatedStorage.Packages.BridgeNet) 12 | 13 | local Remote = BridgeNet.CreateBridge("Remote") 14 | 15 | while true do 16 | Remote:FireAll("Hello, ", "world!") -- Fires to everyone 17 | Remote:FireTo(game.Players.Someone, "Hello, ", "someone!") -- Fires to a specific player 18 | task.wait(1) 19 | end 20 | ``` 21 | 22 | ## Client 23 | ```lua name="example.client.lua" 24 | local ReplicatedStorage = game:GetService("ReplicatedStorage") 25 | 26 | local BridgeNet = require(ReplicatedStorage.Packages.BridgeNet) 27 | 28 | local Remote = BridgeNet.CreateBridge("Remote") 29 | 30 | Remote:Connect(function(stringA, stringB) 31 | print(stringA .. stringB) -- Prints "Hello, someone!" 32 | end) 33 | ``` -------------------------------------------------------------------------------- /docs/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # BridgeNet 6 | 7 | BridgeNet is a networking library that solves a multitude of annoyances and problems when working directly with RemoteEvents, while remaining performant 8 | and not losing the ability to easily debug. BridgeNet takes a philosophy of letting the developer optimize what's sent over the wire, while optimizing the calls itself, 9 | trying to be as non-intrusive as possible. 10 | 11 | ## Features 12 | - A multitude of utility functions such as ``:FireAllInRange()``, ``:FireAllExcept``, and ``:FireAllInRangeExcept``. 13 | - Directly cutting down the amount of data it takes to call a remote event 14 | - Easy-to-use, dynamic serialization/deserialization layer 15 | - Dynamic send/receive rates 16 | - Dynamically creating RemoteEvents while keeping all the above features 17 | 18 | ## Upcoming features (order = priority) 19 | - Support for rate limiting 20 | - Typechecking 21 | 22 | ## Prior art 23 | - RbxNet 24 | - This is a continuation of my previous networking system [NetworkObject](https://devforum.roblox.com/t/networkobject-a-light-weight-network-module-usable-for-everyone/1526416) 25 | - This [devforum post](https://devforum.roblox.com/t/ore-one-remote-event/569721/25) by Tomarty -------------------------------------------------------------------------------- /moonwave.toml: -------------------------------------------------------------------------------- 1 | title = "BridgeNet" 2 | gitRepoUrl = "https://github.com/ffrostflame/bridgenet/" 3 | 4 | gitSourceBranch = "main" 5 | changelog = true 6 | 7 | [docusaurus] 8 | projectName = "BridgeNet" 9 | tagline = "Easy to use, optimized, and full of useful functionality." 10 | favicon = "favicon.png" 11 | 12 | [navbar.logo] 13 | alt = "BridgeNet" 14 | src = "logo.png" 15 | 16 | [footer] 17 | style = "dark" 18 | 19 | [home] 20 | enabled = true 21 | includeReadme = false -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rbxts/bridgenet", 3 | "version": "2.0.0-rc4", 4 | "description": "A networking library for Roblox", 5 | "main": "src/init.lua", 6 | "repository": "https://github.com/ffrostflame/BridgeNet", 7 | "homepage": "https://ffrostflame.github.io/BridgeNet/", 8 | "typings": "src/index.d.ts", 9 | "keywords": [ 10 | "roblox", 11 | "networking", 12 | "net-library", 13 | "roblox-ts", 14 | "bridge", 15 | "bridgenet" 16 | ], 17 | "author": "@rbxts", 18 | "license": "MIT", 19 | "devDependencies": { 20 | "@rbxts/compiler-types": "^1.3.3-types.1", 21 | "@rbxts/types": "^1.0.642", 22 | "@typescript-eslint/eslint-plugin": "^5.33.0", 23 | "@typescript-eslint/parser": "^5.33.0", 24 | "eslint": "^8.21.0", 25 | "eslint-config-prettier": "^8.5.0", 26 | "eslint-plugin-prettier": "^4.2.1", 27 | "eslint-plugin-roblox-ts": "^0.0.34", 28 | "prettier": "^2.7.1", 29 | "roblox-ts": "^1.3.3", 30 | "typescript": "^4.7.4", 31 | "typescript-transform-paths": "^3.3.1", 32 | "@docusaurus/core": "^2.0.0-rc.1", 33 | "@docusaurus/preset-classic": "^2.0.0-rc.1" 34 | }, 35 | "dependencies": { 36 | }, 37 | "files": [ 38 | "src" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /publish.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bridgenet-publish", 3 | "tree": { 4 | "$className": "DataModel", 5 | "ReplicatedStorage": { 6 | "$className": "ReplicatedStorage", 7 | "Packages": { 8 | "$path": "Packages", 9 | "BridgeNet": { 10 | "$path": "default.project.json" 11 | } 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /selene.toml: -------------------------------------------------------------------------------- 1 | std = "roblox" -------------------------------------------------------------------------------- /src/Bridge.lua: -------------------------------------------------------------------------------- 1 | type config = { 2 | ReplicationRate: number?, 3 | NilAllowed: boolean, 4 | Server: { 5 | InboundMiddleware: { (...any) -> ...any }?, 6 | OutboundMiddleware: { (...any) -> ...any }?, 7 | }?, 8 | Client: { 9 | InboundMiddleware: { (...any) -> ...any }?, 10 | OutboundMiddleware: { (...any) -> ...any }?, 11 | }?, 12 | } 13 | 14 | return function(config: config?) 15 | if config == nil then 16 | return { _isBridge = true } 17 | end 18 | return { 19 | _isBridge = true, 20 | server = config["Server"], 21 | client = config["Client"], 22 | replicationrate = config["ReplicationRate"], 23 | allowsnil = config["NilAllowed"], 24 | } 25 | end 26 | -------------------------------------------------------------------------------- /src/ClientBridge.lua: -------------------------------------------------------------------------------- 1 | --!strict 2 | local ReplicatedStorage = game:GetService("ReplicatedStorage") 3 | local RunService = game:GetService("RunService") 4 | 5 | local SerdesLayer = require(script.Parent.SerdesLayer) 6 | 7 | type sendPacketQueue = { remote: string, args: { any }, requestType: string, replRate: number, uuid: string? } 8 | type receivePacketQueue = { remote: string, args: { any } } 9 | 10 | local RemoteEvent: RemoteEvent 11 | local Invoke: string 12 | local InvokeReply: string 13 | local SendQueue: { sendPacketQueue } = {} 14 | local ReceiveQueue: { receivePacketQueue } = {} 15 | local BridgeObjects = {} 16 | local threads: { thread? } = {} 17 | local freeThread = nil 18 | 19 | local FromCompressed = SerdesLayer.FromCompressed 20 | 21 | local function functionPasser(fn, ...) 22 | fn(...) 23 | end 24 | 25 | local function yielder() 26 | while true do 27 | functionPasser(coroutine.yield()) 28 | end 29 | end 30 | 31 | local function maybeSpawn(fn, ...) 32 | if not freeThread then 33 | freeThread = coroutine.create(yielder) 34 | coroutine.resume(freeThread) 35 | end 36 | local acquiredThread = freeThread 37 | freeThread = nil 38 | task.spawn(acquiredThread, fn, ...) 39 | freeThread = acquiredThread 40 | end 41 | 42 | --[=[ 43 | @class ClientBridge 44 | 45 | Client-sided object for handling networking. 46 | ]=] 47 | local ClientBridge = {} 48 | ClientBridge.__index = ClientBridge 49 | 50 | local function Connection(obj, v, callback) 51 | local result 52 | for _, func in obj._inboundMiddleware do 53 | if result then 54 | local potential = { func(table.unpack(result)) } 55 | if potential[1] == nil then 56 | return 57 | end 58 | result = potential 59 | else 60 | result = { func(table.unpack(v.args)) } 61 | end 62 | end 63 | 64 | result = result or v.args 65 | 66 | callback(table.unpack(result)) 67 | end 68 | 69 | local function ConnectionWithNil(obj, v, callback, argCount) 70 | local result 71 | for _, func in obj._inboundMiddleware do 72 | if result then 73 | local potential = { func(table.unpack(result), 1, argCount) } 74 | if potential[1] == nil then 75 | return 76 | end 77 | result = potential 78 | else 79 | result = { func(table.unpack(v.args, 1, argCount)) } 80 | end 81 | end 82 | 83 | result = result or v.args 84 | 85 | callback(table.unpack(result)) 86 | end 87 | 88 | local function ConnectionWithoutMiddleware(callback, args) 89 | callback(table.unpack(args)) 90 | end 91 | 92 | local function ConnectionWithoutMiddlewareWithNil(callback, args, argCount) 93 | callback(table.unpack(args), 1, argCount) 94 | end 95 | 96 | --[=[ 97 | Starts the internal processes for ClientBridge. 98 | 99 | @ignore 100 | ]=] 101 | function ClientBridge._start() 102 | RemoteEvent = ReplicatedStorage:WaitForChild("RemoteEvent") 103 | 104 | Invoke = SerdesLayer.FromIdentifier("Invoke") 105 | InvokeReply = SerdesLayer.FromIdentifier("InvokeReply") 106 | 107 | local passingReplRates = {} 108 | 109 | RunService.Heartbeat:Connect(function() 110 | debug.profilebegin("ClientBridge") 111 | local currentTime = os.clock() 112 | 113 | debug.profilebegin("HandleReceive") 114 | for _, v in ReceiveQueue do 115 | local obj = BridgeObjects[FromCompressed(v.remote)] 116 | if not obj then 117 | continue 118 | end 119 | 120 | local args = v.args 121 | local allowsNil = obj._allowsNil 122 | 123 | if allowsNil then 124 | for i = 1, #args do 125 | if args[i] == SerdesLayer.NilIdentifier then 126 | args[i] = nil 127 | end 128 | end 129 | end 130 | 131 | if args[1] == InvokeReply then 132 | local argCount = #args 133 | local uuid = SerdesLayer.UnpackUUID(args[2]) 134 | table.remove(args, 1) 135 | table.remove(args, 1) 136 | argCount -= 2 137 | task.spawn(threads[uuid], table.unpack(args, 1, argCount)) 138 | threads[uuid] = nil -- don't want a memory leak ;) 139 | continue 140 | end 141 | 142 | if #obj._inboundMiddleware == 0 then 143 | if allowsNil then 144 | for _, callback in obj._connections do 145 | maybeSpawn(Connection, obj, args, callback) 146 | end 147 | else 148 | for _, callback in obj._connections do 149 | maybeSpawn(ConnectionWithNil, obj, args, callback, #args) 150 | end 151 | end 152 | else 153 | if allowsNil then 154 | for _, callback in obj._connections do 155 | maybeSpawn(ConnectionWithoutMiddleware, callback, args) 156 | end 157 | else 158 | for _, callback in obj._connections do 159 | maybeSpawn(ConnectionWithoutMiddlewareWithNil, callback, args, #args) 160 | end 161 | end 162 | end 163 | end 164 | table.clear(ReceiveQueue) 165 | debug.profileend() 166 | 167 | debug.profilebegin("HandleSend") 168 | local toSend = {} 169 | local replTicks = {} 170 | local remainingQueue = {} 171 | 172 | for i, v in remainingQueue do 173 | if (currentTime - replTicks[v.replRate]) <= (1 / v.replRate - 0.003) then 174 | table.insert(SendQueue, v) 175 | continue 176 | else 177 | table.remove(remainingQueue, i) 178 | end 179 | end 180 | 181 | for _, v: sendPacketQueue in SendQueue do 182 | if replTicks[v.replRate] then 183 | -- subtract 0.003 to make sure we don't accidentally skip any frames due to rounding errors 184 | if (currentTime - replTicks[v.replRate]) <= (1 / v.replRate - 0.003) then 185 | passingReplRates[v.replRate] = true 186 | if not passingReplRates[v.replRate] then 187 | table.insert(remainingQueue, v) 188 | continue 189 | end 190 | end 191 | end 192 | 193 | replTicks[v.replRate] = currentTime 194 | 195 | for i = 1, #v.args do 196 | if v.args[i] == nil then 197 | v.args[i] = SerdesLayer.NilIdentifier 198 | end 199 | end 200 | 201 | if v.requestType == "invoke" then 202 | local tbl = { v.remote, Invoke, v.uuid } 203 | 204 | for _, k in v.args do 205 | table.insert(tbl, k) 206 | end 207 | 208 | table.insert(toSend, tbl) 209 | elseif v.requestType == "send" then 210 | local tbl = { v.remote } 211 | local bridgeObj = BridgeObjects[FromCompressed(v.remote)] 212 | 213 | if not (#bridgeObj._outboundMiddleware == 0) then 214 | local result 215 | for _, func in bridgeObj._outboundMiddleware do 216 | if result then 217 | local potential = { func(table.unpack(result)) } 218 | if #potential == 0 then 219 | continue 220 | end 221 | result = potential 222 | else 223 | result = { func(table.unpack(v.args)) } 224 | end 225 | end 226 | 227 | if result == nil then 228 | result = v.args 229 | end 230 | 231 | for _, k in result do 232 | table.insert(tbl, k) 233 | end 234 | else 235 | for _, k in v.args do 236 | table.insert(tbl, k) 237 | end 238 | end 239 | 240 | table.insert(toSend, tbl) 241 | end 242 | end 243 | 244 | if #toSend ~= 0 then 245 | RemoteEvent:FireServer(toSend) 246 | end 247 | SendQueue = remainingQueue 248 | debug.profileend() 249 | 250 | debug.profileend() 251 | end) 252 | 253 | RemoteEvent.OnClientEvent:Connect(function(tbl) 254 | for _, v in tbl do 255 | local params = v 256 | local remote = params[1] 257 | table.remove(params, 1) 258 | table.insert(ReceiveQueue, { 259 | remote = remote, 260 | args = params, 261 | }) 262 | end 263 | end) 264 | end 265 | 266 | function ClientBridge.new(remoteName: string) 267 | assert(type(remoteName) == "string", "[BridgeNet] remote name must be a string") 268 | 269 | local found = ClientBridge.from(remoteName) 270 | if found ~= nil then 271 | return found 272 | end 273 | 274 | local self = setmetatable({}, ClientBridge) 275 | 276 | self._name = remoteName 277 | self._connections = {} 278 | 279 | self._replRate = 60 280 | 281 | self._inboundMiddleware = {} 282 | self._outboundMiddleware = {} 283 | 284 | self._id = SerdesLayer.FromIdentifier(self._name) 285 | if self._id == nil then 286 | task.spawn(function() 287 | local timer = 0 288 | local nextOutput = timer + 0.1 289 | repeat 290 | timer += task.wait() 291 | self._id = SerdesLayer.FromIdentifier(self._name) 292 | if timer > nextOutput then 293 | nextOutput += 0.1 294 | print(string.format("[BridgeNet] waiting for %* to be created on the server", self._name)) 295 | end 296 | until self._id ~= nil or timer >= 10 297 | end) 298 | end 299 | 300 | BridgeObjects[self._name] = self 301 | return self 302 | end 303 | 304 | function ClientBridge.from(remoteName: string) 305 | assert(type(remoteName) == "string", "[BridgeNet] Remote name must be a string") 306 | return BridgeObjects[remoteName] 307 | end 308 | 309 | function ClientBridge.waitForBridge(remoteName: string) 310 | while not BridgeObjects[remoteName] do 311 | task.wait() 312 | end 313 | return BridgeObjects[remoteName] 314 | end 315 | 316 | function ClientBridge._returnQueue() 317 | return SendQueue, ReceiveQueue 318 | end 319 | 320 | --[=[ 321 | The equivalent of :FireServer(). 322 | 323 | ```lua 324 | local Bridge = BridgeNet.CreateBridge("Remote") 325 | 326 | Bridge:Fire("Hello", "world!") 327 | ``` 328 | 329 | @param ... any 330 | ]=] 331 | function ClientBridge:Fire(...: any) 332 | if self._id == nil then 333 | self._id = SerdesLayer.FromIdentifier(self._name) 334 | end 335 | table.insert(SendQueue, { 336 | remote = self._id, 337 | requestType = "send", 338 | args = { ... }, 339 | replRate = self._replRate, 340 | }) 341 | end 342 | 343 | --[=[ 344 | Invokes the server for a response. Promise wrapper over :InvokeServerAsync() 345 | 346 | ```lua 347 | local Bridge = BridgeNet.CreateBridge("Remote") 348 | 349 | local data = Bridge:InvokeServerAsync("whats 2+2?") 350 | print(data) -- prints "4" 351 | ``` 352 | 353 | @param ... any 354 | @return ...any 355 | ]=] 356 | function ClientBridge:InvokeServerAsync(...: any) 357 | if self._id == nil then 358 | self._id = SerdesLayer.FromIdentifier(self._name) 359 | end 360 | 361 | local thread = coroutine.running() 362 | local uuid = SerdesLayer.CreateUUID() 363 | 364 | threads[uuid] = thread 365 | 366 | table.insert(SendQueue, { 367 | remote = self._id, 368 | requestType = "invoke", 369 | uuid = SerdesLayer.PackUUID(uuid), 370 | args = { ... }, 371 | replRate = self._replRate, 372 | }) 373 | 374 | local response = { coroutine.yield() } 375 | if response[1] == "err" then 376 | error(response[2], 2) 377 | end 378 | 379 | return table.unpack(response) 380 | end 381 | 382 | --[=[ 383 | Creates a connection. Can be disconnected using :Disconnect(). 384 | 385 | ```lua 386 | local Bridge = BridgeNet.CreateBridge("Remote") 387 | 388 | Bridge:Connect(function(text) 389 | print(text) 390 | end) 391 | ``` 392 | 393 | @param func function 394 | @return nil 395 | ]=] 396 | function ClientBridge:Connect(func: (...any) -> nil) 397 | assert(type(func) == "function", "[BridgeNet] Attempt to connect non-function to a Bridge") 398 | local stashedRef = func 399 | 400 | local disconnect = function() 401 | table.remove(self._connections, table.find(self._connections, stashedRef)) 402 | end 403 | 404 | return disconnect 405 | end 406 | 407 | --[[ 408 | Gets the ClientBridge's name. 409 | 410 | ```lua 411 | local Bridge = BridgeNet.CreateBridge("Remote") 412 | 413 | print(Bridge:GetName()) -- Prints "Remote" 414 | ``` 415 | 416 | @return string 417 | ]] 418 | function ClientBridge:GetName() 419 | return self._name 420 | end 421 | 422 | --[=[ 423 | Creates a connection, when fired it will disconnect. 424 | 425 | ```lua 426 | local Bridge = BridgeNet.CreateBridge("ConstantlyFiringText") 427 | 428 | Bridge:Connect(function(text) 429 | print(text) -- Fires multiple times 430 | end) 431 | 432 | Bridge:Once(function(text) 433 | print(text) -- Fires once 434 | end) 435 | ``` 436 | 437 | @param func function 438 | @return nil 439 | ]=] 440 | function ClientBridge:Once(func: (...any) -> nil) 441 | assert(typeof(func) == "function", "[BridgeNet] :once() requires a function to be passed through") 442 | local connection 443 | connection = self:Connect(function(...) 444 | connection:Disconnect() 445 | func(...) 446 | end) 447 | end 448 | 449 | --[=[ 450 | Sets the rate of which the Bridge sends and receives data. 451 | 452 | @param replRate number 453 | @return nil 454 | ]=] 455 | function ClientBridge:SetReplicationRate(replRate: number) 456 | assert(typeof(replRate) == "number", "[BridgeNet] replication rate must be a number") 457 | self._replRate = replRate 458 | end 459 | 460 | --[=[ 461 | Sets the Bridge's outbound middleware functions. Any function which returns nil will drop the sequence completely. Overrides existing middleware. 462 | 463 | A more comprehensive guide on middleware will be coming soon. 464 | ```lua 465 | Object:SetOutboundMiddleware({ 466 | function(plr, ...) -- Called first 467 | return ... 468 | end, 469 | function(plr, ...) -- Called second 470 | print("1") 471 | return ... 472 | end, 473 | function(plr, ...) -- Called third 474 | print("2") 475 | return ... 476 | end, 477 | }) 478 | ``` 479 | 480 | @param middlewareTbl { (...any) -> nil } 481 | @return nil 482 | ]=] 483 | function ClientBridge:SetOutboundMiddleware(middlewareTbl: { (plr: Player, ...any) -> ...any }) 484 | assert(typeof(middlewareTbl) == "table", "[BridgeNet] outbound middleware must be a table") 485 | self._outboundMiddleware = middlewareTbl 486 | end 487 | 488 | --[=[ 489 | Sets the Bridge's inbound middleware functions. Any function which returns nil will drop the remote request completely. Overrides existing middleware. 490 | 491 | Allows you to change arguments or drop remote calls. 492 | 493 | A more comprehensive guide on middleware will be coming soon. 494 | ```lua 495 | Object:SetInboundMiddleware({ 496 | function(...) -- Called first 497 | return ... 498 | end, 499 | function(...) -- Called second 500 | print("1") 501 | return ... 502 | end, 503 | function(...) -- Called third 504 | print("2") 505 | return ... 506 | end, 507 | }) 508 | ``` 509 | 510 | @param middlewareTbl { (...any) -> nil } 511 | @return nil 512 | ]=] 513 | function ClientBridge:SetInboundMiddleware(middlewareTbl: { (plr: Player, ...any) -> ...any }) 514 | assert(typeof(middlewareTbl) == "table", "[BridgeNet] inbound middleware must be a table") 515 | self._inboundMiddleware = middlewareTbl 516 | end 517 | 518 | --[=[ 519 | Allows nil parameters to be passed through without any weirdness. Does have a performance cost- this is off by default. 520 | 521 | @param allowed boolean 522 | @return nil 523 | ]=] 524 | function ClientBridge:SetNilAllowed(allowed: boolean) 525 | assert(typeof(allowed) == "boolean", "[BridgeNet] cannot set nilAllowed to a non-bool") 526 | self._nilAllowed = allowed 527 | end 528 | 529 | --[=[ 530 | Destroys the ClientBridge object. Doesn't destroy the RemoteEvent, or destroy the identifier. It doesn't send anything to the server. Just destroys the client sided object. 531 | 532 | ```lua 533 | local Bridge = ClientBridge.new("Remote") 534 | 535 | ClientBridge:Destroy() 536 | 537 | ClientBridge:Fire() -- Errors 538 | ``` 539 | 540 | @return nil 541 | ]=] 542 | function ClientBridge:Destroy() 543 | BridgeObjects[self._name] = nil 544 | for k, v in self do 545 | if v.Destroy ~= nil then 546 | v:Destroy() 547 | else 548 | self[k] = nil 549 | end 550 | end 551 | setmetatable(self, nil) 552 | end 553 | 554 | export type ClientObject = typeof(ClientBridge.new("")) 555 | 556 | return ClientBridge 557 | -------------------------------------------------------------------------------- /src/CreateBridgeTree.lua: -------------------------------------------------------------------------------- 1 | local RunService = game:GetService("RunService") 2 | 3 | local ServerBridge = require(script.Parent.ServerBridge) 4 | local ClientBridge = require(script.Parent.ClientBridge) 5 | 6 | local function search(name, v) 7 | local ReturnValue 8 | local bridge = if RunService:IsServer() then ServerBridge.new(name) else ClientBridge.new(name) 9 | 10 | if v["server"] and RunService:IsServer() then 11 | if v["OutboundMiddleware"] then 12 | bridge:SetOutboundMiddleware(v.outboundmiddleware) 13 | end 14 | 15 | if v["InboundMiddleware"] then 16 | bridge:SetInboundMiddleware(v.inboundmiddleware) 17 | end 18 | end 19 | 20 | if v["client"] and not RunService:IsServer() then 21 | if v["OutboundMiddleware"] then 22 | bridge:SetOutboundMiddleware(v.outboundmiddleware) 23 | end 24 | 25 | if v["InboundMiddleware"] then 26 | bridge:SetInboundMiddleware(v.inboundmiddleware) 27 | end 28 | end 29 | 30 | if v["replicationrate"] then 31 | bridge:SetReplicationRate(v.replicationrate) 32 | end 33 | 34 | if v["allowsnil"] then 35 | bridge:SetNilAllowed(true) 36 | end 37 | 38 | ReturnValue = bridge 39 | 40 | return ReturnValue 41 | end 42 | 43 | local function recursiveSearch(passedTable) 44 | local ReturnValue = {} 45 | for k, v in passedTable do 46 | assert( 47 | type(v) == "table", 48 | "Everything in BridgeNet.CreateBridgeTree must be a dictionary or BridgeNet.Bridge()" 49 | ) 50 | 51 | if v._isBridge == true then 52 | ReturnValue[k] = search(k, v) 53 | else 54 | ReturnValue[k] = recursiveSearch(v) 55 | continue 56 | end 57 | end 58 | return ReturnValue 59 | end 60 | 61 | return recursiveSearch 62 | -------------------------------------------------------------------------------- /src/SerdesLayer.lua: -------------------------------------------------------------------------------- 1 | --!strict 2 | local HttpService = game:GetService("HttpService") 3 | local ReplicatedStorage: ReplicatedStorage = game:GetService("ReplicatedStorage") 4 | local RunService: RunService = game:GetService("RunService") 5 | 6 | local receiveDict: { [string]: string? } = {} 7 | local sendDict: { [string]: string? } = {} 8 | local numOfSerials: number = 0 9 | 10 | --[=[ 11 | @class SerdesLayer 12 | 13 | This module handles serialization and deserialization for you. 14 | ]=] 15 | local SerdesLayer = {} 16 | 17 | local AutoSerde: Folder = nil 18 | local isServer = RunService:IsServer() 19 | 20 | type toSend = string 21 | 22 | SerdesLayer.NilIdentifier = "null" 23 | 24 | local function fromHex(toConvert: string): string 25 | return string.gsub(toConvert, "..", function(cc) 26 | return string.char(tonumber(cc, 16)) 27 | end) :: string 28 | end 29 | 30 | local function toHex(toConvert: string): string 31 | return string.gsub(toConvert, ".", function(c) 32 | return string.format("%02X", string.byte(c :: any)) 33 | end) :: string 34 | end 35 | 36 | function SerdesLayer._start() 37 | if RunService:IsClient() then 38 | AutoSerde = ReplicatedStorage:WaitForChild("AutoSerde") 39 | for id, value in AutoSerde:GetAttributes() do 40 | sendDict[id] = value 41 | receiveDict[value] = id 42 | end 43 | AutoSerde.AttributeChanged:Connect(function(id: string) 44 | local packed: string = AutoSerde:GetAttribute(id) 45 | if packed then 46 | sendDict[id] = packed 47 | receiveDict[packed] = id 48 | else 49 | local oldValue = sendDict[id] 50 | sendDict[id] = nil 51 | receiveDict[oldValue] = nil 52 | end 53 | end) 54 | else 55 | AutoSerde = Instance.new("Folder") 56 | AutoSerde.Name = "AutoSerde" 57 | AutoSerde.Parent = ReplicatedStorage 58 | end 59 | end 60 | 61 | function SerdesLayer._destroy() 62 | AutoSerde:Destroy() 63 | end 64 | 65 | --[=[ 66 | Creates an identifier and associates it with a compressed value. This is shared between the server and the client. 67 | If the identifier already exists, it will be returned. 68 | 69 | ```lua 70 | BridgeNet.CreateIdentifier("Something") 71 | 72 | print(BridgeNet.WhatIsThis("Something", "compressed")) 73 | ``` 74 | 75 | @param id string 76 | @return string 77 | ]=] 78 | function SerdesLayer.CreateIdentifier(id: string): string 79 | assert(type(id) == "string", "ID must be a string") 80 | 81 | if not sendDict[id] and not isServer then 82 | return SerdesLayer.WaitForIdentifier(id) 83 | elseif sendDict[id] and not isServer then 84 | return sendDict[id] 85 | end 86 | 87 | if numOfSerials >= 65536 then 88 | error("Over the identification cap: " .. id) 89 | end 90 | numOfSerials += 1 91 | 92 | local packed: string = string.pack("H", numOfSerials) 93 | AutoSerde:SetAttribute(id, packed) 94 | 95 | sendDict[id] = packed 96 | receiveDict[packed] = id 97 | 98 | return packed 99 | end 100 | 101 | function SerdesLayer.WaitForIdentifier(id: string): string 102 | while sendDict[id] == nil do 103 | task.wait() 104 | end 105 | return sendDict[id] 106 | end 107 | 108 | --[=[ 109 | Retrieves the full version of a compressed string 110 | 111 | @param compressed string 112 | @return string 113 | ]=] 114 | function SerdesLayer.FromCompressed(compressed: string) 115 | return receiveDict[compressed] 116 | end 117 | 118 | --[=[ 119 | Retrieves the compressed version of an identifier string 120 | 121 | @param identifier string 122 | @return string 123 | ]=] 124 | function SerdesLayer.FromIdentifier(identifier: string) 125 | return sendDict[identifier] 126 | end 127 | 128 | --[=[ 129 | Creates an identifier and associates it with a compressed value. This is shared between the server and the client. 130 | 131 | @param id string 132 | @return nil 133 | ]=] 134 | function SerdesLayer.DestroyIdentifier(id: string): nil 135 | assert(isServer, "You cannot destroy identifiers on the client.") 136 | assert(type(id) == "string", "ID must be a string") 137 | 138 | receiveDict[sendDict[id]] = nil 139 | sendDict[id] = nil 140 | 141 | numOfSerials -= 1 142 | 143 | AutoSerde:SetAttribute(id, nil) 144 | return nil 145 | end 146 | 147 | --[=[ 148 | Creates a UUID. 149 | 150 | ```lua 151 | print(BridgeNet.CreateUUID()) -- Prints 93179AF839C94B9C975DB1B4A4352D75 152 | ``` 153 | 154 | @return string 155 | ]=] 156 | function SerdesLayer.CreateUUID(): string 157 | return string.gsub(HttpService:GenerateGUID(false), "-", "") :: string 158 | end 159 | 160 | --[=[ 161 | Packs a UUID in hexadecimal form into a string, which can be sent over network as smaller. 162 | 163 | ```lua 164 | print(BridgeNet.PackUUID(BridgeNet.CreateUUID())) -- prints something like �#F}ЉF��\�rY�* 165 | ``` 166 | 167 | @param uuid string 168 | @return string 169 | ]=] 170 | function SerdesLayer.PackUUID(uuid: string): string 171 | assert(typeof(uuid) == "string", "[BridgeNet] uuid must be a string") 172 | return fromHex(uuid) 173 | end 174 | 175 | --[=[ 176 | Takes a packed UUID and convetrs it into hexadecimal/readable form 177 | 178 | ```lua 179 | print(BridgeNet.UnpackUUID(somePackedUUID)) -- Prints 93179AF839C94B9C975DB1B4A4352D75 180 | ``` 181 | 182 | @param uuid string 183 | @return string 184 | ]=] 185 | function SerdesLayer.UnpackUUID(uuid: string): string 186 | assert(typeof(uuid) == "string", "[BridgeNet] uuid must be a string") 187 | return toHex(uuid) 188 | end 189 | 190 | --[=[ 191 | Alphabetically sorts a dictionary and turns it into a table. Useful because string keys are typically unnecessary when sending things 192 | over the wire. 193 | 194 | Please note: This doesn't play too nicely with special characters. 195 | 196 | ```lua 197 | print(BridgeNet.DictionaryToTable({ alpha = 999, bravo = 1000, charlie = 1001, delta = 1002 })) -- prints {999,1000,1001,1002} 198 | ``` 199 | 200 | @param dict {[string]: any} 201 | @return string 202 | ]=] 203 | function SerdesLayer.DictionaryToTable(dict: { [string]: any }) 204 | assert(typeof(dict) == "table", "[BridgeNet] dict must be a dictionary") 205 | assert(getmetatable(dict) == nil, "[BridgeNet] Passed dictionary may not have a metatable") 206 | 207 | local keys = {} 208 | for key, _ in dict do 209 | table.insert(keys, key) 210 | end 211 | 212 | table.sort(keys, function(a, b) 213 | return string.lower(a) < string.lower(b) 214 | end) 215 | 216 | local toReturn = {} 217 | for _, v in keys do 218 | table.insert(toReturn, dict[v]) 219 | end 220 | 221 | return toReturn 222 | end 223 | 224 | return SerdesLayer 225 | -------------------------------------------------------------------------------- /src/ServerBridge.lua: -------------------------------------------------------------------------------- 1 | local RunService = game:GetService("RunService") 2 | local ReplicatedStorage = game:GetService("ReplicatedStorage") 3 | local Players = game:GetService("Players") 4 | 5 | local SerdesLayer = require(script.Parent.SerdesLayer) 6 | 7 | type queueSendPacket = { 8 | plrs: string | Player | { Player }, 9 | remote: string, 10 | args: { any }, 11 | replRate: number, 12 | invokeReply: any?, 13 | uuid: string?, 14 | } 15 | type queueReceivePacket = { plr: Player, remote: string, args: { any } } 16 | 17 | local FromCompressed = SerdesLayer.FromCompressed 18 | 19 | local SendQueue: { queueSendPacket } = {} 20 | local ReceiveQueue: { queueReceivePacket } = {} 21 | local BridgeObjects = {} 22 | local RemoteEvent 23 | local Invoke 24 | local InvokeReply 25 | local freeThread 26 | 27 | local function functionPasser(fn, ...) 28 | fn(...) 29 | end 30 | 31 | local function yielder() 32 | while true do 33 | functionPasser(coroutine.yield()) 34 | end 35 | end 36 | 37 | local function maybeSpawn(fn, ...) 38 | if not freeThread then 39 | freeThread = coroutine.create(yielder) 40 | coroutine.resume(freeThread) 41 | end 42 | local acquiredThread = freeThread 43 | freeThread = nil 44 | task.spawn(acquiredThread, fn, ...) 45 | freeThread = acquiredThread 46 | end 47 | 48 | local function spawnConnection(obj, v, callback) 49 | local result 50 | for _, func in obj._inboundMiddleware do 51 | if result then 52 | local potential = { func(v.plr, table.unpack(result)) } 53 | if potential[1] == nil then 54 | return 55 | end 56 | result = potential 57 | else 58 | result = { func(v.plr, table.unpack(v.args)) } 59 | end 60 | end 61 | 62 | result = result or v.args 63 | 64 | callback(v.plr, table.unpack(result)) 65 | end 66 | 67 | local function spawnConnectionWithNil(obj, v, callback, argCount) 68 | local result 69 | for _, func in obj._inboundMiddleware do 70 | if result then 71 | local potential = { func(v.plr, table.unpack(result), 1, argCount) } 72 | if potential[1] == nil then 73 | return 74 | end 75 | result = potential 76 | else 77 | result = { func(v.plr, table.unpack(v.args, 1, argCount)) } 78 | end 79 | end 80 | 81 | result = result or v.args 82 | 83 | callback(v.plr, table.unpack(result)) 84 | end 85 | 86 | local function spawnConnectionWithoutMiddleware(callback, v) 87 | callback(v.plr, table.unpack(v.args)) 88 | end 89 | 90 | local function spawnConnectionWithoutMiddlewareWithNil(callback, v, argCount) 91 | callback(v.plr, table.unpack(v.args, 1, argCount)) 92 | end 93 | 94 | local function deepCopy(tbl): originalType 95 | local result: originalType = {} 96 | for key: number, v in tbl do 97 | if typeof(v) == "table" then 98 | result[key] = deepCopy(v) 99 | elseif typeof(v) == "userdata" then 100 | warn("warning: copying userdata? you should never see this.") 101 | result[key] = v 102 | else 103 | result[key] = v 104 | end 105 | end 106 | return result 107 | end 108 | 109 | --[=[ 110 | @class ServerBridge 111 | 112 | The general method of communicating from the server to the client. 113 | ]=] 114 | local ServerBridge = {} 115 | ServerBridge.__index = ServerBridge 116 | 117 | --[=[ 118 | Starts the internal processes for ServerBridge. 119 | 120 | @ignore 121 | ]=] 122 | function ServerBridge._start(): nil 123 | RemoteEvent = Instance.new("RemoteEvent") 124 | RemoteEvent.Name = "RemoteEvent" 125 | RemoteEvent.Parent = ReplicatedStorage 126 | 127 | Invoke = SerdesLayer.CreateIdentifier("Invoke") 128 | InvokeReply = SerdesLayer.CreateIdentifier("InvokeReply") 129 | 130 | local replTicks = {} 131 | 132 | RunService.Heartbeat:Connect(function() 133 | debug.profilebegin("ServerBridge") 134 | local currentTime = os.clock() 135 | 136 | --[[if (time() - lastClear) > 60 then 137 | lastClear = time() 138 | 139 | for _, v in BridgeObjects do 140 | v._rateInThisMinute = 0 141 | end 142 | end]] 143 | 144 | for _, v in ReceiveQueue do 145 | local args = v.args 146 | for i = 1, #v.args do 147 | if v.args[i] == SerdesLayer.NilIdentifier then 148 | v.args[i] = nil 149 | end 150 | end 151 | 152 | local obj = BridgeObjects[FromCompressed(v.remote)] 153 | if obj == nil then 154 | continue 155 | end 156 | 157 | local allowsNil = obj._allowsNil 158 | 159 | if v.args[1] == Invoke then 160 | if obj._onInvoke ~= nil then 161 | task.spawn(function() 162 | local uuid = args[2] 163 | 164 | table.remove(args, 1) 165 | table.remove(args, 1) -- Arg 2 becomes arg1 after arg1 is removed. 166 | table.insert(SendQueue, { 167 | plrs = v.plr, 168 | remote = obj._id, 169 | uuid = uuid, 170 | invokeReply = true, 171 | replRate = 60, 172 | args = { obj._onInvoke(v.plr, unpack(v.args)) }, 173 | }) 174 | end) 175 | else 176 | -- onInvoke is not set, send an error to the client 177 | local uuid = args[2] 178 | 179 | table.remove(args, 1) 180 | table.remove(args, 1) -- Arg 2 becomes arg1 after arg1 is removed. 181 | table.insert(SendQueue, { 182 | plrs = v.plr, 183 | remote = obj._id, 184 | uuid = uuid, 185 | invokeReply = true, 186 | replRate = 60, 187 | args = { "err", "onInvoke has not yet been registered on the server for " .. obj._name }, 188 | }) 189 | end 190 | continue 191 | end 192 | 193 | if #obj._inboundMiddleware == 0 then 194 | if allowsNil then 195 | for _, callback in obj._connections do 196 | maybeSpawn(spawnConnection, obj, v, callback) 197 | end 198 | else 199 | for _, callback in obj._connections do 200 | maybeSpawn(spawnConnectionWithNil, obj, v, callback) 201 | end 202 | end 203 | else 204 | if allowsNil then 205 | for _, callback in obj._connections do 206 | spawnConnectionWithoutMiddleware(callback, v) 207 | end 208 | else 209 | for _, callback in obj._connections do 210 | spawnConnectionWithoutMiddlewareWithNil(callback, v) 211 | end 212 | end 213 | end 214 | end 215 | 216 | table.clear(ReceiveQueue) 217 | 218 | local toSendAll = {} 219 | local toSendPlayers = {} 220 | local remainingQueue = {} 221 | local passingReplRates = {} 222 | 223 | for i, v in remainingQueue do 224 | if (currentTime - replTicks[v.replRate]) <= (1 / v.replRate - 0.003) then 225 | table.insert(SendQueue, v) 226 | continue 227 | else 228 | table.remove(remainingQueue, i) 229 | end 230 | end 231 | 232 | for _, v: queueSendPacket in SendQueue do 233 | if replTicks[v.replRate] then 234 | -- subtract 0.003 to make sure we don't accidentally skip any frames due to rounding errors 235 | if (currentTime - replTicks[v.replRate]) <= (1 / v.replRate - 0.003) then 236 | passingReplRates[v.replRate] = true 237 | if not passingReplRates[v.replRate] then 238 | table.insert(remainingQueue, v) 239 | continue 240 | end 241 | end 242 | end 243 | 244 | for i = 1, #v.args do 245 | if v.args[i] == nil then 246 | v.args[i] = SerdesLayer.NilIdentifier 247 | end 248 | end 249 | 250 | if not v.invokeReply then 251 | local tbl = { v.remote } 252 | local bridgeObj = BridgeObjects[SerdesLayer.FromCompressed(v.remote)] 253 | 254 | if not (#bridgeObj._outboundMiddleware == 0) then 255 | local result 256 | for _, func in bridgeObj._outboundMiddleware do 257 | if result then 258 | local potential = { func(table.unpack(result)) } 259 | if #potential == 0 then 260 | continue 261 | end 262 | result = potential 263 | else 264 | result = { func(table.unpack(v.args)) } 265 | end 266 | end 267 | 268 | if result == nil then 269 | result = v.args 270 | end 271 | 272 | for _, k in result do 273 | table.insert(tbl, k) 274 | end 275 | else 276 | for _, k in v.args do 277 | table.insert(tbl, k) 278 | end 279 | end 280 | 281 | if v.plrs == "all" then 282 | table.insert(toSendAll, tbl) 283 | elseif typeof(v.plrs) == "table" then 284 | for _, l in v.plrs do 285 | if toSendPlayers[l] == nil then 286 | toSendPlayers[l] = {} 287 | end 288 | 289 | table.insert(toSendPlayers[l], tbl) 290 | end 291 | else 292 | if toSendPlayers[v.plrs] == nil then 293 | toSendPlayers[v.plrs] = { tbl } 294 | else 295 | table.insert(toSendPlayers[v.plrs], tbl) 296 | end 297 | end 298 | elseif v.invokeReply then 299 | if toSendPlayers[v.plrs] == nil then 300 | toSendPlayers[v.plrs] = {} 301 | end 302 | 303 | local tbl = { v.remote, InvokeReply, v.uuid } 304 | 305 | for _, k in v.args do 306 | table.insert(tbl, k) 307 | end 308 | 309 | table.insert(toSendAll, tbl) 310 | end 311 | end 312 | 313 | if #toSendAll ~= 0 then 314 | RemoteEvent:FireAllClients(toSendAll) 315 | end 316 | for l, k in toSendPlayers do 317 | RemoteEvent:FireClient(l, k) 318 | end 319 | 320 | table.clear(SendQueue) 321 | 322 | for key, _ in passingReplRates do 323 | passingReplRates[key] = false 324 | end 325 | 326 | debug.profileend() 327 | end) 328 | 329 | RemoteEvent.OnServerEvent:Connect(function(plr, tbl) 330 | for _, v in tbl do 331 | local args = v 332 | local remote = args[1] 333 | table.remove(args, 1) 334 | local toInsert = { 335 | remote = remote, 336 | plr = plr, 337 | args = args, 338 | } 339 | table.insert(ReceiveQueue, toInsert) 340 | end 341 | end) 342 | 343 | return nil 344 | end 345 | 346 | function ServerBridge._returnQueue() 347 | return SendQueue, ReceiveQueue 348 | end 349 | 350 | --[[function ServerBridge._log(duration) 351 | if activeLogger ~= nil then 352 | return warn("[BridgeNet] active logger already exists") 353 | else 354 | local returnValue = {} 355 | activeLogger = {} 356 | task.delay(duration, function() 357 | for _, v in activeLogger do 358 | table.insert(returnValue, { 359 | Name = v.Name, 360 | }) 361 | end 362 | activeLogger = nil 363 | end) 364 | end 365 | end]] 366 | 367 | function ServerBridge._destroy() 368 | RemoteEvent:Destroy() 369 | end 370 | 371 | function ServerBridge.new(remoteName: string) 372 | assert(type(remoteName) == "string", "[BridgeNet] Remote name must be a string") 373 | 374 | local found = ServerBridge.from(remoteName) 375 | if found ~= nil then 376 | return found 377 | end 378 | 379 | local self = setmetatable({}, ServerBridge) 380 | 381 | self._name = remoteName 382 | 383 | self._onInvoke = nil 384 | self._connections = {} 385 | 386 | self._replRate = 60 387 | 388 | self._rateLimit = nil 389 | self._rateHandler = nil 390 | self._rateInThisMinute = { 391 | num = 0, 392 | min = 0, 393 | } 394 | 395 | self._id = SerdesLayer.CreateIdentifier(remoteName) 396 | 397 | self._outboundMiddleware = {} 398 | self._inboundMiddleware = {} 399 | 400 | BridgeObjects[self._name] = self 401 | return self 402 | end 403 | 404 | function ServerBridge.from(remoteName: string) 405 | return BridgeObjects[remoteName] 406 | end 407 | 408 | function ServerBridge.waitForBridge(remoteName: string) 409 | while true do 410 | local bridge = BridgeObjects[remoteName] 411 | if bridge then 412 | return bridge 413 | end 414 | task.wait() 415 | end 416 | end 417 | 418 | --[=[ 419 | Sends data to a specific player. 420 | 421 | ```lua 422 | local Bridge = BridgeNet.CreateBridge("Remote") 423 | Bridge:FireTo(Players.Someone, "Hello", "World!") 424 | ``` 425 | 426 | @param plr Player 427 | @param ... ...any 428 | @return nil 429 | ]=] 430 | function ServerBridge:FireTo(plr: Player, ...: any) 431 | local args: { any } = { ... } 432 | local toSend: queueSendPacket = { 433 | plrs = plr, 434 | remote = self._id, 435 | args = args, 436 | replRate = self._replRate, 437 | } 438 | table.insert(SendQueue, toSend) 439 | end 440 | 441 | --[=[ 442 | Set the handler for when the server is invoked. By default, this is nil. The client will hang forever as of writing this right now. 443 | 444 | ```lua 445 | local Bridge = BridgeNet.CreateBridge("Remote") 446 | 447 | local data = Bridge:OnInvoke(function(data) 448 | if data == "whats 2+2?" then 449 | return "4" 450 | end 451 | end) 452 | ``` 453 | 454 | @param callback (...any) -> nil 455 | @return Promise 456 | ]=] 457 | function ServerBridge:OnInvoke(callback: (...any) -> nil) 458 | local function wrappedCallback(...) 459 | local success, args = pcall(function(...) 460 | return table.pack(callback(...)) 461 | end, ...) 462 | 463 | if success == true then 464 | return table.unpack(args) 465 | else 466 | return "err", args 467 | end 468 | end 469 | 470 | self._onInvoke = wrappedCallback --wrappedCallback 471 | end 472 | 473 | --[=[ 474 | Sends data to every player except for one. 475 | 476 | ```lua 477 | local Bridge = BridgeNet.CreateBridge("Remote") 478 | Bridge:FireToAllExcept(Players.Someone, "Hello", "World!") 479 | Bridge:FireToAllExcept({Players.A, Players.B}, "Not to A or B, but to C.") 480 | ``` 481 | 482 | @param blacklistedPlrs Player | {Player} 483 | @param ... ...any 484 | @return nil 485 | ]=] 486 | function ServerBridge:FireToAllExcept(blacklistedPlrs: Player | { Player }, ...: any): { Player } 487 | local toSend = {} 488 | for _, v: Player in Players:GetPlayers() do 489 | if typeof(blacklistedPlrs) == "table" then 490 | if table.find(blacklistedPlrs, v) then 491 | continue 492 | end 493 | else 494 | if blacklistedPlrs == v then 495 | continue 496 | end 497 | end 498 | table.insert(toSend, v) 499 | end 500 | 501 | local toSendPacket: queueSendPacket = { 502 | plrs = toSend, 503 | remote = self._id, 504 | args = { ... }, 505 | replRate = self._replRate, 506 | } 507 | table.insert(SendQueue, toSendPacket) 508 | 509 | return toSend :: { Player } 510 | end 511 | 512 | --[=[ 513 | Sends data to every single player within the range except certain blacklisted players. Returns the players affected, for usage later. 514 | 515 | ```lua 516 | local Bridge = BridgeNet.CreateBridge("Remote") 517 | local PlayersSent = Bridge:FireToAllInRangeExcept( 518 | Players.Someone, 519 | Vector3.new(50, 50, 50), 520 | 10, 521 | "Hello", 522 | "World!" 523 | ) 524 | 525 | task.wait(5) 526 | 527 | Bridge:FireToMultiple(PlayersSent, "Time for an update!") 528 | ``` 529 | 530 | @param blacklistedPlrs Player | {Player} 531 | @param point Vector3 532 | @param range number 533 | @param ... ...any 534 | @return {Player} 535 | ]=] 536 | function ServerBridge:FireAllInRangeExcept( 537 | blacklistedPlrs: Player | { Player }, 538 | point: Vector3, 539 | range: number, 540 | ...: any 541 | ) 542 | local toSend = {} 543 | for _, v: Player in Players:GetPlayers() do 544 | if v:DistanceFromCharacter(point) <= range then 545 | if typeof(blacklistedPlrs) == "table" then 546 | if table.find(blacklistedPlrs, v) then 547 | continue 548 | end 549 | else 550 | if blacklistedPlrs == v then 551 | continue 552 | end 553 | end 554 | table.insert(toSend, v) 555 | end 556 | end 557 | 558 | local toSendPacket: queueSendPacket = { 559 | plrs = toSend, 560 | remote = self._id, 561 | args = { ... }, 562 | replRate = self._replRate, 563 | } 564 | table.insert(SendQueue, toSendPacket) 565 | 566 | return toSend 567 | end 568 | 569 | --[=[ 570 | Sends data to every single player within the range. Returns the players affected, for usage later. 571 | 572 | ```lua 573 | local Bridge = BridgeNet.CreateBridge("Remote") 574 | local PlayersSent = Bridge:FireAllInRange( 575 | Vector3.new(50, 50, 50), 576 | 10, 577 | "Hello", 578 | "World!" 579 | ) 580 | 581 | task.wait(5) 582 | 583 | Bridge:FireToMultiple(PlayersSent, "Time for an update!") 584 | ``` 585 | 586 | @param point Vector3 587 | @param range number 588 | @param ... ...any 589 | @return {Player} 590 | ]=] 591 | function ServerBridge:FireAllInRange(point: Vector3, range: number, ...: any): { Player } 592 | assert(typeof(point) == "Vector3", "[BridgeNet] point must be a Vector3") 593 | assert(typeof(range) == "number", "[BridgeNet] range must be a number") 594 | 595 | local toSend = {} 596 | for _, v: Player in Players:GetPlayers() do 597 | if v:DistanceFromCharacter(point) <= range then 598 | table.insert(toSend, v) 599 | end 600 | end 601 | 602 | local toSendPacket: queueSendPacket = { 603 | plrs = toSend, 604 | remote = self._id, 605 | args = { ... }, 606 | replRate = self._replRate, 607 | } 608 | table.insert(SendQueue, toSendPacket) 609 | 610 | return toSend :: { Player } 611 | end 612 | 613 | --[=[ 614 | Sends data to every single player, with no exceptions. 615 | 616 | ```lua 617 | local Bridge = BridgeNet.CreateBridge("Remote") 618 | Bridge:FireAll("Hello, world!") 619 | ``` 620 | 621 | @param ... ...any 622 | @return nil 623 | ]=] 624 | function ServerBridge:FireAll(...: any): nil 625 | local args: { any } = { ... } 626 | local toSend: queueSendPacket = { 627 | plrs = "all", 628 | remote = self._id, 629 | args = args, 630 | replRate = self._replRate, 631 | } 632 | table.insert(SendQueue, toSend) 633 | return nil 634 | end 635 | 636 | --[=[ 637 | Sends data to multiple players. 638 | 639 | ```lua 640 | local Bridge = BridgeNet.CreateBridge("Remote") 641 | Bridge:FireToMultiple({Players.A, Players.B}, "Hi!", "Hello.") 642 | ``` 643 | 644 | @param plrs {Player} 645 | @param ... ...any 646 | @return nil 647 | ]=] 648 | function ServerBridge:FireToMultiple(plrs: { Player }, ...: any): nil 649 | assert(type(plrs) == "table", "[BridgeNet] First argument must be a table!") 650 | 651 | local args: { any } = { ... } 652 | local toSend: queueSendPacket = { 653 | plrs = plrs, 654 | remote = self._id, 655 | args = args, 656 | replRate = self._replRate, 657 | } 658 | table.insert(SendQueue, toSend) 659 | return nil 660 | end 661 | 662 | --[=[ 663 | Sets the Bridge's inbound middleware functions. Any function which returns nil will drop the remote request completely. Overrides existing middleware. 664 | 665 | Allows you to change arguments or drop remote calls. 666 | 667 | A more comprehensive guide on middleware will be coming soon. 668 | ```lua 669 | Object:SetInboundMiddleware({ 670 | function(plr, ...) -- Called first 671 | return ... 672 | end, 673 | function(plr, ...) -- Called second 674 | print("1") 675 | return ... 676 | end, 677 | function(plr, ...) -- Called third 678 | print("2") 679 | return ... 680 | end, 681 | }) 682 | ``` 683 | 684 | @param middlewareTable { (...any) -> nil } 685 | @return nil 686 | ]=] 687 | function ServerBridge:SetInboundMiddleware(middlewareTable: { (...any) -> nil }) 688 | assert(typeof(middlewareTable) == "table", "[BridgeNet] middlewareTable must be a table") 689 | self._inboundMiddleware = middlewareTable or {} 690 | end 691 | 692 | --[=[ 693 | Sets the Bridge's outbound middleware functions. Any function which returns nil will drop the sequence completely. Overrides existing middleware. 694 | 695 | A more comprehensive guide on middleware will be coming soon. 696 | ```lua 697 | Object:SetOutboundMiddleware({ 698 | function(plr, ...) -- Called first 699 | return ... 700 | end, 701 | function(plr, ...) -- Called second 702 | print("1") 703 | return ... 704 | end, 705 | function(plr, ...) -- Called third 706 | print("2") 707 | return ... 708 | end, 709 | }) 710 | ``` 711 | 712 | @param middlewareTable { (...any) -> nil } 713 | @return nil 714 | ]=] 715 | function ServerBridge:SetOutboundMiddleware(middlewareTable: { (...any) -> nil }) 716 | assert(typeof(middlewareTable) == "table", "[BridgeNet] middlewareTable must be a table") 717 | self._outboundMiddleware = middlewareTable or {} 718 | end 719 | 720 | --[=[ 721 | Creates a connection, when fired it will disconnect. 722 | 723 | ```lua 724 | local Bridge = BridgeNet.CreateBridge("ConstantlyFiringText") 725 | 726 | Bridge:Connect(function(text) 727 | print(text) -- Fires multiple times 728 | end) 729 | 730 | Bridge:Once(function(text) 731 | print(text) -- Fires once 732 | end) 733 | ``` 734 | 735 | @param func function 736 | @return nil 737 | ]=] 738 | function ServerBridge:Once(func: (...any) -> nil) 739 | local connection 740 | connection = self:Connect(function(...) 741 | connection:Disconnect() 742 | func(...) 743 | end) 744 | end 745 | 746 | --[=[ 747 | Creates a connection. 748 | 749 | ```lua 750 | local Bridge = BridgeNet.CreateBridge("Remote") 751 | Bridge:Connect(function(plr, data) 752 | print(plr .. " has sent " .. data) 753 | end) 754 | ``` 755 | 756 | @param func (plr: Player, ...any) -> nil 757 | @return Connection 758 | ]=] 759 | function ServerBridge:Connect(func: (...any) -> nil) 760 | assert(type(func) == "function", "[BridgeNet] attempt to connect non-function to a Bridge") 761 | local connectionUUID = SerdesLayer.CreateUUID() 762 | self._connections[connectionUUID] = func 763 | 764 | local connection = {} 765 | 766 | function connection.Disconnect() 767 | self._connections[connectionUUID] = nil 768 | connectionUUID = nil 769 | end 770 | 771 | return connection 772 | end 773 | 774 | --[[ 775 | Gets the ServerBridge's name. 776 | 777 | ```lua 778 | local Bridge = BridgeNet.CreateBridge("Remote") 779 | 780 | print(Bridge:GetName()) -- Prints "Remote" 781 | ``` 782 | 783 | @return string 784 | ]] 785 | function ServerBridge:GetName() 786 | return self._name 787 | end 788 | 789 | --[=[ 790 | Sets the rate of which the Bridge sends and receives data. 791 | 792 | @param rate number 793 | @return nil 794 | ]=] 795 | function ServerBridge:SetReplicationRate(rate: number) 796 | self._replRate = rate 797 | end 798 | 799 | --[=[ 800 | Destroys the identifier, and deletes the object reference. 801 | 802 | ```lua 803 | local Bridge = BridgeNet.CreateBridge("Remote") 804 | Bridge:Destroy() 805 | 806 | Bridge:FireTo(Players.A) -- Errors, the object is deleted. 807 | ``` 808 | 809 | @return nil 810 | ]=] 811 | function ServerBridge:Destroy() 812 | BridgeObjects[self._name] = nil 813 | SerdesLayer.DestroyIdentifier(self.Name) 814 | for k, v in self do 815 | if v.Destroy ~= nil then 816 | v:Destroy() 817 | else 818 | self[k] = nil 819 | end 820 | end 821 | setmetatable(self, nil) 822 | end 823 | 824 | export type ServerObject = typeof(ServerBridge.new("")) 825 | 826 | return ServerBridge 827 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | type QueueSendPacket = { 4 | plrs: string | Player | Array; 5 | remote: string; 6 | args: Array; 7 | replRate: number; 8 | invokeReply?: any; 9 | uuid?: string; 10 | }; 11 | 12 | type QueueReceivePacket = { 13 | plr: Player; 14 | remote: string; 15 | args: Array; 16 | }; 17 | 18 | type SendQueue = Array; 19 | type ReceiveQueue = Array; 20 | 21 | interface Connection { 22 | Disconnect(): void; 23 | } 24 | 25 | interface Bridge { 26 | GetName(): string; 27 | 28 | SetReplicationRate(rate: string): void; 29 | 30 | Connect(callback: (...args: Array) => void): Connection; 31 | Once(callback: (...args: Array) => void): void; 32 | 33 | Destroy(): void; 34 | } 35 | 36 | export interface ServerBridge extends Bridge { 37 | FireTo(plr: Player, ...args: any): void; 38 | FireToMultiple(players: Array, ...args: any): void; 39 | FireToAllExcept( 40 | blacklist: Player | Array, 41 | ...args: any 42 | ): Array; 43 | FireAll(...args: any): void; 44 | } 45 | 46 | export interface ClientBridge extends Bridge { 47 | Fire(...args: any): void; 48 | 49 | SetNilAllowed(allowed: boolean): void; 50 | } 51 | 52 | export namespace BridgeNet { 53 | export function CreateBridge(name: string): ServerBridge | ClientBridge; 54 | export function Identifiers(ids: Array): { 55 | [index in keyof typeof ids]: string; 56 | }; 57 | 58 | export function GetQueue(): LuaTuple<[SendQueue, ReceiveQueue]>; 59 | 60 | // SerdesLayer 61 | export function CreateIdentifier(id: string): string; 62 | export function DestroyIdentifier(id: string): void; 63 | export function CreateUUID(): string; 64 | export function PackUUID(uuid: string): string; 65 | export function UnpackUUID(packed: string): string; 66 | 67 | export function DictionaryToTable(dict: { 68 | [index: string]: T; 69 | }): Array; 70 | } 71 | -------------------------------------------------------------------------------- /src/init.lua: -------------------------------------------------------------------------------- 1 | --!strict 2 | local RunService = game:GetService("RunService") 3 | 4 | local SerdesLayer = require(script.SerdesLayer) 5 | local ServerBridge = require(script.ServerBridge) 6 | local ClientBridge = require(script.ClientBridge) 7 | local CreateBridgeTree = require(script.CreateBridgeTree) 8 | local Bridge = require(script.Bridge) 9 | 10 | local isServer = RunService:IsServer() 11 | 12 | --[=[ 13 | @class BridgeNet 14 | 15 | The interface for the library. 16 | ]=] 17 | 18 | --[=[ 19 | @function GetQueue 20 | @within BridgeNet 21 | 22 | Returns the internal queue BridgeNet uses. Not intended for production purposes- use this to debug potential issues with the module, or your own code. 23 | 24 | @return SendQueue, ReceiveQueue 25 | ]=] 26 | 27 | --[=[ 28 | @function Identifiers 29 | @within BridgeNet 30 | 31 | Returns a dictionary of identifiers based off of the passed array of strings. 32 | ```lua 33 | local Stuff = BridgeNet.Identifiers({ 34 | "Foo", 35 | "Bar", 36 | }) 37 | 38 | print(Stuff.Foo) 39 | print(Stuff.Bar) 40 | ``` 41 | 42 | @return { [string]: string } 43 | ]=] 44 | 45 | --[=[ 46 | @function CreateBridgeTree 47 | @within BridgeNet 48 | 49 | This function creates a series of Bridges with a preset configuration. This function supports namespaces- it takes either a BridgeNet.Bridge() function, or a dictionary. 50 | ```lua 51 | local MyBridgeTree = BridgeNet.CreateBridgeTree({ 52 | BridgeNameHere = BridgeNet.Bridge() 53 | NamespaceHere = { 54 | BridgeHere = BridgeNet.Bridge({ 55 | ReplicationRate = 20 56 | }) 57 | } 58 | }) 59 | ``` 60 | This allows you to create your Bridge objects in one centralized place, as it is runnable on both the client and server. This means that one module can contain all of your 61 | Bridge objects- which makes it much easier to access. Example usage: 62 | ```lua 63 | -- shared/Bridges.luau 64 | local MyBridgeTree = BridgeNet.CreateBridgeTree({ 65 | PrintOnServer = BridgeNet.Bridge() 66 | }) 67 | 68 | return MyBridgeTree 69 | 70 | -- client 71 | local Bridges = require(path.to.Bridges) 72 | 73 | Bridges.PrintOnServer:Fire("Hello, world!") 74 | 75 | -- server 76 | local Bridges = require(path.to.Bridges) 77 | 78 | Bridges.PrintOnServer:Connect(function(player, text) 79 | print("Player " .. player.Name .. " has said " .. text) -- prints "Player SomeUsername has said Hello, world! 80 | end) 81 | ``` 82 | 83 | @param BridgeTree { [string]: thisType | BridgeConfig } 84 | @return { [string]: thisType | Bridge } 85 | ]=] 86 | 87 | --[=[ 88 | @function Bridge 89 | @within BridgeNet 90 | 91 | This function is only intended for usage within BridgeNet.CreateBridgeTree(). You are not supposed to use this anywhere else. 92 | This function lets you assign middleware, a replication rate, and in the future certain things like logging and typechecking. 93 | 94 | ```lua 95 | local MyBridgeTree = BridgeNet.CreateBridgeTree({ 96 | Print = BridgeNet.Bridge({ 97 | ReplicationRate = 20, -- twenty times per second 98 | Server = { 99 | OutboundMiddleware = { 100 | function(...) 101 | print("Telling the client to print...") 102 | return ... 103 | end, 104 | }, 105 | InboundMiddleware = { 106 | function(plr, ...) 107 | print("Player " .. plr.Name .. " has fired PrintOnServer") 108 | return ... 109 | end, 110 | }, 111 | }, 112 | Client = { 113 | OutboundMiddleware = { 114 | function(...) 115 | print("Telling the server to print...") 116 | return ... 117 | end, 118 | }, 119 | InboundMiddleware = { 120 | function(plr, ...) 121 | print("The server has told us to print") 122 | return ... 123 | end, 124 | }, 125 | } 126 | }) 127 | }) 128 | ``` 129 | 130 | @return BridgeConfig 131 | ]=] 132 | 133 | --[=[ 134 | @function CreateBridge 135 | @within BridgeNet 136 | 137 | Creates a ServerBridge or a ClientBridge depending on if it's the server or client calling. If a Bridge of that name already exists, it'll return that Bridge object. 138 | This can be used to fetch bridges, but .WaitForBridge is recommended. 139 | 140 | ```lua 141 | local Bridge = BridgeNet.CreateBridge("Remote") 142 | ``` 143 | 144 | @param remoteName string 145 | @return ServerBridge | ClientBridge 146 | ]=] 147 | 148 | export type ServerBridge = ServerBridge.ServerObject 149 | export type ClientBridge = ClientBridge.ClientObject 150 | 151 | export type Bridge = ServerBridge | ClientBridge 152 | 153 | script.Destroying:Connect(function() 154 | SerdesLayer._destroy() 155 | if isServer then 156 | ServerBridge._destroy() 157 | end 158 | end) 159 | 160 | SerdesLayer._start() 161 | if isServer then 162 | ServerBridge._start() 163 | else 164 | ClientBridge._start() 165 | end 166 | 167 | return { 168 | CreateBridgeTree = CreateBridgeTree, 169 | Bridge = Bridge, 170 | 171 | Identifiers = function(tbl: { string }) 172 | local ReturnValue = {} 173 | 174 | for _, v in tbl do 175 | ReturnValue[v] = SerdesLayer.CreateIdentifier(v) 176 | end 177 | 178 | return ReturnValue :: { [string]: string } 179 | end, 180 | 181 | CreateIdentifier = SerdesLayer.CreateIdentifier, 182 | DestroyIdentifier = SerdesLayer.DestroyIdentifier, 183 | 184 | CreateUUID = SerdesLayer.CreateUUID, 185 | PackUUID = SerdesLayer.PackUUID, 186 | UnpackUUID = SerdesLayer.UnpackUUID, 187 | 188 | DictionaryToTable = SerdesLayer.DictionaryToTable, 189 | 190 | --[[LogNetTraffic = function(duration: number) 191 | if isServer then 192 | return ServerBridge._log(duration) 193 | else 194 | return ClientBridge._log(duration) 195 | end 196 | end,]] 197 | 198 | GetQueue = function() 199 | if isServer then 200 | local send, receive = ServerBridge._returnQueue() 201 | return send, receive 202 | else 203 | local send, receive = ClientBridge._returnQueue() 204 | return send, receive 205 | end 206 | end, 207 | 208 | CreateBridge = function(str) 209 | if isServer then 210 | return ServerBridge.new(str) 211 | else 212 | return ClientBridge.new(str) 213 | end 214 | end, 215 | } 216 | -------------------------------------------------------------------------------- /test.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bridgenet-test", 3 | "tree": { 4 | "$className": "DataModel", 5 | "ReplicatedStorage": { 6 | "$className": "ReplicatedStorage", 7 | "Packages": { 8 | "$className": "Folder", 9 | "BridgeNet": { 10 | "$path": "default.project.json" 11 | }, 12 | "Tester" : { 13 | "$path" : "tests/framework" 14 | } 15 | } 16 | }, 17 | "ServerScriptService": { 18 | "ServerTest": { 19 | "$path": "tests/server" 20 | } 21 | }, 22 | "StarterPlayer": { 23 | "StarterPlayerScripts": { 24 | "ClientTest": { 25 | "$path": "tests/client" 26 | } 27 | }, 28 | "$className": "StarterPlayer" 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /tests/client/ClientTest2.client.lua: -------------------------------------------------------------------------------- 1 | local ReplicatedStorage = game:GetService("ReplicatedStorage") 2 | 3 | local BridgeNet = require(ReplicatedStorage.Packages.BridgeNet) 4 | 5 | local STRESS_TEST = true 6 | local BRIDGENET_OR_ROBLOX = "roblox" 7 | 8 | if not STRESS_TEST then 9 | local uuid = BridgeNet.CreateUUID() 10 | print(uuid) 11 | local packed = BridgeNet.PackUUID(uuid) 12 | print(packed) 13 | print(BridgeNet.UnpackUUID(packed)) 14 | 15 | local Identifiers = BridgeNet.Identifiers({ 16 | "Test", 17 | "Funny", 18 | "Haha", 19 | "TestB", 20 | "yes", 21 | }) 22 | print(Identifiers) 23 | 24 | local Bridges = BridgeNet.CreateBridgeTree({ 25 | RemoteA = BridgeNet.Bridge({ 26 | ReplicationRate = 20, 27 | }), 28 | RemoteCategory = { 29 | RemoteB = BridgeNet.Bridge({ 30 | ReplicationRate = 45, 31 | Server = { 32 | OutboundMiddleware = { 33 | function(...) 34 | print("CreateBridgeTree server middleware outgoing") 35 | return ... 36 | end, 37 | }, 38 | InboundMiddleware = { 39 | function(plr, ...) 40 | print("CreateBridgeTree server middleware inbound: " .. plr.Name) 41 | return ... 42 | end, 43 | }, 44 | }, 45 | Client = { 46 | OutboundMiddleware = { 47 | function(...) 48 | print("CreateBridgeTree client middleware outgoing") 49 | return ... 50 | end, 51 | }, 52 | InboundMiddleware = { 53 | function(...) 54 | print("CreateBridgeTree client middleware inbound") 55 | return ... 56 | end, 57 | }, 58 | }, 59 | }), 60 | RemoteC = BridgeNet.Bridge({}), 61 | }, 62 | }) 63 | 64 | if Bridges["RemoteA"] == nil then 65 | error(".Declare error, RemoteA is nil") 66 | end 67 | 68 | if Bridges["RemoteCategory"] == nil then 69 | error("RemoteCategory is nil") 70 | end 71 | 72 | if Bridges.RemoteCategory["RemoteB"] == nil then 73 | error(".Declare error, RemoteB is nil") 74 | end 75 | 76 | if Bridges.RemoteA["_id"] == nil and Bridges.RemoteA["_name"] == nil then 77 | error(".Declare does not return a bridge") 78 | end 79 | 80 | local connection = Bridges.RemoteA:Connect(function(arg1) 81 | print(arg1) 82 | end) 83 | 84 | Bridges.RemoteCategory.RemoteB:Connect(function() end) 85 | 86 | local lastTwentyHz = 0 87 | BridgeNet.ReplicationStep(20, function() 88 | print(os.clock() - lastTwentyHz) 89 | lastTwentyHz = os.clock() 90 | end) 91 | 92 | task.delay(5, function() 93 | connection:Disconnect() 94 | end) 95 | 96 | while true do 97 | Bridges.RemoteCategory.RemoteB:Fire("client to server check") 98 | Bridges.RemoteCategory.RemoteB:InvokeServerAsync("Args working") 99 | task.wait(2) 100 | end 101 | elseif STRESS_TEST then 102 | if BRIDGENET_OR_ROBLOX == "bridgenet" then 103 | local stresser = BridgeNet.CreateBridge("stresser") 104 | 105 | stresser:Connect(function() end) 106 | elseif BRIDGENET_OR_ROBLOX == "roblox" then 107 | local stresser = ReplicatedStorage:WaitForChild("TestEvent") 108 | 109 | stresser.OnClientEvent:Connect(function() end) 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /tests/framework/bootstrapper.lua: -------------------------------------------------------------------------------- 1 | type options = { 2 | context: any, 3 | } 4 | 5 | type set = { T | { T } } 6 | 7 | local function testDirectory(dir: set, options: options) 8 | local dirResults = {} 9 | 10 | for _, child in dir do 11 | if child:IsA("ModuleScript") and child.Name:match("%.spec$") then 12 | local module = require(child) 13 | 14 | dirResults[child.Name] = {} 15 | 16 | local currentTestCase = dirResults[child.Name] 17 | 18 | for caseName, caseFunction in module do 19 | local ok, err = pcall(caseFunction, options.context) 20 | 21 | currentTestCase[caseName] = { ok = ok, err = err } 22 | end 23 | elseif child:IsA("Folder") then 24 | dirResults[child.Name] = testDirectory(child:GetChildren(), options) 25 | end 26 | end 27 | 28 | return dirResults 29 | end 30 | 31 | local function isSuccessful(folder) 32 | local numOfFails = 0 33 | local numOfSuccess = 0 34 | 35 | for _, value in folder do 36 | if value.ok ~= nil then 37 | if value.ok then 38 | numOfSuccess += 1 39 | else 40 | numOfFails += 1 41 | end 42 | else 43 | local wins, fails = isSuccessful(value) 44 | numOfSuccess += wins 45 | numOfFails += fails 46 | end 47 | end 48 | 49 | return numOfSuccess, numOfFails 50 | end 51 | 52 | local function readiyDirectory(dir, spaceStr) 53 | local finalString = "" 54 | 55 | for name, value in dir do 56 | if value.ok ~= nil then 57 | local marker = value.ok and "+" or "-" 58 | finalString ..= "\n" 59 | finalString ..= spaceStr .. string.format("[%s] ", marker) 60 | finalString ..= name 61 | else 62 | local _, fails = isSuccessful(value) 63 | local marker = fails > 0 and "-" or "+" 64 | finalString ..= "\n" 65 | finalString ..= spaceStr .. string.format("[%s] ", marker) 66 | finalString ..= name 67 | finalString ..= readiyDirectory(value, spaceStr .. " ") 68 | end 69 | end 70 | 71 | return finalString 72 | end 73 | 74 | local function readifyDirectoryErrs(dir) 75 | local finalString = "" 76 | 77 | for _, value in dir do 78 | if value.ok ~= nil then 79 | if value.ok == false then 80 | finalString ..= "\n" 81 | finalString ..= value.err 82 | end 83 | elseif typeof(value) == "table" then 84 | finalString ..= readifyDirectoryErrs(value) 85 | end 86 | end 87 | 88 | return finalString 89 | end 90 | 91 | local bootstrapper = {} 92 | 93 | function bootstrapper:start(configuration: { directories: {}, options: options }) 94 | local testResults = {} 95 | 96 | for _, directory: Folder in configuration.directories do 97 | testResults[directory.Name] = testDirectory(directory:GetChildren(), configuration.options) 98 | end 99 | 100 | local finalString = "\nTesting results" 101 | 102 | for dirName, dirValue in testResults do 103 | finalString ..= "\n " 104 | finalString ..= "[!] " .. dirName 105 | finalString ..= readiyDirectory(dirValue, " ") 106 | end 107 | 108 | print(finalString) 109 | 110 | local errMessage = "" 111 | 112 | for _, dirValue in testResults do 113 | errMessage ..= readifyDirectoryErrs(dirValue) 114 | end 115 | 116 | if #errMessage > 2 then 117 | error(errMessage) 118 | end 119 | end 120 | return bootstrapper 121 | -------------------------------------------------------------------------------- /tests/framework/expect.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | We use a "cases" table for storing assertion method so that 3 | we can add more methods and alias later on without being in elseif's hell 4 | ]] 5 | 6 | local cases = { 7 | equal = function(self) 8 | return function(expectedThing: any) 9 | local never = rawget(self, "_never") 10 | local value = rawget(self, "_value") 11 | 12 | local result = value == expectedThing 13 | 14 | if never then 15 | result = not result 16 | end 17 | 18 | assert( 19 | result, 20 | string.format( 21 | "Expected value to %sbe %s, got %s", 22 | never and " never" or "", 23 | tostring(expectedThing), 24 | tostring(value) 25 | ) 26 | ) 27 | end 28 | end, 29 | 30 | never = function(self) 31 | rawset(self, "_never", not rawget(self, "_never")) 32 | return self 33 | end, 34 | 35 | exist = function(self) 36 | return function() 37 | local never = rawget(self, "_never") 38 | local value = rawget(self, "_value") 39 | 40 | local result = value ~= nil 41 | 42 | if never then 43 | result = not result 44 | end 45 | 46 | assert( 47 | result, 48 | string.format("Expected value to%s exist, got %s", never and " never" or "", tostring(value)) 49 | ) 50 | end 51 | end, 52 | 53 | throw = function(self) 54 | return function() 55 | local never = rawget(self, "_never") 56 | local value = rawget(self, "_value") 57 | 58 | local result, _ = pcall(value) 59 | result = not result 60 | 61 | if never then 62 | result = not result 63 | end 64 | 65 | assert(result, string.format("Expected value to %serror", never and " never" or "")) 66 | end 67 | end, 68 | 69 | match = function(self) 70 | return function(expectedPattern: string) 71 | local never = rawget(self, "_never") 72 | local value = rawget(self, "_value") 73 | 74 | local result = string.match(value, expectedPattern) ~= nil 75 | 76 | if never then 77 | result = not result 78 | end 79 | 80 | assert( 81 | result, 82 | string.format( 83 | "Expected %s to %smatch '%s'", 84 | tostring(value), 85 | never and " never" or "", 86 | tostring(expectedPattern) 87 | ) 88 | ) 89 | end 90 | end, 91 | 92 | near = function(self) 93 | return function(expectToBeNear: number) 94 | local never = rawget(self, "_never") 95 | local value = rawget(self, "_value") 96 | 97 | local result = math.round(value) == math.round(expectToBeNear) 98 | 99 | if never then 100 | result = not result 101 | end 102 | 103 | assert( 104 | result, 105 | string.format( 106 | "Expected %s to %sbe near '%s'", 107 | tostring(value), 108 | never and " never" or "", 109 | tostring(expectToBeNear) 110 | ) 111 | ) 112 | end 113 | end, 114 | a = function(self) 115 | return function(expectedType: string) 116 | local never = rawget(self, "_never") 117 | local value = rawget(self, "_value") 118 | 119 | local result = (typeof(value) == expectedType) or (typeof(value) == "Instance" and value:IsA(expectedType)) 120 | 121 | if never then 122 | result = not result 123 | end 124 | 125 | assert( 126 | result, 127 | string.format( 128 | "Expected %s to%s be %s", 129 | tostring(value), 130 | never and " never" or "", 131 | tostring(expectedType) 132 | ) 133 | ) 134 | end 135 | end, 136 | } 137 | 138 | -- alias 139 | cases.equals = cases.equal 140 | cases.exists = cases.exist 141 | cases.throws = cases.throw 142 | cases.fail = cases.throw 143 | cases.fails = cases.throw 144 | cases.matches = cases.match 145 | 146 | return function(this: any) 147 | local query = setmetatable({ 148 | _value = this, 149 | _never = false, 150 | }, { 151 | __index = function(self, queryName) 152 | local func = cases[queryName] 153 | 154 | if func then 155 | return func(self) 156 | end 157 | 158 | return self 159 | end, 160 | }) 161 | 162 | return query 163 | end 164 | -------------------------------------------------------------------------------- /tests/framework/init.lua: -------------------------------------------------------------------------------- 1 | local bootstrapper = require(script.bootstrapper) 2 | local expect = require(script.expect) 3 | 4 | return { 5 | expect = expect, 6 | bootstrapper = bootstrapper 7 | } -------------------------------------------------------------------------------- /tests/server/ServerTest2.server.lua: -------------------------------------------------------------------------------- 1 | local ReplicatedStorage = game:GetService("ReplicatedStorage") 2 | 3 | local BridgeNet = require(ReplicatedStorage.Packages.BridgeNet) 4 | 5 | local RunService = game:GetService("RunService") 6 | 7 | local STRESS_TEST = true 8 | local BRIDGENET_OR_ROBLOX = "roblox" 9 | 10 | if not STRESS_TEST then 11 | local uuid = BridgeNet.CreateUUID() 12 | print(uuid) 13 | local packed = BridgeNet.PackUUID(uuid) 14 | print(packed) 15 | print(BridgeNet.UnpackUUID(packed)) 16 | 17 | local Identifiers = BridgeNet.Identifiers({ 18 | "Test", 19 | "Funny", 20 | "Haha", 21 | "TestB", 22 | "yes", 23 | }) 24 | print(Identifiers) 25 | 26 | local Bridges = BridgeNet.CreateBridgeTree({ 27 | RemoteA = BridgeNet.Bridge({ 28 | ReplicationRate = 20, 29 | }), 30 | RemoteCategory = { 31 | RemoteAA = BridgeNet.Bridge({ 32 | ReplicationRate = 20, 33 | }), 34 | RemoteB = BridgeNet.Bridge({ 35 | ReplicationRate = 45, 36 | Server = { 37 | OutboundMiddleware = { 38 | function(...) 39 | print("CreateBridgeTree server middleware outgoing") 40 | return ... 41 | end, 42 | }, 43 | InboundMiddleware = { 44 | function(plr, ...) 45 | print("CreateBridgeTree server middleware inbound: " .. plr.Name) 46 | return ... 47 | end, 48 | }, 49 | }, 50 | Client = { 51 | OutboundMiddleware = { 52 | function(...) 53 | print("CreateBridgeTree client middleware outgoing") 54 | return ... 55 | end, 56 | }, 57 | InboundMiddleware = { 58 | function(...) 59 | print("CreateBridgeTree client middleware inbound") 60 | return ... 61 | end, 62 | }, 63 | }, 64 | }), 65 | RemoteC = BridgeNet.Bridge({}), 66 | }, 67 | }) 68 | 69 | if Bridges["RemoteA"] == nil then 70 | error(".Declare error, RemoteA is nil") 71 | end 72 | 73 | if Bridges["RemoteCategory"] == nil then 74 | error("RemoteCategory is nil") 75 | end 76 | 77 | if Bridges.RemoteCategory["RemoteB"] == nil then 78 | error(".Declare error, RemoteB is nil") 79 | end 80 | 81 | if Bridges.RemoteA["_id"] == nil and Bridges.RemoteA["_name"] == nil then 82 | error(".Declare does not return a bridge") 83 | end 84 | 85 | Bridges.RemoteCategory.RemoteB:Connect(function(plr, arg1) 86 | print(plr, arg1) 87 | end) 88 | 89 | Bridges.RemoteCategory.RemoteB:OnInvoke(function(plr, arg2) 90 | print("Invoke working") 91 | print(plr, arg2) 92 | end) 93 | 94 | while task.wait(2) do 95 | Bridges.RemoteA:FireAll("FireAll check") 96 | Bridges.RemoteA:FireTo(game.Players:GetPlayers()[1], "FireTo check") 97 | task.defer(function() 98 | task.wait() 99 | print(BridgeNet.GetQueue()) 100 | end) 101 | Bridges.RemoteCategory.RemoteB:FireTo(game.Players:GetPlayers()[1], "Middleware check") 102 | 103 | Bridges.RemoteCategory.RemoteAA:FireAll("Queueing test") 104 | 105 | Bridges.RemoteA:FireAll("Rapid") 106 | Bridges.RemoteA:FireAll("Succession") 107 | Bridges.RemoteA:FireAll("Check") 108 | end 109 | elseif STRESS_TEST then 110 | if BRIDGENET_OR_ROBLOX == "roblox" then 111 | local RemoteEvent = Instance.new("RemoteEvent") 112 | RemoteEvent.Name = "TestEvent" 113 | RemoteEvent.Parent = ReplicatedStorage 114 | 115 | RunService.Heartbeat:Connect(function() 116 | for _ = 1, 200 do 117 | RemoteEvent:FireAllClients() 118 | end 119 | end) 120 | else 121 | local stresser = BridgeNet.CreateBridge("stresser") 122 | RunService.Heartbeat:Connect(function() 123 | for _ = 1, 200 do 124 | stresser:FireAll() 125 | end 126 | end) 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "module": "commonjs", 5 | "strict": true, 6 | "noFallthroughCasesInSwitch": true, 7 | "strictNullChecks": true, 8 | "esModuleInterop": true, 9 | "removeComments": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "composite": true, 12 | "noLib": true, 13 | "sourceMap": true, 14 | "outDir": "out", 15 | "baseUrl": "src", 16 | "noImplicitAny": false, 17 | "moduleResolution": "Node", 18 | "allowSyntheticDefaultImports": true, 19 | "typeRoots": ["node_modules/@rbxts"], 20 | "paths": { 21 | "*": ["*"] 22 | }, 23 | "plugins": [ 24 | { 25 | "transform": "typescript-transform-paths", 26 | "exclude": ["**/node_modules/**"] 27 | }, 28 | { 29 | "transform": "typescript-transform-paths", 30 | "exclude": ["**/node_modules/**"], 31 | "afterDeclarations": true 32 | } 33 | ], 34 | "rootDir": "src", 35 | "incremental": true, 36 | "tsBuildInfoFile": "out/tsconfig.tsbuildinfo" 37 | } 38 | } -------------------------------------------------------------------------------- /wally.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Wally. 2 | # It is not intended for manual editing. 3 | registry = "test" 4 | 5 | [[package]] 6 | name = "evaera/promise" 7 | version = "4.0.0" 8 | dependencies = [] 9 | 10 | [[package]] 11 | name = "ffrostflame/bridgenet" 12 | version = "1.9.9" 13 | dependencies = [["GoodSignal", "stravant/goodsignal@0.2.1"], ["Promise", "evaera/promise@4.0.0"]] 14 | 15 | [[package]] 16 | name = "stravant/goodsignal" 17 | version = "0.2.1" 18 | dependencies = [] 19 | -------------------------------------------------------------------------------- /wally.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ffrostflame/bridgenet" 3 | version = "2.0.0-rc4" 4 | registry = "https://github.com/UpliftGames/wally-index" 5 | realm = "shared" 6 | description = "A feature-packed networking library oriented towards optimization and ease of usage." 7 | license = "MIT" --------------------------------------------------------------------------------