├── .darklua.json
├── .gitattributes
├── .gitignore
├── .luaurc
├── .moonwave
└── static
│ ├── favicon.png
│ └── logo.png
├── .vscode
├── extensions.json
├── settings.json
└── vectorTypes.d.luau
├── CHANGELOG.md
├── CONTRIBUTORS.md
├── LICENSE
├── README.md
├── build.project.json
├── docs
├── Installation.md
├── Technical Details
│ ├── Identifiers.md
│ └── _category_.json
├── Tutorials
│ ├── Best Practices.md
│ ├── Getting Started.md
│ ├── Logging.md
│ ├── Using Identifiers.md
│ └── _category_.json
└── intro.md
├── external
├── data
│ └── styles.luau
├── installGitDependencies.luau
├── installWallyDependencies.luau
├── makeDirectories.luau
├── spawnDarklua.luau
└── spawnSourcemap.luau
├── lune
├── dev.luau
├── initialize.luau
├── installDependencies.luau
└── tests.luau
├── moonwave.toml
├── rokit.toml
├── selene.toml
├── selene_definitions.yml
├── src
├── api.luau
├── client
│ ├── bridge.luau
│ ├── identifiers.luau
│ ├── parallelBridge.luau
│ └── process.luau
├── core
│ ├── deserializer.luau
│ ├── parallelConnections.luau
│ └── serializer.luau
├── dataModelTree.luau
├── identifierMap.luau
├── init.luau
├── initialization
│ ├── clientActor.luau
│ ├── clientSerial.luau
│ ├── serverActor.luau
│ ├── serverSerial.luau
│ ├── starter.client.luau
│ └── starter.server.luau
├── logStrings.luau
├── logger.luau
├── server
│ ├── bridge.luau
│ ├── identifiers.luau
│ └── process.luau
├── types.luau
└── util
│ └── result.luau
├── style-guide.md
├── stylua.toml
├── test-build.project.json
├── test-source.project.json
├── tests
├── lune
│ └── core.spec.luau
├── roblox
│ ├── clientRunner.client.luau
│ ├── clientStress.client.luau
│ ├── serverRunner.server.luau
│ └── serverStress.server.luau
├── suite.luau
└── utils.luau
├── wally.lock
└── wally.toml
/.darklua.json:
--------------------------------------------------------------------------------
1 | {
2 | "process": [
3 | {
4 | "current": {
5 | "name": "path",
6 | "sources": {
7 | "@pkg": "Packages/",
8 | "@dev-pkg": "DevPackages/",
9 | "@tests": "tests/",
10 | "@src": "src/"
11 | }
12 | },
13 | "rule": "convert_require",
14 | "target": {
15 | "indexing_style": "wait_for_child",
16 | "name": "roblox",
17 | "rojo_sourcemap": "sourcemap.json"
18 | }
19 | },
20 | {
21 | "rule": "inject_global_value",
22 | "identifier": "__timetracing__",
23 | "env": "timetracing"
24 | },
25 | {
26 | "rule": "inject_global_value",
27 | "identifier": "__verbose__",
28 | "env": "verbose"
29 | },
30 | {
31 | "rule": "inject_global_value",
32 | "identifier": "__testmode__",
33 | "env": "testmode"
34 | },
35 | {
36 | "rule": "compute_expression"
37 | },
38 | {
39 | "rule": "remove_unused_if_branch"
40 | }
41 | ]
42 | }
43 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | Packages
2 | node_modules
3 | build
4 | standalone.rbxm
5 | standalone
6 | *.rbxm
7 | sourcemap
8 | sourcemap.json
9 | src/old/**
10 | sourcemap.json
11 |
--------------------------------------------------------------------------------
/.luaurc:
--------------------------------------------------------------------------------
1 | {
2 | "languageMode": "strict",
3 | "aliases": {
4 | "pkg": "Packages/",
5 | "dev-pkg": "DevPackages/",
6 | "vendor": "vendor/",
7 | "src": "src/",
8 | "tests": "tests/",
9 | "lune": "~/.lune/.typedefs/0.8.9/",
10 | "ext": "external/"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/.moonwave/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ffrostfall/BridgeNet2/d7fd18a9613004c9cab959ec96b62ba3c3f9ac2e/.moonwave/static/favicon.png
--------------------------------------------------------------------------------
/.moonwave/static/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ffrostfall/BridgeNet2/d7fd18a9613004c9cab959ec96b62ba3c3f9ac2e/.moonwave/static/logo.png
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "johnnymorganz.stylua",
4 | "filiptibell.tooling-language-server",
5 | "kampfkarren.selene-vscode",
6 | "johnnymorganz.luau-lsp",
7 | "usernamehw.errorlens",
8 | "tamasfe.even-better-toml"
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "[luau]": {
3 | "editor.defaultFormatter": "JohnnyMorganz.stylua",
4 | "editor.formatOnSave": true
5 | },
6 |
7 | "files.eol": "\n",
8 |
9 | "luau-lsp.completion.imports.separateGroupsWithLine": true,
10 | "luau-lsp.completion.imports.suggestRequires": false,
11 | "luau-lsp.completion.imports.suggestServices": true,
12 |
13 | "luau-lsp.diagnostics.strictDatamodelTypes": true,
14 | "luau-lsp.ignoreGlobs": [
15 | "**/_Index/**",
16 | "build/**",
17 | "vendor/**",
18 | "Packages/**",
19 | ".vscode/**"
20 | ],
21 |
22 | "luau-lsp.fflags.enableNewSolver": true,
23 |
24 | "luau-lsp.sourcemap.autogenerate": false,
25 | "luau-lsp.sourcemap.enabled": true,
26 | "luau-lsp.types.definitionFiles": [
27 | ".vscode/vectorTypes.d.luau"
28 | ],
29 |
30 | "prettier.useTabs": true,
31 | "stylua.targetReleaseVersion": "latest",
32 | }
--------------------------------------------------------------------------------
/.vscode/vectorTypes.d.luau:
--------------------------------------------------------------------------------
1 | export type vector = Vector3
2 |
3 | declare vector: {
4 | create: (x: number, y: number, z: number) -> vector,
5 | magnitude: (vec: vector | Vector3) -> number,
6 | normalize: (vec: vector | Vector3) -> vector,
7 | cross: (vec1: vector | Vector3, vec2: vector | Vector3) -> vector,
8 | dot: (vec1: vector | Vector3, vec2: vector | Vector3) -> number,
9 | angle: (vec1: vector | Vector3, vec2: vector | Vector3, axis: (vector | Vector3)?) -> number,
10 | floor: (vec: vector | Vector3) -> vector,
11 | ceil: (vec: vector | Vector3) -> vector,
12 | abs: (vec: vector | Vector3) -> vector,
13 | sign: (vec: vector | Vector3) -> vector,
14 | clamp: (vec: vector | Vector3, min: vector | Vector3, max: vector | Vector3) -> vector,
15 | max: (...vector) -> vector,
16 | min: (...vector) -> vector,
17 |
18 | zero: vector,
19 | one: vector,
20 | }
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # BridgeNet2
2 |
3 | This project uses [semantic versioning](https://semver.org/spec/v2.0.0.html).
4 |
5 | ## version 2.0.0-rc1: 12/xx/2024
6 |
7 | This is a rewrite.
8 |
9 | ### TODO
10 |
11 | - Added `pass` and `drop` functions to middleware. This lets you drop data going through middleware explicitly
12 |
13 | ### Added
14 |
15 | - Added support for using BridgeNet2 in parallel
16 | - Added `ServerBridge:Fire(player)`, `ServerBridge:FireAll()`, `ServerBridge:FireAllExcept`, `ServerBridge:FireGroup`
17 | - Added an invocation queue. This should help with some loading order issues and just fix some edge cases
18 | - Added a cap of 255 identifiers. This is because if you go over that, it would cause de-optimization
19 |
20 | ### Improvements
21 |
22 | - Connections are now functions. Disconnect by calling the connection
23 | - Instead of a custom signal implementation, luausignal is used. This should yield perf improvements
24 | - Completely redid the backend using an improved format. Should see slightly better bandwidth and massively improved CPU usage
25 | - `ServerBridge.OnServerInvoke = func` is now `ServerBridge:OnInvoke(func)`
26 | - Enabled native codegen in a few places
27 | - Codebase is now properly typechecked
28 | - Redid logging & error messages
29 | - No longer using wally instance manager. Now using a reconciliation-based system (this should help w packages and parallel)
30 | - Receive & send code is now completely shared
31 |
32 | ### Fixes
33 |
34 | - Fixed players loading before initialization being bugged
35 | - Fixed requiring "warmup"
36 |
37 | ### Removed
38 |
39 | - Removed Player containers
40 | - Removed Connection class
41 | - Removed `BridgeNet2.CreateUUID()` (just use httpservice tbh)
42 | - Removed rate limiting. It's better to have it implemented on the user side
43 | - Removed `Bridge:Destroy()`. This did not work anyways
44 | - Removed `BridgeNet2.HandleInvalidPlayer`. It's dangerous and shouldn't actually be used to ban players
45 |
46 | ### Internal Codebase Improvements
47 |
48 | - Casing is now consistent: Everything internal is now camelCase, everything external is now PascalCase
49 | - Introduced automated testing
50 | - Switched to string requires using darklua
51 | - Switched to Luau syntax highlighting
52 | - Switched line endings from `crlf` to `lf`
53 | - Replaced JavaScript tooling with Lune tooling
54 |
55 | ## [version 1.0.0](https://github.com/ffrostfall/BridgeNet2/releases/tag/v1.0.0): 10/20/2023
56 |
57 | ### Added
58 |
59 | - Added an easy way to type payloads using generics. This will be elaborated on in documentation later
60 |
61 | ### Fixes
62 |
63 | - Fixed sending singular nil values with nothing else in the frame
64 | - Fixed a bug w/ the loading queue. Finally got around to that (https://github.com/ffrostfall/BridgeNet2/issues/35)
65 | - Type improvements
66 |
67 | ### Improvements
68 |
69 | - Added unique IDs to the invoke functionality. Should fix a multitude of bugs.
70 | - Re-did rate limiting. I'm confident that it's stable.
71 |
72 | ## [version 0.5.6](https://github.com/ffrostfall/BridgeNet2/releases/tag/v0.5.6): 9/25/2023
73 |
74 | ### Added
75 |
76 | - Now uses a system to manage package version control w/ instances. This ensures that even if you're running 2 separate versions of BridgeNet2, it will communicate correctly, and everything will work as expected.
77 |
78 | ## [version 0.5.5](https://github.com/ffrostfall/BridgeNet2/releases/tag/v0.5.5): 8/26/2023
79 |
80 | ### Fixes
81 |
82 | - All coroutine.resume instances have been replaced with task.spawn. This fixes a lot of obscure bugs.
83 |
84 | ## [version 0.5.4](https://github.com/ffrostfall/BridgeNet2/releases/tag/v0.5.4): 8/19/2023
85 |
86 | ### Added
87 |
88 | - You can now name connections, which will show up in logging and in the microprofiler.
89 | - Calling script and line are now shown for connections and firing bridges
90 |
91 | ### Improvements
92 |
93 | - Type improvements
94 |
95 | ## [version 0.5.3](https://github.com/ffrostfall/BridgeNet2/releases/tag/v0.5.3): 7/31/2023
96 |
97 | ### Added
98 |
99 | - A mock API for when BridgeNet2 is ran in edit mode. Limitations: InvokeServerAsync will infinitely yield, connections will never run.
100 |
101 | ### Improvements
102 |
103 | - BridgeNet2 nows prints the current version upon being loaded
104 | - Improved output readability
105 | - **Potentially breaking:** Bridges now are cached- this means you will have the same bridge object across scripts. This should be more expected behavior, and should overall be an improvement.
106 |
107 | ### Fixes
108 |
109 | - Potentially fixed some issues with loading and identifiers?
110 | - Fixed a bug where referencing a bridge multiple times clearing connections each time (Except on the client this time)
111 |
112 | ## [version 0.5.2](https://github.com/ffrostfall/BridgeNet2/releases/tag/v0.5.2): 7/15/2023
113 |
114 | ### Improvements
115 |
116 | - PlayerContainers now error if an incorrect amount of arguments is passed
117 |
118 | ### **Fixes**
119 |
120 | - Fixed a bug where referencing a bridge multiple times clearing connections each time
121 | - Fixed a bug where the player loading before BridgeNet2 starts would never initialize the player
122 |
123 | ## [version 0.5.0](https://github.com/ffrostfall/BridgeNet2/releases/tag/v0.5.0): 6/21/2023
124 |
125 | ### **Added**
126 |
127 | - Added `ServerBridge.OnServerInvoke` and `ClientBridge:InvokeServer()`
128 | - Added `.Connected` to connections
129 |
130 | ### **Fixes**
131 |
132 | - Fixed a bug with `Bridge:Wait`
133 |
134 | ### **Improvements**
135 |
136 | - Refactored the object-oriented programming pattern used w/ Bridges
137 | - Connections are now their own class
138 | - Calling methods with `.` instead of `:` will now error
139 | - Type improvements
140 | - `tostring()`-ing a bridge will now return its class type
141 |
142 | ## [version 0.4.1](https://github.com/ffrostfall/BridgeNet2/releases/tag/v0.4.1): 6/11/2023
143 |
144 | ### **Fixes**
145 |
146 | - Fixed some behavior w/ nil values
147 |
148 | ## [version 0.4.0](https://github.com/ffrostfall/BridgeNet2/releases/tag/v0.4.0): 6/10/2023
149 |
150 | ### **Added**
151 |
152 | - Added `BridgeNet2.ServerBridge` and `BridgeNet2.ClientBridge` constructors- they are identical to ReferenceBridge, just with better types. The current ReferenceBridge will not be deprecated or affected by this.
153 | - Instead of being limited to tables, you can now pass any type into :Fire(). This means you can finally pass in nil values to :Fire() too.
154 |
155 | ### **Fixes**
156 |
157 | - Fixed rate limiting
158 | - Re-added methods on the `Bridge` type to types
159 |
160 | ### **Improvements**
161 |
162 | - Added `Connection` type to `Bridge:Connect()` functions
163 | - `RateLimitActive` is now a public boolean that can be set by the user
164 | - Improved error messages
165 | - Improved logger readability.
166 | - I made a script to automate releases- there should hopefully be less inconsistencies with releases from now on.
167 |
168 | ## [version 0.3.0](https://github.com/ffrostfall/BridgeNet2/releases/tag/v0.3.0): 6/8/2023
169 |
170 | ### **Added**
171 |
172 | - Added Hoarcekat support
173 |
174 | ### **Removed**
175 |
176 | - NumberToBestForm removed (feature bloat)
177 | - StartLogging and StopLogging have been removed in favor of `object.Logging =`
178 |
179 | ### **Fixes**
180 |
181 | - Literally dozens of bugfixes
182 |
183 | ### **Improvements**
184 |
185 | - Massive internal re-structuring
186 | - Renamed `FromIdentifier` and `FromCompressed` to `Serialize` and `Deserialize`
187 | - Internally commented the entire project
188 | - Removed SetSecurity and SecurityEnums in favor of `HandleInvalidPlayer`
189 | - Type improvements
190 | - Logging now displays the packet size in bytes (using @PysephWasntAvailable's [RemotePacketSizeCounter](https://github.com/PysephWasntAvailable/RemotePacketSizeCounter) package)
191 | - Compliant with strict Luau typing
192 |
193 | ## version 0.2.2
194 |
195 | ### **Fixes**
196 |
197 | - Hotfix
198 |
199 | ## [version 0.2.1](https://github.com/ffrostfall/BridgeNet2/releases/tag/v0.2.1): 6/3/2023
200 |
201 | ### **Added**
202 |
203 | - Added :Once() and :Wait() to ClientBridge
204 |
205 | ### **Fixes**
206 |
207 | - Fixed a bug w/ :Wait() on ClientBridge
208 |
209 | ### **Improvements**
210 |
211 | - Default format/type security errors no longer throw
212 |
213 | ## [version 0.2.0](https://github.com/ffrostfall/BridgeNet2/releases/tag/v0.2.0): 5/11/2023
214 |
215 | ### **Added**
216 |
217 | - Added :Once()
218 | - Added :Wait()
219 | - Added :StartLogging() and :StopLogging()
220 | - Added :Disconnect() to connections
221 |
222 | ### **Removed**
223 |
224 | - Removed .Hook()
225 |
226 | ### **Improvements**
227 |
228 | - Added single-player targeting
229 | - "Silent" logs will display only in studio
230 |
231 | ## [version 0.1.0](https://github.com/ffrostfall/BridgeNet2/releases/tag/v0.1.0): 2/19/2023
232 |
233 | - Release
234 |
--------------------------------------------------------------------------------
/CONTRIBUTORS.md:
--------------------------------------------------------------------------------
1 | # Setup
2 |
3 | ---
4 |
5 | - Run `aftman install`
6 | - Run `wally install`
7 | - Start syncing `testing.project.json` for feature-related things, `stresstest.project.json` for performance-related things.
8 |
9 | # Contributing
10 |
11 | ---
12 |
13 | - **Do not publicly export functions directly.** There should always be something in the middle- preferably a file that returns a function.
14 | - Make sure you format all edited files before finalizing a PR. I'll set up actions for this eventually.
15 | - All new files must end in .luau if they are in the Luau lang. That being said- avoid creating new files. It clutters the source directory.
16 |
17 | # What to look out for
18 |
19 | ---
20 |
21 | - When fixing a bug, make sure to comment the bugfix if necessary- if it's an edge case for example, that requires a few extra lines, make comment that explains why that bug fix is there.
22 | - Avoid placing anything remotely performance-intensive inside of things like callback runners, middleware, outbound bridges, etc.
23 | - Never intrude on user code, never iterate over it, never touch it. This is one main selling point of BridgeNet2.
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2025 ffrostfall
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # I strongly recommend you use ByteNet over BridgeNet2: https://github.com/ffrostflame/ByteNet
2 |
3 |
4 |

5 |
6 |
7 | # BridgeNet2 v1.0.0
8 |
9 | ## Blazing fast & opinionated networking library designed to reduce bandwidth.
10 |
11 | BridgeNet2 is a networking library for Roblox with a focus on performance. It cuts out header data from RemoteEvent calls by 7 bytes, which is beneficial because it cuts down on the total number of packets per player. This in turn decreases server bandwidth used, so you can send more data. Games using BridgeNet2 will never hit the RemoteEvent throttle limit. BridgeNet2 also decreases the amount of time to process packets on the client by approximately 75-80%.
12 |
13 | BridgeNet2 has a simplistic API that mirrors RemoteEvents. It does this by using `Bridge:Fire()` instead of `RemoteEvent:FireClient()`, and `Bridge:Connect()` instead of `RemoteEvent.OnServerEvent:Connect()`. BridgeNet2 wraps remoteevents, making the developers job easier, by encapsulating a complex optimization process.
14 |
15 | Developers cannot fire a bridge with multiple parameters. This means you have to pass a table into `Bridge:Fire()`, instead of separate arguments. This design choice was chosen because it removes a layer of complexity. This choice is better for performance, stability, and typechecking. Also, doing this means BridgeNet2 never needs to touch the data that's pushed, it can just directly push that data through the RemoteEvent. As a side effect, BridgeNet2 allows developers to group data into an array or dictionary, as found in other Roblox projects.
16 |
17 | This library favors performance, and therefore we made choices that resulted in an opinionated library. BridgeNet2 never manipulates your data under the hood, but it does encourage developing in favor of performance.
18 |
19 | [Further Documentation](https://ffrostflame.github.io/BridgeNet2/)
20 |
--------------------------------------------------------------------------------
/build.project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bridgenet2",
3 | "tree": {
4 | "$path": "build/package"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/docs/Installation.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 2
3 | ---
4 |
5 | # Installation
6 |
7 | ## Through Wally [Recommended]
8 |
9 | If you're using Wally, you can simply drop this snippet in, except replace `latest` with the latest BridgeNet2 version.
10 |
11 | ```toml title="wally.toml"
12 | [dependencies]
13 | BridgeNet2 = "ffrostfall/bridgenet2@latest"
14 | ```
15 |
16 | ## Standalone
17 |
18 | Download `standalone.rbxm` from the [latest release](https://github.com/ffrostfall/BridgeNet2/releases/latest), and then insert it into your Roblox game.
19 |
--------------------------------------------------------------------------------
/docs/Technical Details/Identifiers.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 1
3 | ---
4 |
5 | # Identifiers
6 |
7 | Identifiers are actually extremely simple: it's a shared key-value list. The key is the compressed identifier, the value is the string value. The list is stored as attributes under the BridgeNet2 RemoteEvent. Identifiers are actually just numbers compressed using `string.pack`- every time you create an identifier, it increments that number by one and creates the attribute.
8 |
9 | ```lua title="/src/Server/ServerIdentifiers.luau" showLineNumbers
10 | -- optimization for under 255 identifiers
11 | local packed = if identifierCount <= 255
12 | then string.pack("B", identifierCount)
13 | else string.pack("H", identifierCount)
14 |
15 | identifierCount += 1
16 | identifierStorage:SetAttribute(identifierName, packed)
17 |
18 | fullIdentifierMap[identifierName] = packed
19 | compressedIdentifierMap[packed] = identifierName
20 | ```
21 |
22 | On the client, the client listens for new attributes added and attribute changes. It then stores these identifiers locally- the `Serialize` and `Deserialize` functions just directly interface with that table. This is why the `ReferenceIdentifier` function yields- it basically just waits a bit to see if the attribute loads in. If it already exists, it just accesses that in the local table.
23 |
24 | ```lua title="/src/Client/ClientIdentifiers.luau" {2-3} showLineNumbers
25 | for id, value in identifierStorage:GetAttributes() do
26 | fullIdentifierMap[id] = value
27 | compressedIdentifierMap[value] = id
28 | end
29 | ```
30 |
--------------------------------------------------------------------------------
/docs/Technical Details/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Technical Details",
3 | "position": 4
4 | }
--------------------------------------------------------------------------------
/docs/Tutorials/Best Practices.md:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Tutorials",
3 | "position": 7
4 | }
5 |
--------------------------------------------------------------------------------
/docs/Tutorials/Getting Started.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 1
3 | ---
4 |
5 | # Getting started with BridgeNet2
6 |
7 | ## Your first Bridge
8 | Bridges are extremely simple; they're just RemoteEvents, but BridgeNet2-ified! The API is, as said in previous sections, extremely simple and similar to RemoteEvents. The first difference between RemoteEvents and bridges is that bridges are created from strings, and RemoteEvents are instances. You can use the `ReferenceBridge` function under BridgeNet2 to create a bridge like so:
9 | ```lua
10 | local myFirstBridge = BridgeNet2.ReferenceBridge("myFirstBridge")
11 | ```
12 |
13 | :::info
14 |
15 | The variable name and the bridge's name don't need to match (as shown later), but it is a recommended practice when using BridgeNet2.
16 |
17 | :::
18 |
19 | It's very important to note that the strings must be the **exact same** on both the client and the server. But don't worry; BridgeNet2 will warn and tell you something's wrong if it can't find a bridge on the client!
20 |
21 | ### But wait, what about client vs. server?
22 | BridgeNet2's surface level API is almost the exact same, regardless of server and client! `ReferenceBridge` is used the exact same way on the server and the client. This is because there's no real reason to make them different, apart from typechecking. If you are interested in more "correct" typechecking/autocomplete, you can use the `ServerBridge` and `ClientBridge` functions like so:
23 | ```lua
24 | -- On the client..
25 | local myFirstBridgeClientVersion = BridgeNet2.ClientBridge("myFirstBridge")
26 |
27 | -- On the server..
28 | local myFirstBridgeServerVersion = BridgeNet2.ServerBridge("myFirstBridge")
29 | ```
30 | :::caution
31 |
32 | `ClientBridge` yields, and so does using `ReferenceBridge` on the client! You can set a timeout using the second optional timeout parameter.
33 |
34 | :::
35 |
36 | ### Firing a bridge
37 | The biggest API changes are in the `Fire` method. You can only pass a single argument in, but this argument can be *anything*. A table, a string, a boolean, nothing, whatever you want (this will become important later)! One very important differentation from the client's `Fire` function is that the server's version of the `Fire` method's first parameter is for the player(s)!
38 | ```lua
39 | local Players = game:GetService("Players")
40 |
41 | local myFirstBridge = BridgeNet2.ReferenceBridge("myFirstBridge")
42 |
43 | -- On the server..
44 | local you = Players.theReader
45 |
46 | myFirstBridge:Fire(you, "Hello!")
47 | ```
48 |
49 | ### Firing to every single player
50 | BridgeNet2 has built-in support for firing to multiple players, but there isn't a `FireAllClients` method! Instead, we have the `AllPlayers` function. This returns a "symbol" that says "fire this stuff to every single player". It's used in the same parameter as a singular player, like so:
51 | ```lua
52 | local myFirstBridge = BridgeNet2.ReferenceBridge("myFirstBridge")
53 |
54 | myFirstBridge:Fire(BridgeNet2.AllPlayers(), "Hello everyone!")
55 | ```
56 |
57 | ### Firing to an array of players
58 | Alongside the `AllPlayers` function there is also the `Players` function. This function lets you easily fire data to a specific set of players like so:
59 | ```lua
60 | local myFirstBridge = BridgeNet2.ReferenceBridge("myFirstBridge")
61 |
62 | myFirstBridge:Fire(BridgeNet2.Players({ Players.SpecialPlayerA, Players.SpecialPlayerB }), "Hello special players! Only you get to see this.")
63 | ```
64 |
65 | ### Firing to every player except certain players
66 | This function is the inverted version of the `Players` function. You can fire to everyone except certain players- this is useful for things like client-sided prediction. It can be used just like the `Players` function:
67 | ```lua
68 | local myFirstBridge = BridgeNet2.ReferenceBridge("myFirstBridge")
69 |
70 | myFirstBridge:Fire(BridgeNet2.PlayersExcept({ Players.BadPlayerA }), "Everyone except BadPlayerA gets this!")
71 | ```
72 |
73 | ### Firing from the client
74 | Firing from the client is the exact same as firing on the server, except without the `targetPlayer` parameter:
75 | ```lua
76 | local firstClientBridge = BridgeNet2.ClientBridge("myFirstBridge")
77 |
78 | firstClientBridge:Fire("Hey, server!")
79 | ```
80 |
81 | ### Connecting to a bridge
82 | Connecting to a bridge and connecting to a RemoteEvent are very similar; one's just way shorter! It's basically the exact same:
83 | ```lua
84 | local myFirstBridge = BridgeNet2.ReferenceBridge("myFirstBridge")
85 |
86 | -- On the client..
87 | myFirstBridge:Connect(function(message)
88 | print(message) -- prints "Hello everyone!"
89 | end)
90 |
91 | -- On the server..
92 | myFirstBridge:Connect(function(player, message)
93 | print(`{player.Name} said {message}`) -- prints "Client said Hey, server!"
94 | end)
95 | ```
96 |
97 | ### What if I wanted to send multiple things?
98 | Since BridgeNet2 only allows you to send a singualr argument through `Fire`, you can just pass a table!
99 | ```lua
100 | -- On the server..
101 | local myFirstBridge = BridgeNet2.ReferenceBridge("myFirstBridge")
102 |
103 | myFirstBridge:Fire(BridgeNet2.AllPlayers(), {
104 | "This is a fully intact array!",
105 | "Tables can store any data you need.",
106 | "You don't need multiple arguments if you can use tables."
107 | })
108 | ```
109 | ```lua
110 | -- On the client..
111 | local myFirstBridge = BridgeNet2.ReferenceBridge("myFirstBridge")
112 |
113 | myFirstBridge:Connect(function(array)
114 | print(array) -- Prints the array we sent from the server
115 | end)
116 | ```
--------------------------------------------------------------------------------
/docs/Tutorials/Logging.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 2
3 | ---
4 |
5 | # Logging network traffic
6 |
7 | BridgeNet2's most powerful non-performance-related feature is logging. Logging can be enabled with a single line of code, anywhere. You can enable logging by setting the `Logging` property on any bridge to `true`, and disable logging by setting the `Logging` property to false.
8 |
9 | When you start logging a bridge, it will look like a little bit like this:
10 |
11 | 
12 |
13 | BridgeNet2 converts any passed argument into a string- including tables, and nested tables. BridgeNet2 will also count the number of bytes and appends it to the end of the log (thats the "(28B)" you see!). This is done using Pyseph's [RemotePacketSizeCounter](https://github.com/PysephWasntAvailable/RemotePacketSizeCounter) library.
14 |
15 | ### Log dumps
16 |
17 | As of right now, log dumps are not a supported feature unfortunately. However, this will be coming in the future.
--------------------------------------------------------------------------------
/docs/Tutorials/Using Identifiers.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 3
3 | ---
4 |
5 | # Identifiers
6 | Identifiers are one of the cooler parts about BridgeNet2- they basically take a string you can understand and read, then assign it a three-byte identifier that both the client and server can understand! This is important because it lets you organize your data in a readable *and* efficient way. For example, take the following code:
7 | ```lua
8 | local sendSomeData = BridgeNet2.ReferenceBridge("sendSomeData")
9 |
10 | sendSomeData:Fire({
11 | firstThingToSend = 5,
12 | anotherThing = false,
13 | })
14 | ```
15 |
16 | :::tip
17 |
18 | If you'd like to know more about bandwidth usage w/ networking on Roblox, [you can check out this link here](https://devforum.roblox.com/t/in-depth-information-about-robloxs-remoteevents-instance-replication-and-physics-replication-w-sources/1847340).
19 |
20 | :::
21 |
22 | ## What's the problem?
23 | We can understand `firstThingToSend` and `anotherThingToSend` as humans, but they take more bandwidth than our actual data, *three times over!* Bandwidth is valuable, and we don't really want to shorten the names we see just because it takes a lot of bandwidth to send. That just makes our code harder to read.
24 |
25 | ## What's the solution?
26 | This is where identifiers come in:
27 | ```lua
28 | local sendSomeData = BridgeNet2.ReferenceBridge("sendSomeData")
29 |
30 | local firstThingToSend = BridgeNet2.ReferenceIdentifier("firstThingToSend")
31 | local anotherThing = BridgeNet2.ReferenceIdentifier("anotherThing")
32 |
33 | sendSomeData:Fire({
34 | [firstThingToSend] = 5,
35 | [anotherThing] = false,
36 | })
37 | ```
38 | At the cost of 2 lines of code, we completely solved the problem! Our code is now both readable and efficient.
39 |
--------------------------------------------------------------------------------
/docs/Tutorials/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Tutorials",
3 | "position": 3
4 | }
--------------------------------------------------------------------------------
/docs/intro.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 1
3 | ---
4 |
5 | # Intro
6 |
7 | BridgeNet2 is a networking library for Roblox that focuses on performance. It's meant to have a simplistic API that mirrors RemoteEvents, with the changes of `Bridge:Fire` instead of `RemoteEvent:FireClient`, and `Bridge:Connect` instead of `RemoteEvent.OnServerEvent:Connect`. This is because BridgeNet2's optimization process is super complex, and bridges need to have a simplistic API or else I will go insane as the person maintaining this library.
8 |
9 | ## API Design
10 |
11 | When using BridgeNet2, you can't fire a bridge with multiple arguments. This means you need to pass a table into Bridge:Fire if you want to send multiple arguments, instead of separate arguments. This design choice was chosen because it removes a layer of complexity, alongside being better for performance, stability, and typechecking. Doing this also means BridgeNet2 never needs to manipulate the data that's pushed, the library can just directly send that data through the RemoteEvent.
12 |
13 | ## Singular parameter for `:Fire()`
14 |
15 | This library favors performance, and therefore we made choices that resulted in an opinionated library. This is shown with a few decisions: cutting out varargs, opting for thread reusage which clutters the stack trace, and the `PlayerContainer`s: `BridgeNet2.Players()`, `BridgeNet2.ExceptPlayers()`, etc.
16 |
--------------------------------------------------------------------------------
/external/data/styles.luau:
--------------------------------------------------------------------------------
1 | local colorful = require("@vendor/lunePackages/colorful")
2 |
3 | local style = colorful.combineStyles
4 | local colors = colorful.color
5 | local modifiers = colorful.modifier
6 |
7 | return {
8 | header = style({ colors.blue, modifiers.bold }),
9 | detail = style({ colors.white, modifiers.dim }),
10 |
11 | success = style({ colors.green, modifiers.bold }),
12 | error = style({ colors.red, modifiers.bold }),
13 | warning = style({ colors.yellow, modifiers.bold }),
14 | }
15 |
--------------------------------------------------------------------------------
/external/installGitDependencies.luau:
--------------------------------------------------------------------------------
1 | local async = require("@vendor/lunePackages/async")
2 | local fs = require("@lune/fs")
3 | local spawnProcess = require("@vendor/lunePackages/spawnProcess")
4 | local styles = require("./data/styles")
5 |
6 | return function()
7 | print(styles.header("Cloning git repositories"))
8 | async(spawnProcess("git", {
9 | "clone",
10 | "https://github.com/ffrostfall/lunePackages.git",
11 | "vendor/lunePackagesRepo",
12 | }))
13 |
14 | print(styles.header("Copying repositories to vendor"))
15 | if fs.isDir("vendor/lunePackages") then fs.removeDir("vendor/lunePackages") end
16 | fs.copy("vendor/lunePackagesRepo", "vendor/lunePackages")
17 |
18 | fs.removeDir("vendor/lunePackagesRepo")
19 | end
20 |
--------------------------------------------------------------------------------
/external/installWallyDependencies.luau:
--------------------------------------------------------------------------------
1 | local async = require("@vendor/lunePackages/async")
2 | local process = require("@lune/process")
3 | local spawnProcess = require("@vendor/lunePackages/spawnProcess")
4 | local spawnSourcemap = require("@ext/spawnSourcemap")
5 | local styles = require("./data/styles")
6 |
7 | return function()
8 | print(styles.header("Installing Wally dependencies"))
9 | async(spawnProcess("wally", { "install" }))
10 |
11 | print(styles.header("Building sourcemap"))
12 |
13 | async(spawnSourcemap(false))
14 |
15 | print(styles.header("Exporting types"))
16 |
17 | process.spawn("wally-package-types", { "--sourcemap", "sourcemap.json", "Packages" }, {
18 | stdio = "forward",
19 | })
20 |
21 | print("\n" .. styles.success("Done!"))
22 | end
23 |
--------------------------------------------------------------------------------
/external/makeDirectories.luau:
--------------------------------------------------------------------------------
1 | local fs = require("@lune/fs")
2 |
3 | local function reconcileDirectory(path: string)
4 | if not fs.isDir(path) then
5 | fs.writeDir(path)
6 | end
7 | end
8 |
9 | return function()
10 | reconcileDirectory("build/")
11 | reconcileDirectory("build/package/")
12 | reconcileDirectory("build/tests/")
13 | end
14 |
--------------------------------------------------------------------------------
/external/spawnDarklua.luau:
--------------------------------------------------------------------------------
1 | local spawnProcess = require("@vendor/lunePackages/spawnProcess")
2 |
3 | return function()
4 | spawnProcess("darklua", {
5 | "process",
6 | "src/",
7 | "build/package/",
8 | "--watch",
9 | })
10 |
11 | spawnProcess("darklua", {
12 | "process",
13 | "tests/roblox/",
14 | "build/tests/",
15 | "--watch",
16 | })
17 | end
18 |
--------------------------------------------------------------------------------
/external/spawnSourcemap.luau:
--------------------------------------------------------------------------------
1 | local spawnProcess = require("@vendor/lunePackages/spawnProcess")
2 |
3 | return function(watch: boolean?): thread
4 | local parameters = {
5 | "sourcemap",
6 | "test-source.project.json",
7 | "--output",
8 | "sourcemap.json",
9 | }
10 |
11 | if watch then table.insert(parameters, "--watch") end
12 |
13 | return spawnProcess("rojo", parameters)
14 | end
15 |
--------------------------------------------------------------------------------
/lune/dev.luau:
--------------------------------------------------------------------------------
1 | local async = require("@vendor/lunePackages/async")
2 | local colorful = require("@vendor/lunePackages/colorful")
3 | local spawnDarklua = require("@ext/spawnDarklua")
4 | local spawnProcess = require("@vendor/lunePackages/spawnProcess")
5 | local spawnSourcemap = require("@ext/spawnSourcemap")
6 | local task = require("@lune/task")
7 | local process = require("@lune/process")
8 |
9 | local function getargs(): {
10 | timetracing: boolean,
11 | verbose: boolean,
12 | testmode: "benchmark" | "dev",
13 | }
14 | local args = process.args
15 | return {
16 | timetracing = table.find(args, "timetracing") ~= nil,
17 | verbose = table.find(args, "verbose") ~= nil,
18 | testmode = if table.find(args, "benchmark") ~= nil then "benchmark" else "dev",
19 | }
20 | end
21 |
22 | local args = getargs()
23 | process.env["timetracing"] = tostring(args.timetracing)
24 | process.env["verbose"] = tostring(args.verbose)
25 | process.env["testmode"] = args.testmode
26 |
27 | print(colorful.color.blueBright("Starting development server"))
28 |
29 | spawnSourcemap(true)
30 |
31 | -- avoid sourcemap dependency issues
32 | task.wait(0.5)
33 |
34 | spawnDarklua()
35 |
36 | async(spawnProcess("rojo", {
37 | "serve",
38 | "test-build.project.json",
39 | }))
40 |
--------------------------------------------------------------------------------
/lune/initialize.luau:
--------------------------------------------------------------------------------
1 | local fs = require("@lune/fs")
2 | local process = require("@lune/process")
3 |
4 | if not fs.isDir("vendor/") then fs.writeDir("vendor/") end
5 |
6 | process.spawn("git", {
7 | "clone",
8 | "https://github.com/ffrostfall/lunePackages.git",
9 | "vendor/lunePackagesRepo",
10 | }, {
11 | stdio = "forward",
12 | })
13 |
14 | if fs.isDir("vendor/lunePackages") then fs.removeDir("vendor/lunePackages") end
15 | fs.copy("vendor/lunePackagesRepo", "vendor/lunePackages")
16 | fs.removeDir("vendor/lunePackagesRepo")
17 |
--------------------------------------------------------------------------------
/lune/installDependencies.luau:
--------------------------------------------------------------------------------
1 | local installGitDependencies = require("@ext/installGitDependencies")
2 | local installWallyDependencies = require("@ext/installWallyDependencies")
3 | local process = require("@lune/process")
4 |
5 | if #process.args == 0 then
6 | installGitDependencies()
7 | installWallyDependencies()
8 | elseif process.args[1] == "git" then
9 | installGitDependencies()
10 | elseif process.args[1] == "wally" then
11 | installWallyDependencies()
12 | end
13 |
--------------------------------------------------------------------------------
/lune/tests.luau:
--------------------------------------------------------------------------------
1 | local colorful = require("@vendor/lunePackages/colorful")
2 | local fs = require("@lune/fs")
3 | local luau = require("@lune/luau")
4 | local process = require("@lune/process")
5 | local roblox = require("@lune/roblox")
6 | local serde = require("@lune/serde")
7 | local task = require("@lune/task")
8 |
9 | local tests = fs.readDir("tests/lune/")
10 | local luaurc = serde.decode("json", fs.readFile(".luaurc"))
11 | local Vector3 = ((roblox :: any).Vector3) :: typeof(Vector3)
12 | local CFrame = (roblox :: any).CFrame :: typeof(CFrame)
13 |
14 | local testCases = {}
15 |
16 | function injectedRequire(path: string): any
17 | if string.sub(path, 1, 5) == "@lune" then
18 | return require(path) :: any
19 | end
20 |
21 | local workingPath = ""
22 | local parts = string.split(path, "/")
23 |
24 | if string.sub(parts[1], 1, 1) == "@" then
25 | workingPath = luaurc.aliases[string.sub(parts[1], 2, -1)]
26 | table.remove(parts, 1)
27 |
28 | workingPath ..= table.concat(parts, "/")
29 |
30 | local filePath = ""
31 | if fs.isDir(workingPath) then
32 | filePath = workingPath .. "/init.luau"
33 | else
34 | filePath = workingPath .. ".luau"
35 | end
36 |
37 | local compiled = luau.compile(fs.readFile(filePath))
38 | local loaded = luau.load(compiled, {
39 | environment = env(),
40 | })
41 |
42 | process.env["__currentModule"] = workingPath
43 | local result = loaded()
44 | process.env["__currentModule"] = nil
45 |
46 | return result
47 | else
48 | assert(process.env["__currentModule"], "Cannot require non-aliased modules outside of a module context")
49 |
50 | local currentModule = process.env["__currentModule"]
51 |
52 | local source = fs.readFile(currentModule .. path .. ".luau")
53 |
54 | local compiled = luau.compile(source)
55 | local loaded = luau.load(compiled, {
56 | environment = env(),
57 | })
58 |
59 | process.env["__currentModule"] = currentModule .. path
60 | local result = loaded()
61 | process.env["__currentModule"] = currentModule
62 |
63 | return result
64 | end
65 | end
66 |
67 | function env()
68 | return {
69 | Vector3 = Vector3,
70 | task = task,
71 | CFrame = CFrame,
72 |
73 | require = injectedRequire,
74 | }
75 | end
76 |
77 | for _, fileName in tests do
78 | testCases[fileName] = luau.load(luau.compile(fs.readFile(`tests/lune/{fileName}`)), {
79 | environment = env(),
80 | })
81 | end
82 |
83 | for fileName, runner in testCases do
84 | local results = runner()
85 | print(colorful.modifier.bold(fileName))
86 |
87 | for _, case in results do
88 | case()
89 | end
90 | end
91 |
--------------------------------------------------------------------------------
/moonwave.toml:
--------------------------------------------------------------------------------
1 | title = "BridgeNet2"
2 | gitRepoUrl = "https://github.com/ffrostfall/bridgenet2/"
3 |
4 | changelog = true
5 |
6 | [docusaurus]
7 | projectName = "BridgeNet2"
8 | tagline = "Blazing fast & opinionated networking library designed to reduce bandwidth"
9 | favicon = "favicon.png"
10 |
11 | [navbar.logo]
12 | alt = "BridgeNet2"
13 | src = "logo.png"
14 |
15 | [footer]
16 | style = "dark"
17 |
18 | [home]
19 | enabled = true
20 | includeReadme = false
21 |
22 | [[home.features]]
23 | title = "Cuts the header from RemoteEvents"
24 | description = "BridgeNet2 cuts down successive remote event call size from 9 bytes to 2 bytes"
25 |
26 | [[home.features]]
27 | title = "High quality logging"
28 | description = "BridgeNet2 makes logging network traffic incredibly easy in an extremely readable manner"
29 |
30 | [[home.features]]
31 | title = "Oriented around performance"
32 | description = "If your game is having performance issues related to networking, switching to BridgeNet2 will help."
--------------------------------------------------------------------------------
/rokit.toml:
--------------------------------------------------------------------------------
1 | [tools]
2 | rojo = "rojo-rbx/rojo@7.4.4"
3 | wally = "UpliftGames/wally@0.3.1"
4 | stylua = "JohnnyMorganz/stylua@2.0.2"
5 | moonwave = "evaera/moonwave@1.2.1"
6 | lune = "lune-org/lune@0.8.9"
7 | wally-package-types = "JohnnyMorganz/wally-package-types@1.3.2"
8 | darklua = "seaofvoices/darklua@0.15.0"
9 |
--------------------------------------------------------------------------------
/selene.toml:
--------------------------------------------------------------------------------
1 | std = "selene_definitions"
2 |
3 | exclude = ["Packages/**"]
4 |
5 | [lints]
6 | mixed_table = "allow"
7 | multiple_statements = "allow"
8 | undefined_variable = "allow"
9 | incorrect_standard_library_use = "allow"
10 |
--------------------------------------------------------------------------------
/selene_definitions.yml:
--------------------------------------------------------------------------------
1 | base: roblox
2 |
3 | name: selene_defs
4 | globals:
5 | require:
6 | args:
7 | - type: string
8 |
--------------------------------------------------------------------------------
/src/api.luau:
--------------------------------------------------------------------------------
1 | export type BridgeNet2 = {}
2 |
3 | return nil
4 |
--------------------------------------------------------------------------------
/src/client/bridge.luau:
--------------------------------------------------------------------------------
1 | local ClientBridge = {}
2 | local metatable = { __index = ClientBridge }
3 | export type Identity = typeof(setmetatable({} :: {}, metatable))
4 |
5 | local function constructor(name: string): Identity
6 | local self = setmetatable({}, metatable)
7 |
8 | return self
9 | end
10 |
11 | function ClientBridge.Fire() end
12 |
13 | function ClientBridge.Connect() end
14 |
15 | function ClientBridge.InvokeServer() end
16 |
17 | function ClientBridge.OutboundMiddleware() end
18 |
19 | function ClientBridge.InboundMiddleware() end
20 |
21 | function ClientBridge.Wait() end
22 |
23 | function ClientBridge.Once() end
24 |
25 | return {
26 | new = constructor,
27 | }
28 |
--------------------------------------------------------------------------------
/src/client/identifiers.luau:
--------------------------------------------------------------------------------
1 | local dataModelTree = require("@src/dataModelTree")
2 | local logStrings = require("@src/logStrings")
3 | local logger = require("@src/logger").new()
4 | local identifierMap = require("@src/identifierMap")
5 |
6 | local client = {}
7 |
8 | function client.init(tree: dataModelTree.Identity)
9 | for name, id in tree.identifiers:GetAttributes() do
10 | if typeof(id) ~= "number" then
11 | logger:warn(string.format(logStrings.errors.ID_PROPERTY_INCORRECT_TYPE, typeof(id)))
12 | continue
13 | end
14 |
15 | identifierMap.addIdentifier(name, id)
16 | end
17 |
18 | tree.identifiers.AttributeChanged:Connect(function(name)
19 | local id = tree.identifiers:GetAttribute(name)
20 |
21 | if typeof(id) == "number" then
22 | -- Added case
23 | identifierMap.addIdentifier(name, id)
24 | elseif not id then
25 | -- Removed case
26 | identifierMap.removeIdentifier(name)
27 | end
28 | end)
29 | end
30 |
31 | return client
32 |
--------------------------------------------------------------------------------
/src/client/parallelBridge.luau:
--------------------------------------------------------------------------------
1 | local logger = require("@src/logger").new("client (parallel)")
2 | local logStrings = require("@src/logStrings")
3 | local identifierMap = require("@src/identifierMap")
4 | local dataModelTree = require("@src/dataModelTree")
5 |
6 | local tree: dataModelTree.Identity
7 |
8 | local function disallowed(name: string): never
9 | return logger:fatal(string.format(logStrings.errors.PARALLEL_DISALLOWED, name))
10 | end
11 |
12 | local ClientBridge = {}
13 | local metatable = { __index = ClientBridge }
14 | export type Identity = typeof(setmetatable(
15 | {} :: {
16 | _id: number,
17 | },
18 | metatable
19 | ))
20 |
21 | local function constructor(name: string, timeout: number?): Identity
22 | if not tree then
23 | return logger:fatal(logStrings.errors.ACCESS_WITHOUT_INITIALIZATION)
24 | end
25 |
26 | local self = setmetatable({}, metatable)
27 |
28 | local start = os.clock()
29 | while (not identifierMap.fromName(name)) and (os.clock() - start <= (timeout or 1)) do
30 | task.wait()
31 | end
32 |
33 | local id = identifierMap.fromName(name)
34 | if not id then
35 | return logger:fatal(logStrings.errors.ID_TIMEOUT_EXHAUSTED)
36 | end
37 |
38 | self._id = id
39 |
40 | return self
41 | end
42 |
43 | function ClientBridge.Fire(self: Identity, value: any)
44 | tree.parallel:Fire(self._id, value)
45 | end
46 |
47 | function ClientBridge.Connect(self: Identity) end
48 |
49 | function ClientBridge.InvokeServer()
50 | disallowed("ParallelClientBridge:InvokeServer")
51 | end
52 |
53 | function ClientBridge.OutboundMiddleware()
54 | disallowed("ParallelClientBridge:OutboundMiddleware")
55 | end
56 |
57 | function ClientBridge.InboundMiddleware()
58 | disallowed("ParallelClientBridge:InboundMiddleware")
59 | end
60 |
61 | function ClientBridge.Wait()
62 | disallowed("ParallelClientBridge:Wait")
63 | end
64 |
65 | function ClientBridge.Once()
66 | disallowed("ParallelClientBridge:Once")
67 | end
68 |
69 | return {
70 | new = constructor,
71 | tree = function(passed: dataModelTree.Identity)
72 | tree = passed
73 | end,
74 | }
75 |
--------------------------------------------------------------------------------
/src/client/process.luau:
--------------------------------------------------------------------------------
1 | local RunService = game:GetService("RunService")
2 |
3 | local serializer = require("@src/core/serializer")
4 | local deserializer = require("@src/core/deserializer")
5 | local types = require("@src/types")
6 | local dataModelTree = require("@src/dataModelTree")
7 | local logger = require("@src/logger").new("main client process")
8 | local logStrings = require("@src/logStrings")
9 |
10 | local replicationRate: number = 1 / 61
11 | local lastReplicationTick: number = 0
12 | local queue: types.Queue = {}
13 | local remote: RemoteEvent = nil
14 | local metaRemote: RemoteEvent = nil
15 | local fireServer = Instance.new("RemoteEvent").FireServer
16 |
17 | local function _assertRemote(func: string): ()
18 | if not (metaRemote and remote) then
19 | return logger:fatal(string.format(logStrings.errors.ACCESS_WITHOUT_INITIALIZATION, func))
20 | end
21 | end
22 |
23 | local client = {}
24 |
25 | function client.init(tree: dataModelTree.Identity)
26 | remote = tree.remote
27 | metaRemote = tree.meta
28 |
29 | tree.parallel.Event:Connect(client.addEventCall)
30 | RunService.PostSimulation:Connect(client.step)
31 | remote.OnClientEvent:Connect(function(packed)
32 | deserializer.decode(packed[1], packed[2])
33 | end)
34 |
35 | metaRemote:FireServer("\0")
36 | end
37 |
38 | function client.setReplicationRate(newRate: number)
39 | replicationRate = 1 / newRate
40 | end
41 |
42 | function client.addEventCall(id: number, content: unknown)
43 | table.insert(queue, id)
44 | table.insert(queue, content)
45 | end
46 |
47 | function client.step(deltaTime: number)
48 | if ((os.clock() - lastReplicationTick) >= replicationRate) and (#queue > 0) then
49 | local serialized = serializer(queue)
50 | fireServer(remote, { serialized.events :: any, serialized.buff :: any } :: any)
51 | table.clear(queue)
52 |
53 | lastReplicationTick = os.clock()
54 | end
55 | end
56 |
57 | return client
58 |
--------------------------------------------------------------------------------
/src/core/deserializer.luau:
--------------------------------------------------------------------------------
1 | --!native
2 | local signal = require("@pkg/signal")
3 |
4 | local idToSignal: { [number]: signal.Identity<...any> } = {}
5 |
6 | local deserializer = {}
7 |
8 | function deserializer.exists(id: number)
9 | return idToSignal[id] ~= nil
10 | end
11 |
12 | function deserializer.registerId(id: number): signal.Identity<...any>
13 | idToSignal[id] = signal()
14 | return idToSignal[id]
15 | end
16 |
17 | function deserializer.fetchEventSignal(id: number): signal.Identity<...any>
18 | return idToSignal[id]
19 | end
20 |
21 | function deserializer.decode(events: { [number]: { unknown } }, buff: buffer, player: Player?)
22 | if _G.__timetracing__ == "true" then
23 | debug.profilebegin("event begin")
24 | end
25 |
26 | if _G.__verbose__ == "true" then
27 | print("remote call", player, events, buff)
28 | end
29 |
30 | -- it's duplicate but its best performing so
31 | if player then
32 | -- copypaste this one though
33 | for index, calls in events do
34 | local id = buffer.readu8(buff, index - 1)
35 | local eventSignal = idToSignal[id]
36 | if not eventSignal then
37 | warn("todo: handle this case")
38 | continue
39 | end
40 |
41 | for _, value in calls do
42 | -- Event logic
43 | eventSignal:fire(player, value)
44 | end
45 | end
46 | else
47 | for index, calls in events do
48 | local id = buffer.readu8(buff, index - 1)
49 | local eventSignal = idToSignal[id]
50 | if not eventSignal then
51 | warn("todo: handle this case")
52 | continue
53 | end
54 |
55 | for _, value in calls do
56 | -- Event logic
57 | eventSignal:fire(value)
58 | end
59 | end
60 | end
61 |
62 | if _G.__timetracing__ then
63 | debug.profilebegin("server event end")
64 | end
65 | end
66 |
67 | return deserializer
68 |
--------------------------------------------------------------------------------
/src/core/parallelConnections.luau:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ffrostfall/BridgeNet2/d7fd18a9613004c9cab959ec96b62ba3c3f9ac2e/src/core/parallelConnections.luau
--------------------------------------------------------------------------------
/src/core/serializer.luau:
--------------------------------------------------------------------------------
1 | --!native
2 | local types = require("@src/types")
3 |
4 | local function serializer(queue: types.Queue): types.SerializedPacket
5 | local outgoingEvents: types.SerializedPacket = {
6 | events = {},
7 | buff = buffer.create(0),
8 | }
9 |
10 | local idToIndex: { number } = {}
11 | local indexToId: { number } = {}
12 |
13 | for i = 1, #queue, 2 do
14 | local id = queue[i] :: number
15 | local content = queue[i + 1] :: Content
16 | local index = idToIndex[id]
17 |
18 | if index then
19 | table.insert(outgoingEvents.events[index], content)
20 | else
21 | table.insert(indexToId, id)
22 | index = #indexToId
23 | idToIndex[id] = index
24 |
25 | outgoingEvents.events[index :: any] = { content }
26 | end
27 | end
28 |
29 | local buff = buffer.create(#indexToId)
30 | for index, id in indexToId do
31 | buffer.writeu8(buff, index - 1, id)
32 | end
33 |
34 | outgoingEvents.buff = buff
35 |
36 | return outgoingEvents
37 | end
38 |
39 | return serializer
40 |
--------------------------------------------------------------------------------
/src/dataModelTree.luau:
--------------------------------------------------------------------------------
1 | local WFC_TIMEOUT = 3
2 |
3 | local ReplicatedStorage = game:GetService("ReplicatedStorage")
4 | local RunService = game:GetService("RunService")
5 |
6 | local result = require("@src/util/result")
7 | local logStrings = require("@src/logStrings")
8 |
9 | local cached: Identity? = nil
10 |
11 | export type Identity = {
12 | container: Folder,
13 | remote: RemoteEvent,
14 | meta: RemoteEvent,
15 | parallel: BindableEvent,
16 | parallelConnections: Camera,
17 |
18 | identifiers: Folder,
19 | }
20 |
21 | local function create(): Identity
22 | local container = Instance.new("Folder")
23 | local remote = Instance.new("RemoteEvent")
24 | local meta = Instance.new("RemoteEvent")
25 | local identifiers = Instance.new("Folder")
26 | local parallel = Instance.new("BindableEvent")
27 | local parallelConnections = Instance.new("Camera")
28 |
29 | identifiers.Name = "identifiers"
30 | meta.Name = "meta"
31 | remote.Name = "remote"
32 | container.Name = "bridgenet2"
33 | parallel.Name = "parallel"
34 | parallelConnections.Name = "parallelConnections"
35 |
36 | identifiers.Parent = container
37 | meta.Parent = container
38 | remote.Parent = container
39 | parallel.Parent = container
40 | parallelConnections.Parent = container
41 |
42 | container.Parent = ReplicatedStorage
43 |
44 | local tree = {
45 | container = container,
46 | remote = remote,
47 | meta = meta,
48 | parallel = parallel,
49 | identifiers = identifiers,
50 | parallelConnections = parallelConnections,
51 | }
52 | cached = tree
53 |
54 | return tree
55 | end
56 |
57 | local function find(): result.Identity
58 | local container = ReplicatedStorage:WaitForChild("bridgenet2", WFC_TIMEOUT)
59 | if not container then
60 | return result(false, logStrings.errors.INSTANCE_NOT_FOUND)
61 | end
62 |
63 | if not container:IsA("Folder") then
64 | return result(false, logStrings.errors.INSTANCE_NOT_VALID)
65 | end
66 |
67 | local remote = container:WaitForChild("remote", WFC_TIMEOUT)
68 | if not (remote and remote:IsA("RemoteEvent")) then
69 | return result(false, logStrings.errors.INSTANCE_NOT_VALID)
70 | end
71 |
72 | local meta = container:WaitForChild("meta", WFC_TIMEOUT)
73 | if not (meta and meta:IsA("RemoteEvent")) then
74 | return result(false, logStrings.errors.INSTANCE_NOT_VALID)
75 | end
76 |
77 | local parallel = container:WaitForChild("parallel", WFC_TIMEOUT)
78 | if not (parallel and parallel:IsA("BindableEvent")) then
79 | return result(false, logStrings.errors.INSTANCE_NOT_VALID)
80 | end
81 |
82 | local identifiers = container:WaitForChild("identifiers", WFC_TIMEOUT)
83 | if not (identifiers and identifiers:IsA("Folder")) then
84 | return result(false, logStrings.errors.INSTANCE_NOT_VALID)
85 | end
86 |
87 | local parallelConnections = container:WaitForChild("parallelConnections", WFC_TIMEOUT)
88 | if not (parallelConnections and parallelConnections:IsA("Camera")) then
89 | return result(false, logStrings.errors.INSTANCE_NOT_VALID)
90 | end
91 |
92 | local tree = {
93 | container = container,
94 | remote = remote,
95 | parallel = parallel,
96 | meta = meta,
97 | parallelConnections = parallelConnections,
98 | identifiers = identifiers,
99 | }
100 | cached = tree
101 |
102 | return result(true, tree)
103 | end
104 |
105 | return function(fresh: boolean?): result.Identity
106 | if cached then
107 | return result(true, cached)
108 | end
109 |
110 | if RunService:IsServer() then
111 | if fresh then
112 | return result(true, create())
113 | else
114 | return find()
115 | end
116 | else
117 | return find()
118 | end
119 | end
120 |
--------------------------------------------------------------------------------
/src/identifierMap.luau:
--------------------------------------------------------------------------------
1 | local idToName: { [number]: string } = {}
2 | local nameToId: { [string]: number } = {}
3 |
4 | export type Identifier = number
5 |
6 | local identifierMap = {}
7 |
8 | function identifierMap.addIdentifier(name: string, id: Identifier)
9 | idToName[id] = name
10 | nameToId[name] = id
11 | end
12 |
13 | function identifierMap.removeIdentifier(name: string)
14 | local id = nameToId[name]
15 |
16 | idToName[id] = nil
17 | nameToId[name] = nil
18 | end
19 |
20 | function identifierMap.fromName(name: string): Identifier?
21 | return nameToId[name]
22 | end
23 |
24 | function identifierMap.fromId(id: Identifier): string?
25 | return idToName[id]
26 | end
27 |
28 | return identifierMap
29 |
--------------------------------------------------------------------------------
/src/init.luau:
--------------------------------------------------------------------------------
1 | local RunService = game:GetService("RunService")
2 |
3 | local clientActor = require("@src/initialization/clientActor")
4 | local serverActor = require("@src/initialization/serverActor")
5 | local clientSerial = require("@src/initialization/clientSerial")
6 | local serverSerial = require("@src/initialization/serverSerial")
7 |
8 | local threadContext: "actor" | "serial" = if script:GetActor() ~= nil then "actor" else "serial"
9 | local runContext: "server" | "client" = if RunService:IsServer() then "server" else "client"
10 |
11 | if threadContext == "actor" and runContext == "client" then
12 | clientActor()
13 | elseif threadContext == "actor" and runContext == "server" then
14 | serverActor()
15 | elseif threadContext == "serial" and runContext == "client" then
16 | clientSerial()
17 | elseif threadContext == "serial" and runContext == "server" then
18 | serverSerial()
19 | end
20 |
21 | return {}
22 |
--------------------------------------------------------------------------------
/src/initialization/clientActor.luau:
--------------------------------------------------------------------------------
1 | return function() end
2 |
--------------------------------------------------------------------------------
/src/initialization/clientSerial.luau:
--------------------------------------------------------------------------------
1 | local process = require("@src/client/process")
2 | local identifiers = require("@src/server/identifiers")
3 | local dataModelTree = require("@src/dataModelTree")
4 | local logger = require("@src/logger").new("client")
5 | local logStrings = require("@src/logStrings")
6 |
7 | return function()
8 | local treeRes = dataModelTree()
9 | if not treeRes.success then
10 | logger:warn(string.format(logStrings.errors.TREE_FAILURE, treeRes.err))
11 | print(treeRes.trace)
12 | return logger:fatal("exiting bridgenet2")
13 | end
14 |
15 | process.init(treeRes.value)
16 | identifiers.init(treeRes.value)
17 |
18 | logger:log(logStrings.logs.SERIAL_LOADED)
19 | end
20 |
--------------------------------------------------------------------------------
/src/initialization/serverActor.luau:
--------------------------------------------------------------------------------
1 | return function() end
2 |
--------------------------------------------------------------------------------
/src/initialization/serverSerial.luau:
--------------------------------------------------------------------------------
1 | local process = require("@src/server/process")
2 | local identifiers = require("@src/server/identifiers")
3 | local dataModelTree = require("@src/dataModelTree")
4 | local logger = require("@src/logger").new("server")
5 | local logStrings = require("@src/logStrings")
6 |
7 | return function(): ()
8 | local treeRes = dataModelTree(true)
9 | if not treeRes.success then
10 | logger:warn(string.format(logStrings.errors.TREE_FAILURE, treeRes.err))
11 | print(treeRes.trace)
12 | return logger:fatal("exiting bridgenet2")
13 | end
14 |
15 | process.init(treeRes.value)
16 | identifiers.init(treeRes.value)
17 |
18 | logger:log(logStrings.logs.SERIAL_LOADED)
19 | end
20 |
--------------------------------------------------------------------------------
/src/initialization/starter.client.luau:
--------------------------------------------------------------------------------
1 | require("@src/")
2 |
--------------------------------------------------------------------------------
/src/initialization/starter.server.luau:
--------------------------------------------------------------------------------
1 | require("@src/")
2 |
--------------------------------------------------------------------------------
/src/logStrings.luau:
--------------------------------------------------------------------------------
1 | local errorStrings = {
2 | INSTANCE_NOT_FOUND = "couldn't find instances under replicatedstorage, waitforchild timeout hit",
3 | INSTANCE_NOT_VALID = "bridgenet2's internal instance tree wasnt valid, was it modified?",
4 |
5 | ID_PROPERTY_INCORRECT_TYPE = "identifier [%*] has an invalid attribute type, was it modified?",
6 |
7 | ACCESS_WITHOUT_INITIALIZATION = "function [%*] accessed without initialization",
8 |
9 | TREE_FAILURE = "tree failed to initialize: \n\t%*",
10 | SIGNAL_NOT_INITIALIZED = "tried to connect to event ID [%*], but it wasnt registered",
11 |
12 | PARALLEL_DISALLOWED = "cannot call function %* in parallel",
13 |
14 | ID_TIMEOUT_EXHAUSTED = "TODO error message",
15 | }
16 |
17 | local processed: { [any]: any } = {}
18 | for errorName, errorText in pairs(errorStrings) do
19 | if errorName == "TREE_FAILURE" then
20 | processed[errorName] = errorText
21 | continue
22 | end
23 |
24 | processed[errorName] = `{errorText} [{errorName}]`
25 | end
26 |
27 | return {
28 | errors = processed :: typeof(errorStrings),
29 | logs = {
30 | SERIAL_LOADED = "v2.0.0 successfully loaded onto main thread",
31 | },
32 | }
33 |
--------------------------------------------------------------------------------
/src/logger.luau:
--------------------------------------------------------------------------------
1 | --!optimize 1
2 | export type LogLevel = "trace" | "log" | "warn" | "fatal" | "halt"
3 |
4 | local function getNameFromStack()
5 | local name = debug.info(3, "s")
6 | local split = string.split(name, ".")
7 |
8 | return `{split[#split - 1]}/{split[#split]}`
9 | end
10 |
11 | local logger = {}
12 | local metatable = { __index = logger }
13 | export type Identity = typeof(setmetatable(
14 | {} :: {
15 | _name: string,
16 | },
17 | metatable
18 | ))
19 |
20 | local function constructor(name: string?): Identity
21 | local self = setmetatable({}, metatable)
22 |
23 | self._name = name or getNameFromStack()
24 |
25 | return self
26 | end
27 |
28 | function logger.log(self: Identity, text: string)
29 | print(`[bn2/{self._name}] {text}`)
30 | end
31 |
32 | function logger.warn(self: Identity, text: string)
33 | warn(`[bn2/{self._name}] {text}`)
34 | end
35 |
36 | function logger.fatal(self: Identity, text: string): never
37 | return error(`[bn2/{self._name}] {text}`, 0)
38 | end
39 |
40 | return {
41 | new = constructor,
42 | }
43 |
--------------------------------------------------------------------------------
/src/server/bridge.luau:
--------------------------------------------------------------------------------
1 | local signal = require("@pkg/signal")
2 | local process = require("@src/server/process")
3 | local identifiers = require("@src/server/identifiers")
4 | local identifierMap = require("@src/identifierMap")
5 | local deserializer = require("@src/core/deserializer")
6 |
7 | local ServerBridge = {}
8 | local metatable = { __index = ServerBridge }
9 | export type Identity = typeof(setmetatable(
10 | {} :: {
11 | _id: number,
12 | _signal: signal.Identity<...any>,
13 | },
14 | metatable
15 | ))
16 |
17 | local function constructor(name: string): Identity
18 | local self = setmetatable({
19 | _id = identifierMap.fromName(name) or identifiers.register(name),
20 | }, metatable)
21 |
22 | self._signal = if deserializer.exists(self._id)
23 | then deserializer.fetchEventSignal(self._id)
24 | else deserializer.registerId(self._id)
25 |
26 | return self
27 | end
28 |
29 | function ServerBridge.Fire(self: Identity, player: Player, value: any)
30 | process.firePlayer(player, self._id, value)
31 | end
32 |
33 | function ServerBridge.FireAllExcept(self: Identity, exception: Player, value: any) end
34 |
35 | function ServerBridge.FireGroup(self: Identity, players: { Player }, value: any)
36 | for _, player in players do
37 | process.firePlayer(player, self._id, value)
38 | end
39 | end
40 |
41 | function ServerBridge.FireAll(self: Identity)
42 | process.fireAll(self._id)
43 | end
44 |
45 | function ServerBridge.Connect(self: Identity, callback: (player: Player, data: unknown) -> ()): () -> ()
46 | return self._signal:connect(callback)
47 | end
48 |
49 | function ServerBridge.OnInvoke(self: Identity) end
50 |
51 | function ServerBridge.OutboundMiddleware(self: Identity) end
52 |
53 | function ServerBridge.InboundMiddleware(self: Identity) end
54 |
55 | function ServerBridge.Wait(self: Identity) end
56 |
57 | function ServerBridge.Once(self: Identity, callback: (player: Player, data: unknown) -> ()): () -> ()
58 | return self._signal:once(callback)
59 | end
60 |
61 | return {
62 | new = constructor,
63 | }
64 |
--------------------------------------------------------------------------------
/src/server/identifiers.luau:
--------------------------------------------------------------------------------
1 | local dataModelTree = require("@src/dataModelTree")
2 | local logger = require("@src/logger").new("server identifiers")
3 | local logStrings = require("@src/logStrings")
4 | local identifierMap = require("@src/identifierMap")
5 |
6 | local counter: Identifier = 1
7 | local container: Folder
8 |
9 | export type Identifier = number
10 |
11 | local function assertContainer(func: string): ()
12 | if not container then
13 | return logger:fatal(string.format(logStrings.errors.ACCESS_WITHOUT_INITIALIZATION, func))
14 | end
15 | end
16 |
17 | local server = {}
18 |
19 | function server.init(tree: dataModelTree.Identity)
20 | container = tree.container
21 | end
22 |
23 | function server.register(name: string): number
24 | assertContainer("register")
25 |
26 | counter += 1
27 | local chosenId = counter
28 |
29 | container:SetAttribute(name, chosenId)
30 | identifierMap.addIdentifier(name, counter)
31 |
32 | return counter
33 | end
34 |
35 | return server
36 |
--------------------------------------------------------------------------------
/src/server/process.luau:
--------------------------------------------------------------------------------
1 | --!native
2 | local Players = game:GetService("Players")
3 | local RunService = game:GetService("RunService")
4 |
5 | local dataModelTree = require("@src/dataModelTree")
6 | local types = require("@src/types")
7 | local serializer = require("@src/core/serializer")
8 | local deserializer = require("@src/core/deserializer")
9 |
10 | local remote: RemoteEvent
11 | local playerQueues: { [Player]: types.Queue } = {}
12 | local readyPlayers: { [Player]: true } = {}
13 | local fireClient = Instance.new("RemoteEvent").FireClient
14 |
15 | local server = {}
16 |
17 | function server.init(tree: dataModelTree.Identity)
18 | tree.meta.OnServerEvent:Connect(function(player, str)
19 | if str == "\0" then
20 | readyPlayers[player] = true
21 |
22 | server.emptyPlayerQueue(player)
23 |
24 | if _G.__verbose__ then
25 | print(`player {player.Name} readied`)
26 | end
27 | end
28 | end)
29 |
30 | remote = tree.remote
31 |
32 | for _, player in Players:GetPlayers() do
33 | playerQueues[player] = {}
34 | end
35 |
36 | Players.PlayerAdded:Connect(function(player)
37 | playerQueues[player] = {}
38 | end)
39 |
40 | Players.PlayerRemoving:Connect(function(player)
41 | readyPlayers[player] = nil
42 | playerQueues[player] = nil
43 | end)
44 |
45 | RunService.PostSimulation:Connect(server.step)
46 | tree.remote.OnServerEvent:Connect(function(player, packed)
47 | deserializer.decode(packed[1], packed[2], player)
48 | end)
49 | end
50 |
51 | function server.firePlayer(player: Player, id: number, content: unknown)
52 | local queue = playerQueues[player]
53 |
54 | table.insert(queue, id)
55 | table.insert(queue, content)
56 | end
57 |
58 | function server.fireAll(id: number, content: unknown)
59 | for _, queue in playerQueues do
60 | table.insert(queue, id)
61 | table.insert(queue, content)
62 | end
63 | end
64 |
65 | function server.emptyPlayerQueue(player: Player)
66 | if #playerQueues[player] == 0 then
67 | return
68 | end
69 |
70 | if _G.__verbose__ == "true" then
71 | print(`emptying {player.Name} queue`)
72 | end
73 |
74 | local serialized = serializer(playerQueues[player])
75 | fireClient(remote, player, { serialized.events, serialized.buff :: any } :: any)
76 | table.clear(playerQueues[player])
77 | end
78 |
79 | function server.step()
80 | if _G.__timetracing__ == "true" then
81 | debug.profilebegin("server begin")
82 | end
83 |
84 | for player in readyPlayers do
85 | server.emptyPlayerQueue(player)
86 | end
87 |
88 | if _G.__timetracing__ == "true" then
89 | debug.profileend()
90 | end
91 | end
92 |
93 | return server
94 |
--------------------------------------------------------------------------------
/src/types.luau:
--------------------------------------------------------------------------------
1 | --[[
2 | Private
3 | ]]
4 | export type Content = unknown
5 | export type SerializedPacket = {
6 | events: {
7 | -- The numbers here don't cost anything, because it's an array.
8 | [number]: {
9 | -- Each value here is an individual call to an event.
10 | [number]: Content,
11 | },
12 | },
13 |
14 | -- The IDs are packed into this buffer
15 | buff: buffer,
16 | }
17 |
18 | -- { event, content, event, content, event, content }
19 | export type Queue = { number | Content }
20 |
21 | return nil
22 |
--------------------------------------------------------------------------------
/src/util/result.luau:
--------------------------------------------------------------------------------
1 | export type Identity = {
2 | success: true,
3 | value: T,
4 | } | {
5 | success: false,
6 | err: string,
7 | trace: string,
8 | }
9 |
10 | return (
11 | (
12 | function(success: boolean, resultOrError: T & string): Identity
13 | if success then
14 | return {
15 | success = true,
16 | value = resultOrError,
17 | }
18 | else
19 | return {
20 | success = false,
21 | trace = debug.traceback(),
22 | err = resultOrError,
23 | }
24 | end
25 | end
26 | ) :: any
27 | ) :: ((success: true, value: T) -> Identity) & ((success: false, err: string) -> Identity)
28 |
--------------------------------------------------------------------------------
/style-guide.md:
--------------------------------------------------------------------------------
1 | # Style Guide
2 |
3 | This will only list exceptions / additions to the [Roblox Lua Style Guide](https://roblox.github.io/lua-style-guide/).
4 |
5 | ## Casing
6 |
7 | - All **directly publicly exported functions** should be in PascalCase.
8 | - All private functions should be in camelCase.
9 | - **Do not use an underscore unless it is in a public API- in which case, ensure it is gone from the exported type.**
10 |
--------------------------------------------------------------------------------
/stylua.toml:
--------------------------------------------------------------------------------
1 | column_width = 120
2 | line_endings = "Unix"
3 | indent_type = "Tabs"
4 | indent_width = 4
5 | quote_style = "AutoPreferDouble"
6 | collapse_simple_statement = "Never"
--------------------------------------------------------------------------------
/test-build.project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bridgenet2-tests",
3 | "emitLegacyScripts": false,
4 |
5 | "tree": {
6 | "$className": "DataModel",
7 |
8 | "ReplicatedStorage": {
9 | "Packages": {
10 | "$path": "Packages",
11 |
12 | "BridgeNet2": {
13 | "$path": "build/package/"
14 | }
15 | },
16 |
17 | "tests": {
18 | "$path": "build/tests/"
19 | }
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/test-source.project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bridgenet2-tests",
3 |
4 | "tree": {
5 | "$className": "DataModel",
6 |
7 | "ReplicatedStorage": {
8 | "Packages": {
9 | "$path": "Packages",
10 |
11 | "BridgeNet2": {
12 | "$path": "src/"
13 | }
14 | },
15 |
16 | "tests": {
17 | "$path": "tests/roblox/"
18 | }
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tests/lune/core.spec.luau:
--------------------------------------------------------------------------------
1 | local case, eof = require("@tests/suite")()
2 | local encode = require("@src/core/encode")
3 | local utils = require("@tests/utils")
4 |
5 | case("encodes data correctly", function(interface)
6 | interface.expect.equal(encode({ 1, "test", 2, "test" }), {
7 | buff = buffer.fromstring(string.char(1) .. string.char(2)),
8 | events = {
9 | { "test" },
10 | { "test" },
11 | },
12 | })
13 | end)
14 |
15 | return eof()
16 |
--------------------------------------------------------------------------------
/tests/roblox/clientRunner.client.luau:
--------------------------------------------------------------------------------
1 | if _G.__testmode__ == "benchmark" then
2 | return
3 | end
4 |
5 | local ReplicatedStorage = game:GetService("ReplicatedStorage")
6 |
7 | local _BridgeNet2 = require(ReplicatedStorage.Packages.BridgeNet2)
8 | local clientProcess = require("@src/client/process")
9 |
10 | clientProcess.registerEvent(1)
11 |
12 | clientProcess.connect(1, print)
13 |
--------------------------------------------------------------------------------
/tests/roblox/clientStress.client.luau:
--------------------------------------------------------------------------------
1 | if _G.__testmode__ ~= "benchmark" then
2 | return
3 | end
4 |
5 | local ReplicatedStorage = game:GetService("ReplicatedStorage")
6 |
7 | local _BridgeNet2 = require(ReplicatedStorage.Packages.BridgeNet2)
8 | local clientProcess = require("@src/client/process")
9 |
10 | clientProcess.registerEvent(1)
11 |
12 | clientProcess.connect(1, function(data) end)
13 |
--------------------------------------------------------------------------------
/tests/roblox/serverRunner.server.luau:
--------------------------------------------------------------------------------
1 | if _G.__testmode__ == "benchmark" then
2 | return
3 | end
4 |
5 | local ReplicatedStorage = game:GetService("ReplicatedStorage")
6 |
7 | local _BridgeNet2 = require(ReplicatedStorage.Packages.BridgeNet2)
8 | local serverProcess = require("@src/server/process")
9 |
10 | serverProcess.addEvent(1)
11 | serverProcess.connect(1, print)
12 |
13 | while true do
14 | serverProcess.fireAll(1, "testing")
15 |
16 | task.wait(1)
17 | end
18 |
--------------------------------------------------------------------------------
/tests/roblox/serverStress.server.luau:
--------------------------------------------------------------------------------
1 | if _G.__testmode__ ~= "benchmark" then
2 | return
3 | end
4 |
5 | print("running stress test")
6 |
7 | local ReplicatedStorage = game:GetService("ReplicatedStorage")
8 | local RunService = game:GetService("RunService")
9 |
10 | local _BridgeNet2 = require(ReplicatedStorage.Packages.BridgeNet2)
11 | local serverProcess = require("@src/server/process")
12 |
13 | serverProcess.addEvent(1)
14 | serverProcess.connect(1, print)
15 |
16 | RunService.Heartbeat:Connect(function()
17 | debug.profilebegin("benchmark")
18 | for i = 1, 200 do
19 | serverProcess.fireAll(1, "")
20 | end
21 | debug.profileend()
22 | end)
23 |
--------------------------------------------------------------------------------
/tests/suite.luau:
--------------------------------------------------------------------------------
1 | local colorful = require("@vendor/lunePackages/colorful/init")
2 | local utils = require("@tests/utils")
3 |
4 | local blue = colorful.color.blueBright
5 | local bold = colorful.modifier.bold
6 | local red = colorful.color.redBright
7 | local errorStyle = colorful.combineStyles({ red })
8 | local infoStyle = colorful.combineStyles({ blue })
9 | local logStyle = colorful.combineStyles({ bold })
10 |
11 | local failureFormatStrings = {
12 | equality = `\t{infoStyle("Left: ") .. "%*"}\n\t{infoStyle("Right: ") .. "%*"}\n\n\t{logStyle(
13 | "Expected values to be equal"
14 | )}`,
15 |
16 | truthy = `\t{"Expected value [%*] to be truthy"}\n`,
17 | }
18 |
19 | export type Interface = {
20 | expect: {
21 | equal: (left: T, right: T) -> (),
22 | truthy: (value: T) -> (),
23 | },
24 | }
25 |
26 | local function printTable(tbl: { [unknown]: unknown })
27 | local lines = {}
28 |
29 | for key, value in tbl do
30 | if typeof(value) ~= "table" and typeof(value) ~= "buffer" then
31 | table.insert(lines, `\t\[{key}] = {value}`)
32 | elseif typeof(value) == "table" then
33 | local result = printTable(value :: {})
34 | local replaced = string.gsub(result, "\n", "\n\t")
35 |
36 | table.insert(lines, `\t\[{key}] = {replaced}`)
37 | elseif typeof(value) == "buffer" then
38 | return utils.bufferFormatted(value)
39 | end
40 | end
41 |
42 | return `\{\n{table.concat(lines, ",\n")}\n\}`
43 | end
44 |
45 | local function prettytostring(value: unknown): string
46 | if typeof(value) == "table" then
47 | return printTable(value :: any)
48 | elseif typeof(value) == "buffer" then
49 | return utils.bufferFormatted(value)
50 | else
51 | return tostring(value)
52 | end
53 | end
54 |
55 | -- Taken from Roact: https://github.com/Roblox/roact/blob/master/src/assertDeepEqual.lua
56 | local function deepEqual(a: any, b: any): (boolean, string?)
57 | if typeof(a) ~= typeof(b) then
58 | local message = ("{1} is of type %s, but {2} is of type %s"):format(typeof(a), typeof(b))
59 | return false, message
60 | end
61 |
62 | if typeof(a) == "number" then
63 | -- Floating point error!
64 | return math.abs(a - b) < 0.0001
65 | end
66 |
67 | if typeof(a) == "table" then
68 | local visitedKeys = {}
69 |
70 | for key, value in a do
71 | visitedKeys[key] = true
72 |
73 | local success, innerMessage = deepEqual(value, b[key])
74 | if not success and innerMessage then
75 | local message = innerMessage
76 | :gsub("{1}", ("{1}[%s]"):format(prettytostring(key)))
77 | :gsub("{2}", ("{2}[%s]"):format(prettytostring(key)))
78 |
79 | return false, message
80 | end
81 | end
82 |
83 | for key, value in b do
84 | if not visitedKeys[key] then
85 | local success, innerMessage = deepEqual(value, a[key])
86 |
87 | if not success and innerMessage then
88 | local message = innerMessage
89 | :gsub("{1}", ("{1}[%s]"):format(prettytostring(key)))
90 | :gsub("{2}", ("{2}[%s]"):format(prettytostring(key)))
91 |
92 | return false, message
93 | end
94 | end
95 | end
96 |
97 | return true, nil
98 | end
99 |
100 | if typeof(a) == "buffer" then
101 | if buffer.len(a) ~= buffer.len(b) then
102 | return false, "{1} ~= {2}"
103 | end
104 |
105 | for i = 0, buffer.len(a) - 1 do
106 | if buffer.readu8(a, i) ~= buffer.readu8(b, i) then
107 | print("false")
108 | return false, `\{1\}[{i}] ~= \{2\}[{i}]`
109 | end
110 | end
111 |
112 | return true
113 | end
114 |
115 | if a == b then
116 | return true, nil
117 | end
118 |
119 | local message = "{1} ~= {2}"
120 | return false, message
121 | end
122 |
123 | local function interface(failCallback: (err: string) -> (), successCallback: () -> ()): Interface
124 | local function expectEqual(left: T, right: T)
125 | local equality = deepEqual(left, right)
126 |
127 | if equality then
128 | successCallback()
129 | return
130 | end
131 |
132 | failCallback(string.format(failureFormatStrings.equality, prettytostring(left), prettytostring(right)))
133 | end
134 |
135 | local function expectTruthy(value: T)
136 | if value then
137 | successCallback()
138 | return
139 | end
140 |
141 | failCallback(string.format(failureFormatStrings.truthy, prettytostring(value)))
142 | end
143 |
144 | return {
145 | expect = {
146 | equal = expectEqual,
147 | truthy = expectTruthy,
148 | },
149 | }
150 | end
151 |
152 | local function case(name: string, test: (interface: Interface) -> ()): () -> boolean
153 | local checks: { boolean } = {}
154 | local failureMessages: { string } = {}
155 |
156 | local caseInterface = interface(function(err)
157 | table.insert(checks, false)
158 | table.insert(failureMessages, err)
159 | end, function()
160 | table.insert(checks, true)
161 | end)
162 |
163 | return function(): boolean
164 | local success, err: any = pcall(test, caseInterface)
165 |
166 | if success and not table.find(checks, false) then
167 | print(infoStyle("\t- Test case passed: ") .. name)
168 | return true
169 | elseif success and table.find(checks, false) then
170 | print(`\t{errorStyle("- Test case failed: ")}{name}`)
171 | for _, message in failureMessages do
172 | print(message)
173 | end
174 | return false
175 | elseif not success then
176 | print(errorStyle("\t- Error in test case: ") .. name .. "\n\n", err, "\n")
177 | end
178 |
179 | return false
180 | end
181 | end
182 |
183 | return function()
184 | local cases: { () -> boolean } = {}
185 |
186 | return function(name: string, test: (interface: Interface) -> ())
187 | table.insert(cases, case(name, test))
188 | end, function()
189 | return cases
190 | end
191 | end
192 |
--------------------------------------------------------------------------------
/tests/utils.luau:
--------------------------------------------------------------------------------
1 | local function bufferToDecimal(buff: buffer): string
2 | return string.gsub(buffer.tostring(buff), ".", function(c)
3 | return string.format("%03i ", string.byte(c :: any))
4 | end)
5 | end
6 |
7 | local isPrimitiveType = { string = true, number = true, boolean = true, buffer = true }
8 |
9 | local function isPrimitiveArray(array)
10 | local max, n = 0, 0
11 |
12 | for key, value in array do
13 | if typeof(key) ~= "number" then
14 | return false
15 | end
16 | if key <= 0 then
17 | return false
18 | end
19 | if not isPrimitiveType[typeof(value)] then
20 | return false
21 | end
22 |
23 | max = if key > max then key else max
24 | n = n + 1
25 | end
26 |
27 | return n == max
28 | end
29 |
30 | local function formatValue(value)
31 | if typeof(value) == "buffer" then
32 | return bufferToDecimal(value)
33 | end
34 |
35 | if typeof(value) ~= "string" then
36 | return tostring(value)
37 | end
38 |
39 | return string.format("%q", value)
40 | end
41 |
42 | local function formatKey(key, seq)
43 | if seq then
44 | return ""
45 | end
46 |
47 | if typeof(key) ~= "string" then
48 | return `[{tostring(key)}] =`
49 | end
50 |
51 | -- key is a simple identifier
52 | if key:match("^[%a_][%w_]-$") == key then
53 | return `{key} = `
54 | end
55 |
56 | return `[{string.format("%q", key)}] = `
57 | end
58 |
59 | local typeSortOrder = {
60 | ["boolean"] = 1,
61 | ["number"] = 2,
62 | ["string"] = 3,
63 | ["function"] = 4,
64 | ["vector"] = 5,
65 | ["buffer"] = 6,
66 | ["thread"] = 7,
67 | ["table"] = 8,
68 | ["userdata"] = 9,
69 | ["nil"] = 10,
70 | }
71 |
72 | local function traverseTable(dataTable, seen, indent)
73 | local output = ""
74 |
75 | local indentStr = string.rep(" ", indent)
76 |
77 | local keys = {}
78 |
79 | for key, value in dataTable do
80 | if isPrimitiveType[typeof(key)] then
81 | keys[#keys + 1] = key
82 | end
83 | end
84 |
85 | table.sort(keys, function(a, b)
86 | local typeofTableA, typeofTableB = typeof(dataTable[a]), typeof(dataTable[b])
87 |
88 | if typeofTableA ~= typeofTableB then
89 | return typeSortOrder[typeofTableA] < typeSortOrder[typeofTableB]
90 | end
91 |
92 | if typeof(a) == "number" and typeof(b) == "number" then
93 | return a < b
94 | end
95 |
96 | return tostring(a) < tostring(b)
97 | end)
98 |
99 | local inSequence = false
100 | local previousKey = 0
101 |
102 | for idx, key in keys do
103 | if typeof(key) == "number" and key > 0 and key - 1 == previousKey then
104 | previousKey = key
105 | inSequence = true
106 | else
107 | inSequence = false
108 | end
109 |
110 | local value = dataTable[key]
111 | if typeof(value) ~= "table" then
112 | if isPrimitiveType[typeof(value)] then
113 | output = `{output}{indentStr}{formatKey(key, inSequence)}{formatValue(value)},\n`
114 | end
115 |
116 | continue
117 | end
118 |
119 | -- prevents self-referential tables from looping infinitely
120 | if seen[value] then
121 | continue
122 | end
123 |
124 | seen[value] = true
125 |
126 | local hasItems = false
127 | for key, val in value do
128 | if isPrimitiveType[typeof(key)] and isPrimitiveType[typeof(val)] then
129 | hasItems = true
130 | break
131 | end
132 |
133 | if typeof(val) == "table" then
134 | hasItems = true
135 | break
136 | end
137 | end
138 |
139 | if not hasItems then
140 | output = string.format("%s%s%s{},\n", output, indentStr, formatKey(key, inSequence))
141 | continue
142 | end
143 |
144 | if isPrimitiveArray(value) then -- collapse primitive arrays
145 | output = string.format("%s%s%s{", output, indentStr, formatKey(key, inSequence))
146 |
147 | for index = 1, #value do
148 | output = output .. formatValue(value[index])
149 | if index < #value then
150 | output = output .. ", "
151 | end
152 | end
153 |
154 | output = output .. "},\n"
155 | continue
156 | end
157 |
158 | output = string.format(
159 | "%s%s%s{\n%s%s},\n",
160 | output,
161 | indentStr,
162 | formatKey(key, inSequence),
163 | traverseTable(value, seen, indent + 1),
164 | indentStr
165 | )
166 |
167 | seen[value] = nil
168 | end
169 |
170 | return output
171 | end
172 |
173 | local utils = {}
174 |
175 | function utils.bufferFormatted(buff: buffer): string
176 | return bufferToDecimal(buff)
177 | end
178 |
179 | function utils.prettyPrint(data: any)
180 | if typeof(data) == "table" then
181 | print("{\n" .. traverseTable(data, { [data] = true }, 1) .. "}")
182 | return
183 | elseif typeof(data) == "buffer" then
184 | print(bufferToDecimal(data))
185 | return
186 | end
187 |
188 | print(tostring(data))
189 | end
190 |
191 | return utils
192 |
--------------------------------------------------------------------------------
/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 = "ffrostfall/bridgenet2"
7 | version = "1.0.0"
8 | dependencies = [["RemotePacketSizeCounter", "pysephwasntavailable/remotepacketsizecounter@2.1.0"], ["signal", "ffrostfall/luausignal@0.4.0"]]
9 |
10 | [[package]]
11 | name = "ffrostfall/luausignal"
12 | version = "0.4.0"
13 | dependencies = []
14 |
15 | [[package]]
16 | name = "pysephwasntavailable/remotepacketsizecounter"
17 | version = "2.1.0"
18 | dependencies = []
19 |
--------------------------------------------------------------------------------
/wally.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "ffrostfall/bridgenet2"
3 | version = "1.0.0"
4 | registry = "https://github.com/UpliftGames/wally-index"
5 | realm = "shared"
6 | description = "The successor to BridgeNet, BridgeNet2 is a blazing-fast networking library designed for scale and performance."
7 | license = "MIT"
8 |
9 | [dependencies]
10 | RemotePacketSizeCounter = "pysephwasntavailable/remotepacketsizecounter@2.1.0"
11 | signal = "ffrostfall/luausignal@0.4.0"
12 |
--------------------------------------------------------------------------------