├── .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"
--------------------------------------------------------------------------------