├── .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 | ![image|690x89](https://devforum-uploads.s3.dualstack.us-east-2.amazonaws.com/uploads/original/5X/f/9/a/c/f9acd229d50af22ebc8785c272e70133e7b4bc12.png) 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 | --------------------------------------------------------------------------------