├── .gitattributes ├── .luaurc ├── rokit.toml ├── branding ├── favicon.png ├── bytenetLogo.png └── index.md ├── default.project.json ├── .vscode ├── extensions.json └── settings.json ├── .github └── ISSUE_TEMPLATE │ ├── bug.md │ ├── feature.md │ └── improvement.md ├── .gitignore ├── stylua.toml ├── src ├── process │ ├── channel_state.luau │ ├── read.luau │ ├── client.luau │ └── server.luau ├── data_types │ ├── nothing.luau │ ├── int8.luau │ ├── uint8.luau │ ├── int16.luau │ ├── int32.luau │ ├── uint16.luau │ ├── uint32.luau │ ├── float32.luau │ ├── float64.luau │ ├── vec2.luau │ ├── unknown.luau │ ├── string.luau │ ├── inst.luau │ ├── vec3.luau │ ├── color3.luau │ ├── bool.luau │ ├── buff.luau │ ├── optional.luau │ ├── cframe.luau │ ├── array.luau │ ├── struct.luau │ └── map.luau ├── packets │ ├── define_packet.luau │ └── packet.luau ├── types.luau └── init.luau ├── wally.toml ├── wally.lock ├── LICENSE ├── README.md └── CHANGELOG.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.luaurc: -------------------------------------------------------------------------------- 1 | { 2 | "languageMode": "strict" 3 | } -------------------------------------------------------------------------------- /rokit.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | rojo = "rojo-rbx/rojo@7.5.1" 3 | -------------------------------------------------------------------------------- /branding/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ffrostfall/ByteNet/HEAD/branding/favicon.png -------------------------------------------------------------------------------- /branding/bytenetLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ffrostfall/ByteNet/HEAD/branding/bytenetLogo.png -------------------------------------------------------------------------------- /default.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "tree": { 3 | "$path": "src" 4 | }, 5 | "name": "bytenet" 6 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "JohnnyMorganz.stylua", 4 | "JohnnyMorganz.luau-lsp" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Report a bug 3 | about: Report any issues with ByteNet 4 | title: '' 5 | labels: 'type: bug' 6 | --- -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Suggest a feature 3 | about: Request for a feature to be added to ByteNet 4 | title: '' 5 | labels: 'type: feature request' 6 | --- -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Packages 2 | 3 | # Project place file 4 | /ByteNet.rbxlx 5 | 6 | # Roblox Studio lock files 7 | /*.rbxlx.lock 8 | /*.rbxl.lock 9 | 10 | build 11 | node_modules 12 | sourcemap.json 13 | site -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/improvement.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Suggest an improvement 3 | about: Request for an improvement (Optimization, docs, API change, etc.) for ByteNet 4 | title: '' 5 | labels: 'type: enhancement' 6 | --- -------------------------------------------------------------------------------- /stylua.toml: -------------------------------------------------------------------------------- 1 | column_width = 100 2 | line_endings = "Unix" 3 | indent_type = "Tabs" 4 | indent_width = 5 5 | quote_style = "AutoPreferDouble" 6 | call_parentheses = "Input" 7 | 8 | [sort_requires] 9 | enabled = true 10 | -------------------------------------------------------------------------------- /src/process/channel_state.luau: -------------------------------------------------------------------------------- 1 | local types = require("../types") 2 | 3 | local channel_state: types.ChannelState = { 4 | buff = buffer.create(0), 5 | ref_map = {}, 6 | cursor = 0, 7 | } 8 | 9 | return channel_state 10 | -------------------------------------------------------------------------------- /src/data_types/nothing.luau: -------------------------------------------------------------------------------- 1 | local types = require("../types") 2 | 3 | local nothing = { 4 | write = @native function() end, 5 | 6 | read = @native function() 7 | return nil, 0 8 | end, 9 | } 10 | 11 | return @native function(): types.DataType 12 | return nothing 13 | end 14 | -------------------------------------------------------------------------------- /src/data_types/int8.luau: -------------------------------------------------------------------------------- 1 | local types = require("../types") 2 | 3 | local int8 = { 4 | write = writei8, 5 | 6 | read = @native function(b: buffer, cursor: number) 7 | return buffer.readi8(b, cursor), 1 8 | end, 9 | } 10 | 11 | return @native function(): types.DataType 12 | return int8 13 | end 14 | -------------------------------------------------------------------------------- /src/data_types/uint8.luau: -------------------------------------------------------------------------------- 1 | local types = require("../types") 2 | 3 | local uint8 = { 4 | write = writeu8, 5 | 6 | read = @native function(b: buffer, cursor: number) 7 | return buffer.readu8(b, cursor), 1 8 | end, 9 | } 10 | 11 | return @native function(): types.DataType 12 | return uint8 13 | end 14 | -------------------------------------------------------------------------------- /src/data_types/int16.luau: -------------------------------------------------------------------------------- 1 | local types = require("../types") 2 | 3 | local int16 = { 4 | write = writei16, 5 | 6 | read = @native function(b: buffer, cursor: number) 7 | return buffer.readi16(b, cursor), 2 8 | end, 9 | } 10 | 11 | return @native function(): types.DataType 12 | return int16 13 | end 14 | -------------------------------------------------------------------------------- /src/data_types/int32.luau: -------------------------------------------------------------------------------- 1 | local types = require("../types") 2 | 3 | local int32 = { 4 | write = writei32, 5 | 6 | read = @native function(b: buffer, cursor: number) 7 | return buffer.readi32(b, cursor), 4 8 | end, 9 | } 10 | 11 | return @native function(): types.DataType 12 | return int32 13 | end 14 | -------------------------------------------------------------------------------- /src/data_types/uint16.luau: -------------------------------------------------------------------------------- 1 | local types = require("../types") 2 | 3 | local uint16 = { 4 | write = writeu16, 5 | 6 | read = @native function(b: buffer, cursor: number) 7 | return buffer.readu16(b, cursor), 2 8 | end, 9 | } 10 | 11 | return @native function(): types.DataType 12 | return uint16 13 | end 14 | -------------------------------------------------------------------------------- /src/data_types/uint32.luau: -------------------------------------------------------------------------------- 1 | local types = require("../types") 2 | 3 | local uint32 = { 4 | write = writeu32, 5 | 6 | read = @native function(b: buffer, cursor: number) 7 | return buffer.readu32(b, cursor), 4 8 | end, 9 | } 10 | 11 | return @native function(): types.DataType 12 | return uint32 13 | end 14 | -------------------------------------------------------------------------------- /src/data_types/float32.luau: -------------------------------------------------------------------------------- 1 | local types = require("../types") 2 | 3 | local float32: types.DataType = { 4 | __T = ... :: number, 5 | 6 | write = f32, 7 | 8 | read = @native function(b: buffer, cursor: number) 9 | return buffer.readf32(b, cursor), 4 10 | end, 11 | } 12 | 13 | return @native function(): types.DataType 14 | return float32 15 | end 16 | -------------------------------------------------------------------------------- /src/data_types/float64.luau: -------------------------------------------------------------------------------- 1 | local types = require("../types") 2 | 3 | local float64: types.DataType = { 4 | __T = ... :: number, 5 | 6 | write = writef64, 7 | 8 | read = @native function(b: buffer, cursor: number) 9 | return buffer.readf64(b, cursor), 8 10 | end, 11 | } 12 | 13 | return @native function(): types.DataType 14 | return float64 15 | end 16 | -------------------------------------------------------------------------------- /wally.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ffrostfall/bytenet" 3 | version = "0.5.0" 4 | registry = "https://github.com/UpliftGames/wally-index" 5 | realm = "shared" 6 | exclude = [ 7 | "dev", 8 | ".eslintrc", 9 | ".gitattributes", 10 | ".gitignore", 11 | ".prettierrc", 12 | "dev.project.json", 13 | "package.json", 14 | "mkdocs.yml", 15 | "stylua.toml", 16 | "tsconfig.json", 17 | "style.md", 18 | "CONTRIBUTING.md", 19 | ] 20 | -------------------------------------------------------------------------------- /src/packets/define_packet.luau: -------------------------------------------------------------------------------- 1 | --!native 2 | --!optimize 2 3 | 4 | --[[ 5 | Exists so that the namespace can set the ID of the packet 6 | 7 | Packet shouldnt need to care about making its own ID 8 | ]] 9 | local packet = require("./packet") 10 | local types = require("../types") 11 | 12 | @native 13 | local function define_packet(props: types.PacketProps) 14 | return @native function(id: number) 15 | return packet(props, id) 16 | end 17 | end 18 | 19 | return define_packet 20 | -------------------------------------------------------------------------------- /src/data_types/vec2.luau: -------------------------------------------------------------------------------- 1 | local types = require("../types") 2 | 3 | local vec2 = { 4 | --[[ 5 | 2 float32s, one for X, one for Y 6 | ]] 7 | 8 | read = @native function(b: buffer, cursor: number) 9 | return Vector2.new(buffer.readf32(b, cursor), buffer.readf32(b, cursor + 4)), 8 10 | end, 11 | 12 | write = @native function(value: Vector2) 13 | buffer.writef32(0, 0, value.X) 14 | buffer.writef32(0, 0, value.Y) 15 | end, 16 | } 17 | 18 | return @native function(): types.DataType 19 | return vec2 20 | end 21 | -------------------------------------------------------------------------------- /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 = "ffrostflame/bytenet" 7 | version = "0.3.0-dev" 8 | dependencies = [["TableKit", "ffrostflame/tablekit@0.2.4"], ["wallyInstanceManager", "ffrostflame/wally-instance-manager@0.1.0"]] 9 | 10 | [[package]] 11 | name = "ffrostflame/tablekit" 12 | version = "0.2.4" 13 | dependencies = [] 14 | 15 | [[package]] 16 | name = "ffrostflame/wally-instance-manager" 17 | version = "0.1.0" 18 | dependencies = [] 19 | -------------------------------------------------------------------------------- /src/data_types/unknown.luau: -------------------------------------------------------------------------------- 1 | local read_refs = require("../process/readRefs") 2 | local types = require("../types") 3 | 4 | @native 5 | local function unknown(): types.DataType 6 | return { 7 | write = @native function(value: unknown) 8 | alloc(1) 9 | writeReference(value) 10 | end, 11 | 12 | read = @native function(b: buffer, cursor: number) 13 | local refs = read_refs.get() 14 | 15 | if not refs then 16 | return nil, 1 17 | end 18 | 19 | return refs[buffer.readu8(b, cursor)], 1 20 | end, 21 | } 22 | end 23 | 24 | return unknown 25 | -------------------------------------------------------------------------------- /src/data_types/string.luau: -------------------------------------------------------------------------------- 1 | local types = require("../types") 2 | 3 | local str = { 4 | -- 2 bytes for the length, then the string 5 | 6 | read = @native function(b: buffer, cursor: number) 7 | local length = buffer.readu16(b, cursor) 8 | 9 | return buffer.readstring(b, cursor + 2, length), length + 2 10 | end, 11 | write = @native function(data: string) 12 | local length = string.len(data) 13 | writeu16(length) 14 | 15 | dyn_alloc(length) 16 | writestring(data) 17 | end, 18 | } 19 | 20 | return @native function(): types.DataType 21 | return str 22 | end 23 | -------------------------------------------------------------------------------- /src/data_types/inst.luau: -------------------------------------------------------------------------------- 1 | local types = require("../types") 2 | 3 | return @native function(): types.DataType 4 | return { 5 | write = @native function(value) 6 | alloc(1) 7 | writeReference(value) 8 | end, 9 | 10 | read = @native function(b: buffer, cursor: number) 11 | local refs = readRefs.get() 12 | 13 | if not refs then 14 | return nil, 1 15 | end 16 | 17 | local ref = refs[buffer.readu8(b, cursor)] 18 | 19 | if typeof(ref) == "Instance" then 20 | return ref, 1 21 | else 22 | return nil, 1 23 | end 24 | end, 25 | } 26 | end 27 | -------------------------------------------------------------------------------- /src/data_types/vec3.luau: -------------------------------------------------------------------------------- 1 | local types = require("../types") 2 | 3 | local vec3 = { 4 | __T = ... :: vector, 5 | 6 | read = @native function(b: buffer, cursor: number) 7 | return vector.create( 8 | buffer.readf32(b, cursor), 9 | buffer.readf32(b, cursor + 4), 10 | buffer.readf32(b, cursor + 8) 11 | ), 12 | 12 13 | end, 14 | 15 | write = @native function(value: vector) 16 | buffer.writef32(value.x) 17 | buffer.writef32(value.y) 18 | buffer.writef32(value.z) 19 | end, 20 | 21 | length = 12, 22 | } 23 | 24 | return @native function(): types.DataType 25 | return vec3 26 | end 27 | -------------------------------------------------------------------------------- /src/data_types/color3.luau: -------------------------------------------------------------------------------- 1 | local types = require("../types") 2 | 3 | local color3 = { 4 | __T = ... :: Color3, 5 | 6 | write = @native function(input: Color3) 7 | alloc(3) 8 | uint8NoAlloc(input.R * 255) 9 | uint8NoAlloc(input.G * 255) 10 | uint8NoAlloc(input.B * 255) 11 | end, 12 | 13 | read = @native function(b: buffer, cursor: number) 14 | return Color3.fromRGB( 15 | buffer.readu8(b, cursor), 16 | buffer.readu8(b, cursor + 1), 17 | buffer.readu8(b, cursor + 2) 18 | ), 19 | 3 20 | end, 21 | 22 | length = 3, 23 | } 24 | 25 | return @native function(): types.DataType 26 | return color3 27 | end 28 | -------------------------------------------------------------------------------- /src/data_types/bool.luau: -------------------------------------------------------------------------------- 1 | local types = require("../types") 2 | 3 | local bool_modifier = { 4 | [1] = true, 5 | [0] = false, 6 | } 7 | local bool_modifier2 = { 8 | [true] = 1, 9 | [false] = 0, 10 | } 11 | 12 | local bool_data_type = { 13 | __T = (nil :: any) :: boolean, 14 | 15 | --[[ 16 | 1 = true 17 | 0 = false 18 | 19 | Write and read based off a uint8 20 | ]] 21 | read = @native function(b: buffer, cursor: number) 22 | return bool_modifier[buffer.readu8(b, cursor)], 1 23 | end, 24 | 25 | length = 1, 26 | 27 | write = @native function(value) 28 | buffer.writeu8(bool_modifier2[value]) 29 | end, 30 | } 31 | 32 | @native 33 | local function bool(): types.DataType 34 | return bool_data_type 35 | end 36 | 37 | return bool 38 | -------------------------------------------------------------------------------- /src/data_types/buff.luau: -------------------------------------------------------------------------------- 1 | local types = require("../types") 2 | 3 | local buff = { 4 | read = @native function(b: buffer, cursor: number) 5 | local length = buffer.readu16(b, cursor) 6 | local freshBuffer = buffer.create(length) 7 | 8 | -- copy the data from the main buffer to the new buffer with an offset of 2 because of length 9 | buffer.copy(freshBuffer, 0, b, cursor + 2, length) 10 | 11 | return freshBuffer, length + 2 12 | end, 13 | write = @native function(data: buffer) 14 | local length = buffer.len(data) 15 | writeu16(length) 16 | 17 | dyn_alloc(length) 18 | 19 | -- write the length of the buffer, then the buffer itself 20 | writecopy(data) 21 | end, 22 | } 23 | 24 | return @native function(): types.DataType 25 | return buff 26 | end 27 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[luau]": { 3 | "editor.formatOnSave": true, 4 | "editor.defaultFormatter": "JohnnyMorganz.stylua" 5 | }, 6 | 7 | "stylua.targetReleaseVersion": "latest", 8 | 9 | "luau-lsp.fflags.enableNewSolver": true, 10 | "luau-lsp.completion.imports.stringRequires.enabled": true, 11 | 12 | // tabs over spaces 13 | "editor.detectIndentation": false, 14 | "editor.insertSpaces": false, 15 | "prettier.useTabs": true, 16 | "files.eol": "\n", 17 | 18 | "luau-lsp.sourcemap.enabled": true, 19 | "luau-lsp.sourcemap.autogenerate": false, 20 | "luau-lsp.sourcemap.sourcemapFile": "sourcemap.json", 21 | 22 | "luau-lsp.completion.imports.ignoreGlobs": [ 23 | "**/_Index/**", 24 | "**/mirrors/**" 25 | ], 26 | 27 | "luau-lsp.ignoreGlobs": [ 28 | "**/_Index/**", 29 | "build/**", 30 | "vendor/**" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /src/data_types/optional.luau: -------------------------------------------------------------------------------- 1 | local types = require("../types") 2 | 3 | @native 4 | local function optional(value_type: types.DataType): types.DataType 5 | local value_read = value_type.read 6 | local value_write = value_type.write 7 | 8 | return { 9 | --[[ 10 | first byte is a boolean, if it's true, the next bytes are the value of valueType 11 | if it's false, its length of 1 cuz only 1 boolean 12 | ]] 13 | 14 | read = @native function(b: buffer, cursor: number) 15 | if buffer.readu8(b, cursor) == 0 then 16 | -- doesn't exist 17 | return nil, 1 18 | else 19 | -- exists, read the value 20 | local item, length = value_read(b, cursor + 1) 21 | return item, length + 1 22 | end 23 | end, 24 | 25 | write = @native function(value: any) 26 | local exists = value ~= nil 27 | 28 | writebool(exists) 29 | 30 | if exists then 31 | value_write(value) 32 | end 33 | end, 34 | } 35 | end 36 | 37 | return optional 38 | -------------------------------------------------------------------------------- /src/types.luau: -------------------------------------------------------------------------------- 1 | export type ChannelState = { 2 | buff: buffer, 3 | cursor: number, 4 | ref_map: { [number]: unknown }, 5 | } 6 | 7 | export type PacketProps = { 8 | value: T, 9 | 10 | reliability: ("Reliable" | "Unreliable")?, 11 | 12 | max_per_frame: number?, 13 | } 14 | 15 | export type DataType = { 16 | read __T: T, 17 | 18 | write: (value: T) -> (), 19 | read: (b: buffer, cursor: number) -> (T, number), 20 | 21 | read length: number?, 22 | } 23 | 24 | type Packet = { 25 | send_to_all: (data: T) -> (), 26 | send_to: (data: T, target: Player) -> (), 27 | send_to_list: (data: T, targets: { Player }) -> (), 28 | send_to_all_except: (data: T, exception: Player) -> (), 29 | 30 | wait: () -> T, 31 | send: (data: T, target: Player?) -> (), 32 | listen: (callback: (data: T, player: Player?) -> ()) -> (), 33 | } 34 | 35 | export type function ValueFromDataType(data_type: type) 36 | return data_type:readproperty(types.singleton("__T")) 37 | end 38 | 39 | return nil 40 | -------------------------------------------------------------------------------- /src/data_types/cframe.luau: -------------------------------------------------------------------------------- 1 | local types = require("../types") 2 | 3 | local cframe: types.DataType = { 4 | __T = ... :: CFrame, 5 | 6 | read = @native function(b: buffer, cursor: number) 7 | local x = buffer.readf32(b, cursor) 8 | local y = buffer.readf32(b, cursor + 4) 9 | local z = buffer.readf32(b, cursor + 8) 10 | local rx = buffer.readf32(b, cursor + 12) 11 | local ry = buffer.readf32(b, cursor + 16) 12 | local rz = buffer.readf32(b, cursor + 20) 13 | 14 | return CFrame.new(x, y, z) * CFrame.Angles(rx, ry, rz), 24 15 | end, 16 | write = @native function(value: CFrame) 17 | local x, y, z = value.X, value.Y, value.Z 18 | local rx, ry, rz = value:ToEulerAnglesXYZ() 19 | 20 | -- Math done, write it now 21 | alloc(24) 22 | writef32NoAlloc(x) 23 | writef32NoAlloc(y) 24 | writef32NoAlloc(z) 25 | writef32NoAlloc(rx) 26 | writef32NoAlloc(ry) 27 | writef32NoAlloc(rz) 28 | end, 29 | } 30 | 31 | return @native function(): types.DataType 32 | return cframe 33 | end 34 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/data_types/array.luau: -------------------------------------------------------------------------------- 1 | local channel_state = require("../process/channel_state") 2 | local types = require("../types") 3 | 4 | --[[ 5 | Create a new array with the given dataTypeInterface 6 | ]] 7 | @native 8 | local function array(value_ty: types.DataType): types.DataType<{ T }> 9 | local val_write = value_ty.write 10 | local val_read = value_ty.read 11 | 12 | return { 13 | __T = nil :: T, 14 | 15 | read = @native function(buff: buffer, cursor: number) 16 | local array_length = buffer.readu16(buff, cursor) 17 | local array_cursor = cursor + 2 18 | 19 | local array = table.create(array_length) 20 | 21 | for i = 1, array_length do 22 | local item, length = val_read(buff, array_cursor) 23 | array[i] = item 24 | 25 | array_cursor += length 26 | end 27 | 28 | return array, array_cursor - cursor 29 | end, 30 | write = @native function(value: any) 31 | local length = #value 32 | buffer.writeu16(channel_state.buff, channel_state.cursor, length) 33 | 34 | -- numeric iteration is about 2x faster than generic iteration 35 | for i = 1, length do 36 | val_write(value[i]) 37 | end 38 | end, 39 | } 40 | end 41 | 42 | return array 43 | -------------------------------------------------------------------------------- /src/process/read.luau: -------------------------------------------------------------------------------- 1 | --!optimize 2 2 | --!native 3 | local packetIDs = require("../a") 4 | local readRefs = require("./readRefs") 5 | 6 | local ref = packetIDs.ref() 7 | local free_thread: thread? 8 | 9 | @native 10 | local function function_passer(fn, ...) 11 | local aquiredThread = free_thread 12 | free_thread = nil 13 | fn(...) 14 | free_thread = aquiredThread 15 | end 16 | 17 | @native 18 | local function yielder() 19 | while true do 20 | function_passer(coroutine.yield()) 21 | end 22 | end 23 | 24 | @native 25 | local function run_listener(fn, ...) 26 | free_thread = task.spawn(free_thread or task.spawn(coroutine.create(yielder)), fn, ...) 27 | end 28 | 29 | @native 30 | local function read(incoming_buff: buffer, references: { [number]: unknown }?, player: Player?) 31 | local length = buffer.len(incoming_buff) 32 | local read_cursor = 0 33 | 34 | readRefs.set(references) 35 | 36 | while read_cursor < length do 37 | local packet = ref[buffer.readu8(incoming_buff, read_cursor)] 38 | read_cursor += 1 39 | 40 | local value, value_len = packet.reader(incoming_buff, read_cursor) 41 | 42 | read_cursor += value_len 43 | 44 | for _, listener in packet.getListeners() do 45 | run_listener(listener, value, player) 46 | end 47 | end 48 | end 49 | 50 | return read 51 | -------------------------------------------------------------------------------- /src/data_types/struct.luau: -------------------------------------------------------------------------------- 1 | local types = require("../types") 2 | 3 | type structData = { 4 | [string]: number, 5 | } 6 | 7 | return function(input: T & { 8 | [string]: types.DataType, 9 | }): types.DataType 10 | local index_value_type_pairs: { 11 | [number]: types.DataType, 12 | } = {} 13 | local index_key_pairs: { [number]: string } = {} 14 | 15 | local count = 0 16 | for key in input :: any do 17 | count += 1 18 | 19 | -- Store the index-value pairs and the index-key pairs as a shortcut for serializing n all that 20 | index_value_type_pairs[count] = input[key] 21 | index_key_pairs[count] = key 22 | end 23 | 24 | return { 25 | __T = nil :: T, 26 | 27 | read = @native function(b, cursor) 28 | local constructed = table.clone(input) 29 | local struct_cursor = cursor 30 | 31 | for index, value_type in index_value_type_pairs do 32 | local value, length = value_type.read(b, struct_cursor) 33 | 34 | constructed[index_key_pairs[index]] = value 35 | 36 | struct_cursor += length 37 | end 38 | 39 | return constructed, struct_cursor - cursor 40 | end, 41 | 42 | write = @native function(struct_value) 43 | for index, value_type in index_value_type_pairs do 44 | value_type.write(struct_value[index_key_pairs[index]]) 45 | end 46 | end, 47 | } 48 | end 49 | -------------------------------------------------------------------------------- /src/data_types/map.luau: -------------------------------------------------------------------------------- 1 | local types = require("../types") 2 | 3 | @native 4 | local function map( 5 | keyType: types.DataType, 6 | valueType: types.DataType 7 | ): types.DataType<{ [any]: any }> 8 | -- Cache these functions to avoid the overhead of the index 9 | local key_write = keyType.write 10 | local value_write = valueType.write 11 | 12 | return { 13 | read = @native function(b: buffer, cursor: number) 14 | local map = {} 15 | local mapCursor = cursor 16 | 17 | -- Read map length 18 | local mapLength = buffer.readu16(b, mapCursor) 19 | mapCursor += 2 20 | 21 | for _ = 1, mapLength do 22 | -- read key/value pairs and add them to the map 23 | local key, keyLength = keyType.read(b, mapCursor) 24 | mapCursor += keyLength 25 | 26 | local value, valueLength = valueType.read(b, mapCursor) 27 | mapCursor += valueLength 28 | 29 | map[key] = value 30 | end 31 | 32 | -- Return the map, alongside length, because mapCursor - cursor = size 33 | return map, mapCursor - cursor 34 | end, 35 | write = @native function(map: any) 36 | local count = 0 37 | for _ in map do 38 | count += 1 39 | end 40 | 41 | -- Write length 42 | writeu16(count) 43 | 44 | for k, v in map do 45 | -- Write key/value pairs 46 | key_write(k) 47 | value_write(v) 48 | end 49 | end, 50 | } 51 | end 52 | 53 | return map 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | # ByteNet 6 | 7 | ## Simple, buffer-based networking. 8 | 9 | [GitHub](https://github.com/ffrostfall/ByteNet) | [Documentation](https://ffrostfall.github.io/ByteNet/) 10 | 11 |
12 | 13 | ByteNet is an networking library which takes your Luau data, and serializes it into buffers. On the other end, ByteNet deserializes your data, and then feeds it back to your Luau code. You don't need to worry about type validation, optimization, packet structure, etc. ByteNet does all the hard parts for you! Strictly typed with an incredibly basic API that explains itself, ByteNet makes networking simple, easy, and quick. There's very few concepts you need to grasp in order to use ByteNet; it has an incredibly minimalistic & simplistic, yet powerful API. 14 | 15 | ## Installation 16 | 17 | You can install ByteNet on Wally, or through the latest release's `.rbxm` file. 18 | 19 | ## Performance 20 | 21 | ByteNet performs incredibly well compared to non-buffer based libraries like BridgeNet2. This is because ByteNet has a **custom serializer** that takes your Luau data and transforms it into a buffer, sending that and deserializing it on the other side. 22 | 23 | ## Further contact 24 | 25 | You can contact me directly under the ByteNet thread in the [Roblox OSS Server](https://discord.gg/5KjV64PA3d). 26 | 27 | Further documentation [here](https://ffrostfall.github.io/ByteNet/). 28 | 29 | ## License 30 | 31 | This project is under the MIT license! so, it's open source 32 | -------------------------------------------------------------------------------- /src/init.luau: -------------------------------------------------------------------------------- 1 | local RunService = game:GetService("RunService") 2 | 3 | local array = require("@self/data_types/array") 4 | local bool = require("@self/data_types/bool") 5 | local buff = require("@self/data_types/buff") 6 | local cframe = require("@self/data_types/cframe") 7 | local client = require("@self/process/client") 8 | local define_packet = require("@self/packets/define_packet") 9 | local float32 = require("@self/data_types/float32") 10 | local float64 = require("@self/data_types/float64") 11 | local inst = require("@self/data_types/inst") 12 | local int16 = require("@self/data_types/int16") 13 | local int32 = require("@self/data_types/int32") 14 | local int8 = require("@self/data_types/int8") 15 | local map = require("@self/data_types/map") 16 | local nothing = require("@self/data_types/nothing") 17 | local optional = require("@self/data_types/optional") 18 | local server = require("@self/process/server") 19 | local string = require("@self/data_types/string") 20 | local struct = require("@self/data_types/struct") 21 | local uint16 = require("@self/data_types/uint16") 22 | local uint32 = require("@self/data_types/uint32") 23 | local uint8 = require("@self/data_types/uint8") 24 | local unknown = require("@self/data_types/unknown") 25 | local vec2 = require("@self/data_types/vec2") 26 | local vec3 = require("@self/data_types/vec3") 27 | 28 | if RunService:IsServer() then 29 | server.start() 30 | else 31 | client.init() 32 | end 33 | 34 | return table.freeze({ 35 | define_packet = define_packet, 36 | 37 | t = { 38 | array = array, 39 | bool = bool(), 40 | optional = optional, 41 | uint8 = uint8(), 42 | uint16 = uint16(), 43 | uint32 = uint32(), 44 | int8 = int8(), 45 | int16 = int16(), 46 | int32 = int32(), 47 | float32 = float32(), 48 | float64 = float64(), 49 | cframe = cframe(), 50 | string = string(), 51 | vec2 = vec2(), 52 | vec3 = vec3(), 53 | buff = buff(), 54 | struct = struct, 55 | map = map, 56 | inst = inst(), 57 | unknown = unknown(), 58 | nothing = nothing(), 59 | }, 60 | }) 61 | -------------------------------------------------------------------------------- /branding/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - toc 4 | - navigation 5 | --- 6 | 7 | 71 | -------------------------------------------------------------------------------- /src/process/client.luau: -------------------------------------------------------------------------------- 1 | local ReplicatedStorage = game:GetService("ReplicatedStorage") 2 | local RunService = game:GetService("RunService") 3 | 4 | local read = require("./read") 5 | local types = require("../types") 6 | 7 | @native 8 | local function onClientEvent(receivedBuffer, ref) 9 | read(receivedBuffer, ref) 10 | end 11 | 12 | -- Shared with: src/process/server.luau (Infeasible to split this into another file) 13 | @native 14 | local function create(): types.ChannelState 15 | return { 16 | cursor = 0, 17 | size = 256, 18 | references = {}, 19 | buff = buffer.create(256), 20 | } 21 | end 22 | 23 | @native 24 | local function dump(channel: types.ChannelState): (buffer, { unknown }?) 25 | local cursor = channel.cursor 26 | local dump_buffer = buffer.create(cursor) 27 | 28 | buffer.copy(dump_buffer, 0, channel.buff, 0, cursor) 29 | 30 | return dump_buffer, if #channel.references > 0 then channel.references else nil 31 | end 32 | -- No longer shared 33 | 34 | local reliable: types.ChannelState = create() 35 | local unreliable: types.ChannelState = create() 36 | 37 | local client = {} 38 | 39 | function client.send_reliable(id: number, data: T) 40 | reliable = writePacket(reliable, id, writer, data) 41 | end 42 | 43 | function client.send_unreliable(id: number, writer: (value: any) -> (), data: { [string]: any }) 44 | unreliable = writePacket(unreliable, id, writer, data) 45 | end 46 | 47 | function client.init() 48 | local reliableRemote = ReplicatedStorage:WaitForChild("ByteNetReliable") 49 | reliableRemote.OnClientEvent:Connect(onClientEvent) 50 | 51 | local unreliableRemote = ReplicatedStorage:WaitForChild("ByteNetUnreliable") 52 | unreliableRemote.OnClientEvent:Connect(onClientEvent) 53 | 54 | RunService.Heartbeat:Connect(function() 55 | -- Again, checking if there's anything in the channel before we send it. 56 | if reliable.cursor > 0 then 57 | local b, r = dump(reliable) 58 | reliableRemote:FireServer(b, r) 59 | 60 | -- effectively clears the channel 61 | reliable.cursor = 0 62 | table.clear(reliable.references) 63 | end 64 | 65 | if unreliable.cursor > 0 then 66 | local b, r = dump(unreliable) 67 | unreliableRemote:FireServer(b, r) 68 | 69 | unreliable.cursor = 0 70 | table.clear(unreliable.references) 71 | end 72 | end) 73 | end 74 | 75 | return client 76 | -------------------------------------------------------------------------------- /src/packets/packet.luau: -------------------------------------------------------------------------------- 1 | --!native 2 | --!optimize 2 3 | local Players = game:GetService("Players") 4 | local RunService = game:GetService("RunService") 5 | 6 | local types = require("../types") 7 | local client = require("../process/client") 8 | local server = require("../process/server") 9 | 10 | local run_context: "server" | "client" = if RunService:IsServer() then "server" else "client" 11 | 12 | --[[ 13 | We use closures here instead of metatables for performance 14 | It's just faster to use closures than metatables 15 | ]] 16 | return@native function(props: types.PacketProps>, id: number) 17 | -- Basic properties: reliability type, "unique" which is used to get the packet ID, and set up listeners 18 | local reliabilityType = props.reliability or "reliable" 19 | local listeners = {} 20 | 21 | local serverSendFunction: (player: Player, id: number, writer: (value: any) -> (), data: any) -> () = if reliabilityType 22 | == "reliable" 23 | then server.sendPlayerReliable 24 | else server.sendPlayerUnreliable 25 | 26 | local serverSendAllFunction: (id: number, writer: (value: any) -> (), data: any) -> () = if reliabilityType 27 | == "reliable" 28 | then server.sendAllReliable 29 | else server.sendAllUnreliable 30 | 31 | local clientSendFunction: (id: number, writer: (value: any) -> (), data: any) -> () = if reliabilityType 32 | == "reliable" 33 | then client.send_reliable 34 | else client.send_unreliable 35 | 36 | -- shorcut to avoid indexxing 37 | local writer = props.value.write 38 | 39 | local exported = {} 40 | 41 | -- RunContext error checking that doesn't have performance drawbacks 42 | setmetatable(exported, { 43 | __index =@native function(index) 44 | if 45 | (index == "sendTo" or index == "sendToAllExcept" or index == "sendToAll") 46 | and run_context == "client" 47 | then 48 | error("You cannot use sendTo, sendToAllExcept, or sendToAll on the client") 49 | elseif index == "send" and run_context == "server" then 50 | error("You cannot use send on the server") 51 | end 52 | end, 53 | }) 54 | 55 | -- exposed for the reader file 56 | exported.reader = props.value.read 57 | 58 | if run_context == "server" then 59 | function exported.sendToList(data, players: { Player }) 60 | for _, player in players do 61 | serverSendFunction(player, id, writer, data) 62 | end 63 | end 64 | 65 | function exported.sendTo(data, player: Player) 66 | serverSendFunction(player, id, writer, data) 67 | end 68 | 69 | function exported.sendToAllExcept(data, except: Player) 70 | for _, player: Player in Players:GetPlayers() do 71 | if player ~= except then 72 | serverSendFunction(player, id, writer, data) 73 | end 74 | end 75 | end 76 | 77 | function exported.sendToAll(data) 78 | serverSendAllFunction(id, writer, data) 79 | end 80 | elseif run_context == "client" then 81 | function exported.send(data) 82 | clientSendFunction(id, writer, data) 83 | end 84 | end 85 | 86 | function exported.wait() 87 | -- define it up here so we can use it to disconnect 88 | local index: number 89 | 90 | local runningThread = coroutine.running() 91 | table.insert(listeners,@native function(data, player) 92 | task.spawn(runningThread, data, player) 93 | 94 | -- Disconnects the listener 95 | table.remove(listeners, index) 96 | end) 97 | 98 | -- we connected, time to set the index for when we need to disconnect. 99 | index = #listeners 100 | 101 | -- the listener will resume the thread 102 | return coroutine.yield() 103 | end 104 | 105 | function exported.listen(callback) 106 | table.insert(listeners, callback) 107 | end 108 | 109 | function exported.getListeners() 110 | return listeners 111 | end 112 | 113 | return exported 114 | end 115 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # ByteNet Changelog 2 | 3 | ByteNet uses [semantic versioning](https://semver.org/spec/v2.0.0.html). 4 | 5 | --- 6 | 7 | ## 0.4.6 8 | 9 | ### Fixes 10 | 11 | - Fixed Color3 data type by @buildthomas in [#18](https://github.com/ffrostfall/ByteNet/pull/18) 12 | - Fixed CFrame data type by @HooferDevelops in [16](https://github.com/ffrostfall/ByteNet/pull/16) 13 | 14 | ### Changes 15 | 16 | - Uses a growth factor of 2 over `math.round(1.5)` due to performance 17 | 18 | --- 19 | 20 | ## 0.4.4 21 | 22 | ### Fixes 23 | 24 | - Fixed `sendToAllExcept` type 25 | 26 | --- 27 | 28 | ## 0.4.3 29 | 30 | ### Fixes 31 | 32 | - Fixed `i16` allocating 8 bytes instead of 2. 33 | 34 | ### Improvements 35 | 36 | - Significant optimization to optional types 37 | - Array serialization is roughly ~2x as efficient 38 | - All data type write functions now directly reference the buffer writer. This means all allocation calls are inlined, and there are roughly ~3x less function calls. 39 | 40 | --- 41 | 42 | ## 0.4.2 43 | 44 | ### Fixes 45 | 46 | - Fixed sending unreliable events from client -> server 47 | 48 | --- 49 | 50 | ## 0.4.1 51 | 52 | ### Improvements 53 | 54 | - ByteNet now loops through existing players incase the package was initialized late 55 | 56 | ### Fixes 57 | 58 | - Fixed the float64 data type 59 | 60 | --- 61 | 62 | ## 0.4.0 63 | 64 | ### Improvements 65 | 66 | - Arrays are now forced to have number indexes 67 | 68 | ### Fixes 69 | 70 | - Added all of the new data types to the ByteNet type. 71 | - Fixed the client not clearing instance references 72 | - Fixed the client sending a buffer every frame 73 | - Fixed instances/unknowns not being sendable in any special type 74 | 75 | --- 76 | 77 | ## 0.4.0-rc3 78 | 79 | ### Added 80 | 81 | - Namespaces have been added. 82 | - Structs have been added. 83 | - Three new data types: Instance, unknown, and nothing. The "nothing" type is to allow for packets without any contents. 84 | - Added `:wait()` to packets. 85 | 86 | ### Improvements 87 | 88 | - Packets are now based off closures, instead of metatables. This means you now have to use `.` indexxing instead of `:` to call methods. 89 | - You can now have duplicate packet contents 90 | - Packets now take a single value (Which can be a struct) instead of being "special". 91 | - Significant optimization: Packets are now singular values that can be structs, which reduces complexity, thus increasing performance. 92 | 93 | ### Fixes 94 | 95 | - Added `:sendTo()` to the `Packet` type. This fixes autocomplete. 96 | 97 | --- 98 | 99 | ## 0.3.1 100 | 101 | ### Improvements 102 | 103 | - Rewrote serialization to use an allocator w/ resizing instead of using "deferred write" functions. Should be an incredibly large performance boost. 104 | 105 | --- 106 | 107 | ## 0.3.0 108 | 109 | ### Added 110 | 111 | - Types: Vector2, CFrame, Array, Optional, Map 112 | 113 | ### Improvements 114 | 115 | - Rewrote client/server processing. Should drastically improve stability and performance. 116 | - Completely re-did how serialization happens to be a lot more stable, and to allow a lot of room for improvement. 117 | - Many type improvements 118 | - Removed only dependency 119 | 120 | --- 121 | 122 | ## 0.2.1 123 | 124 | ### Fixes 125 | 126 | - Fixed absolute reference that broke the package. oops. 127 | 128 | --- 129 | 130 | ## 0.2.0 131 | 132 | ### Added 133 | 134 | - Added buffer support 135 | 136 | ### Fixes 137 | 138 | - Fixed global queue 139 | 140 | ### Improvements 141 | 142 | - Swapped ordering of packet structure and reliability type 143 | - Reliability type now is an optional argument that defaults to reliable 144 | - Optimization hot path for small amounts of buffers in queue 145 | - Small optimizations 146 | 147 | --- 148 | 149 | ## 0.1.3 150 | 151 | ### Fixes 152 | 153 | - Fixed not all data types having types 154 | 155 | --- 156 | 157 | ## 0.1.2 158 | 159 | ### Fixes 160 | 161 | - Fixed not all data types being accessible 162 | - Fixed absolute references being used instead of relative references, thanks auto-import.. 163 | 164 | --- 165 | 166 | ## 0.1.1 167 | 168 | ### Improvements 169 | 170 | - General code improvements/optimizations across the board. Will get more specific as time goes on 171 | - Added roblox-ts support (have not published to npm yet) 172 | 173 | ### Fixes 174 | 175 | - Fixed a basic spamming vulnerability 176 | 177 | --- 178 | 179 | ## 0.1.0 (Release) 180 | -------------------------------------------------------------------------------- /src/process/server.luau: -------------------------------------------------------------------------------- 1 | --!native 2 | local Players = game:GetService("Players") 3 | local ReplicatedStorage = game:GetService("ReplicatedStorage") 4 | local RunService = game:GetService("RunService") 5 | 6 | local buffer_writer = require("./buffer_writer") 7 | local read = require("./read") 8 | local types = require("../types") 9 | 10 | local write_packet = buffer_writer.write_packet 11 | 12 | -- All ChannelState is set to nil upon being sent which is why these are all optionals 13 | local per_player_reliable: { [Player]: types.ChannelState } = {} 14 | local per_player_unreliable: { [Player]: types.ChannelState } = {} 15 | 16 | -- Shared with: src/process/client.luau (Infeasible to split this into another file) 17 | @native 18 | local function create(): types.ChannelState 19 | return { 20 | cursor = 0, 21 | size = 256, 22 | references = {}, 23 | buff = buffer.create(256), 24 | } 25 | end 26 | 27 | @native 28 | local function dump(channel: types.ChannelState): (buffer, { unknown }?) 29 | local cursor = channel.cursor 30 | local dump_buff = buffer.create(cursor) 31 | 32 | buffer.copy(dump_buff, 0, channel.buff, 0, cursor) 33 | 34 | return dump_buff, if #channel.references > 0 then channel.references else nil 35 | end 36 | -- No longer shared 37 | 38 | local globalReliable: types.ChannelState = create() 39 | local globalUnreliable: types.ChannelState = create() 40 | 41 | -- TODO handle invalid data better 42 | @native 43 | local function on_server_event(player: Player, data, references) 44 | -- Only accept buffer data 45 | if not (typeof(data) == "buffer") then 46 | return 47 | end 48 | 49 | read(data, references, player) 50 | end 51 | 52 | @native 53 | local function player_added(player) 54 | if not per_player_reliable[player] then 55 | per_player_reliable[player] = create() 56 | end 57 | 58 | if not per_player_unreliable[player] then 59 | per_player_unreliable[player] = create() 60 | end 61 | end 62 | 63 | local server = {} 64 | 65 | function server.sendAllReliable(id: number, writer: (value: any) -> (), data: { [string]: any }) 66 | globalReliable = write_packet(globalReliable, id, writer, data) 67 | end 68 | 69 | function server.sendAllUnreliable(id: number, writer: (value: any) -> (), data: { [string]: any }) 70 | globalUnreliable = write_packet(globalUnreliable, id, writer, data) 71 | end 72 | 73 | function server.sendPlayerReliable( 74 | player: Player, 75 | id: number, 76 | writer: (value: any) -> (), 77 | data: { [string]: any } 78 | ) 79 | per_player_reliable[player] = write_packet(per_player_reliable[player], id, writer, data) 80 | end 81 | 82 | function server.sendPlayerUnreliable( 83 | player: Player, 84 | id: number, 85 | writer: (value: any) -> (), 86 | data: { [string]: any } 87 | ) 88 | per_player_unreliable[player] = write_packet(per_player_unreliable[player], id, writer, data) 89 | end 90 | 91 | function server.start() 92 | local reliableRemote = Instance.new("RemoteEvent") 93 | reliableRemote.Name = "ByteNetReliable" 94 | reliableRemote.OnServerEvent:Connect(on_server_event) 95 | reliableRemote.Parent = ReplicatedStorage 96 | 97 | local unreliableRemote = Instance.new("UnreliableRemoteEvent") 98 | unreliableRemote.Name = "ByteNetUnreliable" 99 | unreliableRemote.OnServerEvent:Connect(on_server_event) 100 | unreliableRemote.Parent = ReplicatedStorage 101 | 102 | for _, player in Players:GetPlayers() do 103 | player_added(player) 104 | end 105 | 106 | Players.PlayerAdded:Connect(player_added) 107 | 108 | RunService.Heartbeat:Connect(function() 109 | -- Check if the channel has anything before trying to send it 110 | if globalReliable.cursor > 0 then 111 | local b, r = dump(globalReliable) 112 | reliableRemote:FireAllClients(b, r) 113 | 114 | globalReliable.cursor = 0 115 | table.clear(globalReliable.references) 116 | end 117 | 118 | if globalUnreliable.cursor > 0 then 119 | local b, r = dump(globalUnreliable) 120 | unreliableRemote:FireAllClients(b, r) 121 | 122 | globalUnreliable.cursor = 0 123 | table.clear(globalUnreliable.references) 124 | end 125 | 126 | for _, player in Players:GetPlayers() do 127 | if per_player_reliable[player].cursor > 0 then 128 | local b, r = dump(per_player_reliable[player]) 129 | reliableRemote:FireClient(player, b, r) 130 | 131 | per_player_reliable[player].cursor = 0 132 | table.clear(per_player_reliable[player].references) 133 | end 134 | 135 | if per_player_unreliable[player].cursor > 0 then 136 | local b, r = dump(per_player_unreliable[player]) 137 | unreliableRemote:FireClient(player, b, r) 138 | 139 | per_player_unreliable[player].cursor = 0 140 | table.clear(per_player_unreliable[player].references) 141 | end 142 | end 143 | end) 144 | end 145 | 146 | return server 147 | --------------------------------------------------------------------------------