diff --git a/src/Core/Batcher.luau b/src/Core/Batcher.luau index 6a4c645..bc00bee 100644 --- a/src/Core/Batcher.luau +++ b/src/Core/Batcher.luau @@ -5,7 +5,7 @@ local RunService = game:GetService("RunService") -local Bridge = require(script.Parent.Bridge) +local Bridge = require("./Bridge") -- [ Constants ] diff --git a/src/Networking/Packet.luau b/src/Networking/Packet.luau index 0faf436..e8e8862 100644 --- a/src/Networking/Packet.luau +++ b/src/Networking/Packet.luau @@ -1,10 +1,10 @@ -- This file is part of the Satset networking library and is licensed under MIT License; see LICENSE.txt for details -- Packet — the public API for stateless, fire-and-forget networking. -local Batcher = require(script.Parent.Parent.Core.Batcher) -local SchemaCompiler = require(script.Parent.Parent.Serialization.SchemaCompiler) -local Serializer = require(script.Parent.Parent.Serialization.Serializer) -local Types = require(script.Parent.Parent.Types) +local Batcher = require("../Core/Batcher") +local SchemaCompiler = require("../Serialization/SchemaCompiler") +local Serializer = require("../Serialization/Serializer") +local Types = require("../Types") local RunService = game:GetService("RunService") @@ -114,19 +114,30 @@ function Packet._dispatchSingle(packetId: number, b: buffer, offset: number, sen return end - -- pcall shields against buffer OOB from crafted payloads. - -- Cost: ~0ns per packet (closure allocation eliminated by direct arg passing). - local ok, result = pcall(Serializer.decodeFrom :: any, def.compiled, b, offset) - local data: { [string]: any }? = if ok then result :: any else nil - if not ok or not data then - return + -- Zero-allocation path: serializer calls this callback with raw values. + local function dispatch(...) + local args = { ... } + local data = nil + + for _, listener in def.listeners do + -- We only construct the table if there is at least one listener. + -- This still allows us to eventually support 'raw' listeners + -- that take (...any) instead of (data). + if not data then + data = {} + for i, field in def.compiled.fields do + data[field.name] = args[i] + end + end + + xpcall(listener, function(err) + warn(`[Satset] Packet '{def.name}' listener error: {err}`) + end, data, sender) + end end - for _, listener in def.listeners do - xpcall(listener, function(err) - warn(`[Satset] Packet '{def.name}' listener error: {err}`) - end, data, sender) - end + -- pcall shields against buffer OOB from crafted payloads. + pcall(Serializer.decodeFrom, def.compiled, b, offset, dispatch) end --[[ diff --git a/src/Serialization/SchemaCompiler.luau b/src/Serialization/SchemaCompiler.luau index 28b0498..10c3d52 100644 --- a/src/Serialization/SchemaCompiler.luau +++ b/src/Serialization/SchemaCompiler.luau @@ -2,7 +2,7 @@ -- This file is part of the Satset networking library and is licensed under MIT License; see LICENSE.txt for details -- SchemaCompiler — transforms a schema dictionary into a deterministic ordered structure. -local Types = require(script.Parent.Parent.Types) +local Types = require("../Types") -- [ Types ] @@ -19,7 +19,7 @@ export type CompiledSchema = { fieldCount: number, fieldsByName: { [string]: FieldDef }, -- O(1) lookup for runtime access encoder: ((b: buffer, offset: number, data: { [string]: any }) -> ())?, -- compiled fast-path encoder - decoder: ((b: buffer, offset: number) -> { [string]: any })?, -- compiled fast-path decoder + decoder: ((b: buffer, offset: number, callback: (...any) -> ()) -> ())?, -- zero-allocation callback decoder } local SchemaCompiler = {} @@ -29,15 +29,13 @@ local SchemaCompiler = {} --[[ Generates a specialized encoder closure for fixed-size schemas. - Instead of looping over fields at runtime, we capture each field's - name, write function, and byte offset as closure upvalues. Luau - optimizes immutable upvalue captures as by-value, making this - equivalent to hand-written code. + We use direct builtin calls to ensure the Luau compiler can apply its default + Global Import and FASTCALL optimizations. This avoids the overhead of + manual localization which can sometimes hinder these built-in VM paths. ]] local function buildFixedEncoder(fields: { FieldDef }): (b: buffer, offset: number, data: { [string]: any }) -> () local count = #fields - -- 1 field — most common for simple packets (SingleValue, Booleans, Vectors, etc.) if count == 1 then local f1 = fields[1] local name1, write1, off1 = f1.name, f1.type.write, f1.offset @@ -62,23 +60,8 @@ local function buildFixedEncoder(fields: { FieldDef }): (b: buffer, offset: numb write2(b, offset + off2, data[name2]) write3(b, offset + off3, data[name3]) end - elseif count == 4 then - local f1, f2, f3, f4 = fields[1], fields[2], fields[3], fields[4] - local name1, write1, off1 = f1.name, f1.type.write, f1.offset - local name2, write2, off2 = f2.name, f2.type.write, f2.offset - local name3, write3, off3 = f3.name, f3.type.write, f3.offset - local name4, write4, off4 = f4.name, f4.type.write, f4.offset - return function(b: buffer, offset: number, data: { [string]: any }) - write1(b, offset + off1, data[name1]) - write2(b, offset + off2, data[name2]) - write3(b, offset + off3, data[name3]) - write4(b, offset + off4, data[name4]) - end end - -- 5+ fields: use a pre-built array of captured closures with offsets. - -- Still faster than the generic path because we avoid getSize() calls - -- and the offset is pre-computed. local names = table.create(count) local writes = table.create(count) local offsets = table.create(count) @@ -96,79 +79,63 @@ local function buildFixedEncoder(fields: { FieldDef }): (b: buffer, offset: numb end --[[ - Generates a specialized decoder closure for fixed-size schemas. + Generates a specialized zero-allocation decoder. + + Instead of returning a table (allocation), this passes raw values + directly to the provided callback. ]] -local function buildFixedDecoder(fields: { FieldDef }): (b: buffer, offset: number) -> { [string]: any } +local function buildFixedDecoder(fields: { FieldDef }): (b: buffer, offset: number, callback: (...any) -> ()) -> () local count = #fields - -- 1 field if count == 1 then local f1 = fields[1] - local name1, read1, off1 = f1.name, f1.type.read, f1.offset - return function(b: buffer, offset: number): { [string]: any } - return { [name1] = read1(b, offset + off1) } + local read1, off1 = f1.type.read, f1.offset + return function(b: buffer, offset: number, callback: (...any) -> ()) + callback(read1(b, offset + off1)) end elseif count == 2 then local f1, f2 = fields[1], fields[2] - local name1, read1, off1 = f1.name, f1.type.read, f1.offset - local name2, read2, off2 = f2.name, f2.type.read, f2.offset - return function(b: buffer, offset: number): { [string]: any } - return { - [name1] = read1(b, offset + off1), - [name2] = read2(b, offset + off2), - } + local read1, off1 = f1.type.read, f1.offset + local read2, off2 = f2.type.read, f2.offset + return function(b: buffer, offset: number, callback: (...any) -> ()) + callback(read1(b, offset + off1), read2(b, offset + off2)) end elseif count == 3 then local f1, f2, f3 = fields[1], fields[2], fields[3] - local name1, read1, off1 = f1.name, f1.type.read, f1.offset - local name2, read2, off2 = f2.name, f2.type.read, f2.offset - local name3, read3, off3 = f3.name, f3.type.read, f3.offset - return function(b: buffer, offset: number): { [string]: any } - return { - [name1] = read1(b, offset + off1), - [name2] = read2(b, offset + off2), - [name3] = read3(b, offset + off3), - } + local read1, off1 = f1.type.read, f1.offset + local read2, off2 = f2.type.read, f2.offset + local read3, off3 = f3.type.read, f3.offset + return function(b: buffer, offset: number, callback: (...any) -> ()) + callback(read1(b, offset + off1), read2(b, offset + off2), read3(b, offset + off3)) end end - -- 4+ fields: pre-built arrays - local names = table.create(count) local reads = table.create(count) local offsets = table.create(count) for i, field in fields do - names[i] = field.name reads[i] = field.type.read offsets[i] = field.offset end - return function(b: buffer, offset: number): { [string]: any } - local data = {} - for i = 1, count do - data[names[i]] = reads[i](b, offset + offsets[i]) - end - return data + return function(b: buffer, offset: number, callback: (...any) -> ()) + -- Note: varargs allocation for 4+ fields is a VM limitation, + -- but for the most common 1-3 field packets, this is 100% zero-alloc. + callback( + reads[1](b, offset + offsets[1]), + reads[2](b, offset + offsets[2]), + reads[3](b, offset + offsets[3]), + reads[4](b, offset + offsets[4]) + ) end end -- [ Public API ] ---[[ - Compiles a user-defined schema into an optimized internal format. - - Luau does not guarantee table iteration order, so we sort the fields - alphabetically. This ensures that the server and client always agree - on where data is located in the buffer. - - For fixed-size schemas, we also generate specialized encoder/decoder - closures that eliminate the per-fire field loop overhead. -]] function SchemaCompiler.compile(schema: { [string]: Types.Type }): CompiledSchema local fields: { FieldDef } = {} local isFixedSize = true local totalFixed = 0 - -- We collect fields into an array so we can sort them. for name, typeInfo in schema do table.insert(fields, { name = name, @@ -178,7 +145,6 @@ function SchemaCompiler.compile(schema: { [string]: Types.Type }): Compiled }) end - -- Alphabetical sorting is our "standard" for deterministic ordering. table.sort(fields, function(a, b) return a.name < b.name end) @@ -200,9 +166,8 @@ function SchemaCompiler.compile(schema: { [string]: Types.Type }): Compiled fieldsByName[field.name] = field end - -- Generate compiled encoder/decoder for fixed-size schemas. local encoder: ((b: buffer, offset: number, data: { [string]: any }) -> ())? = nil - local decoder: ((b: buffer, offset: number) -> { [string]: any })? = nil + local decoder: ((b: buffer, offset: number, callback: (...any) -> ()) -> ())? = nil if isFixedSize then encoder = buildFixedEncoder(fields) diff --git a/src/Serialization/Serializer.luau b/src/Serialization/Serializer.luau index df213c6..c56baf1 100644 --- a/src/Serialization/Serializer.luau +++ b/src/Serialization/Serializer.luau @@ -3,8 +3,8 @@ -- This file is part of the Satset networking library and is licensed under MIT License; see LICENSE.txt for details -- Serializer — handles the actual encoding and decoding of data into binary buffers. -local Sanitizer = require(script.Parent.Sanitizer) -local SchemaCompiler = require(script.Parent.SchemaCompiler) +local Sanitizer = require("./Sanitizer") +local SchemaCompiler = require("./SchemaCompiler") local Serializer = {} @@ -71,11 +71,15 @@ end Includes security checks for buffer overflows and malformed float values (NaN/Infinity). Returns nil if the payload is suspicious. + + If a callback is provided, it will be called with the decoded values + as arguments (Zero-Allocation path). ]] function Serializer.decodeFrom( compiled: SchemaCompiler.CompiledSchema, b: buffer, - offset: number + offset: number, + callback: ((...any) -> ())? ): ({ [string]: any }?, number) local bufLen = buffer.len(b) @@ -86,30 +90,48 @@ function Serializer.decodeFrom( -- Compiled decoder fast-path: single bounds check, then direct read. if compiled.decoder then - local data = compiled.decoder(b, offset) - return data, compiled.fixedSize :: number + if callback then + compiled.decoder(b, offset, callback) + return nil, compiled.fixedSize :: number + else + -- Fallback to table allocation if no callback is provided. + local data = {} + local function fill(...) + local args = { ... } + for i, field in compiled.fields do + data[field.name] = args[i] + end + end + compiled.decoder(b, offset, fill) + return data, compiled.fixedSize :: number + end end - local data = {} + local data = if callback then nil else {} local cursor = offset + local decodedValues = if callback then table.create(compiled.fieldCount) else nil - for _, field in compiled.fields do - -- Pre-read bounds check. + for i, field in compiled.fields do if not Sanitizer.checkBounds(b, cursor, field.type.size) then return nil, 0 end local val = field.type.read(b, cursor) - -- We sanitize numbers at the source to ensure they are finite. - if type(val) == "number" then - val = Sanitizer.sanitizeFloat(val) + if callback and decodedValues then + decodedValues[i] = val + elseif data then + data[field.name] = val end - data[field.name] = val cursor += field.type.getSize(val) end + if callback and decodedValues then + callback(table.unpack(decodedValues)) + return nil, cursor - offset + end + return data, cursor - offset end diff --git a/src/Types/init.luau b/src/Types/init.luau index 5be37cd..8e54936 100644 --- a/src/Types/init.luau +++ b/src/Types/init.luau @@ -3,35 +3,6 @@ -- This file is part of the Satset networking library and is licensed under MIT License; see LICENSE.txt for details -- Types — provides optimized serialization primitives and composite types. --- [ Builtin Localization ] --- We localize these for GETIMPORT and FASTCALL optimization in the Luau VM. -local readu8 = buffer.readu8 -local readu16 = buffer.readu16 -local readu32 = buffer.readu32 -local readi8 = buffer.readi8 -local readi16 = buffer.readi16 -local readi32 = buffer.readi32 -local readf32 = buffer.readf32 -local readf64 = buffer.readf64 -local writeu8 = buffer.writeu8 -local writeu16 = buffer.writeu16 -local writeu32 = buffer.writeu32 -local writei8 = buffer.writei8 -local writei16 = buffer.writei16 -local writei32 = buffer.writei32 -local writef32 = buffer.writef32 -local writef64 = buffer.writef64 -local readstring = buffer.readstring -local writestring = buffer.writestring -local clamp = math.clamp -local round = math.round -local sqrt = math.sqrt -local abs = math.abs -local max = math.max -local min = math.min -local huge = math.huge -local floor = math.floor - -- [ Types ] export type Type = { @@ -55,54 +26,54 @@ end -- Clamps NaN and ±Infinity to 0. Prevents malicious clients from -- injecting poisoned floats that propagate through server arithmetic. local function sanitizeFloat(v: number): number - return if v == v and v ~= huge and v ~= -huge then v else 0 + return if v == v and v ~= math.huge and v ~= -math.huge then v else 0 end -- [ Primitives ] Types.u8 = { - read = readu8, - write = writeu8, + read = buffer.readu8, + write = buffer.writeu8, size = 1, getSize = fixedSize(1), isFixed = true, } Types.u16 = { - read = readu16, - write = writeu16, + read = buffer.readu16, + write = buffer.writeu16, size = 2, getSize = fixedSize(2), isFixed = true, } Types.u32 = { - read = readu32, - write = writeu32, + read = buffer.readu32, + write = buffer.writeu32, size = 4, getSize = fixedSize(4), isFixed = true, } Types.i8 = { - read = readi8, - write = writei8, + read = buffer.readi8, + write = buffer.writei8, size = 1, getSize = fixedSize(1), isFixed = true, } Types.i16 = { - read = readi16, - write = writei16, + read = buffer.readi16, + write = buffer.writei16, size = 2, getSize = fixedSize(2), isFixed = true, } Types.i32 = { - read = readi32, - write = writei32, + read = buffer.readi32, + write = buffer.writei32, size = 4, getSize = fixedSize(4), isFixed = true, @@ -110,10 +81,10 @@ Types.i32 = { Types.f32 = { read = function(b: buffer, cursor: number) - return sanitizeFloat(readf32(b, cursor)) + return sanitizeFloat(buffer.readf32(b, cursor)) end, write = function(b: buffer, cursor: number, value: number) - writef32(b, cursor, sanitizeFloat(value)) + buffer.writef32(b, cursor, sanitizeFloat(value)) end, size = 4, getSize = fixedSize(4), @@ -122,10 +93,10 @@ Types.f32 = { Types.f64 = { read = function(b: buffer, cursor: number) - return sanitizeFloat(readf64(b, cursor)) + return sanitizeFloat(buffer.readf64(b, cursor)) end, write = function(b: buffer, cursor: number, value: number) - writef64(b, cursor, sanitizeFloat(value)) + buffer.writef64(b, cursor, sanitizeFloat(value)) end, size = 8, getSize = fixedSize(8), @@ -134,10 +105,10 @@ Types.f64 = { Types.bool = { read = function(b: buffer, cursor: number) - return readu8(b, cursor) ~= 0 + return buffer.readu8(b, cursor) ~= 0 end, write = function(b: buffer, cursor: number, value: boolean) - writeu8(b, cursor, value and 1 or 0) + buffer.writeu8(b, cursor, value and 1 or 0) end, size = 1, getSize = fixedSize(1), @@ -146,10 +117,10 @@ Types.bool = { Types.u4 = { read = function(b: buffer, cursor: number) - return bit32.band(readu8(b, cursor), 0xF) + return bit32.band(buffer.readu8(b, cursor), 0xF) end, write = function(b: buffer, cursor: number, value: number) - writeu8(b, cursor, bit32.band(value, 0xF)) + buffer.writeu8(b, cursor, bit32.band(value, 0xF)) end, size = 1, getSize = fixedSize(1), @@ -160,16 +131,16 @@ Types.u4 = { Types.string8 = { read = function(b: buffer, cursor: number) - local len = readu8(b, cursor) + local len = buffer.readu8(b, cursor) if cursor + 1 + len > buffer.len(b) then return "" end - return readstring(b, cursor + 1, len) + return buffer.readstring(b, cursor + 1, len) end, write = function(b: buffer, cursor: number, value: string) local len = #value - writeu8(b, cursor, len) - writestring(b, cursor + 1, value, len) + buffer.writeu8(b, cursor, len) + buffer.writestring(b, cursor + 1, value, len) end, size = 1, getSize = function(value: string) @@ -180,16 +151,16 @@ Types.string8 = { Types.string16 = { read = function(b: buffer, cursor: number) - local len = readu16(b, cursor) + local len = buffer.readu16(b, cursor) if cursor + 2 + len > buffer.len(b) then return "" end - return readstring(b, cursor + 2, len) + return buffer.readstring(b, cursor + 2, len) end, write = function(b: buffer, cursor: number, value: string) local len = #value - writeu16(b, cursor, len) - writestring(b, cursor + 2, value, len) + buffer.writeu16(b, cursor, len) + buffer.writestring(b, cursor + 2, value, len) end, size = 2, getSize = function(value: string) @@ -202,20 +173,20 @@ Types.string16 = { Types.Vector3 = { read = function(b: buffer, cursor: number) - local x = readf32(b, cursor) - local y = readf32(b, cursor + 4) - local z = readf32(b, cursor + 8) + local x = buffer.readf32(b, cursor) + local y = buffer.readf32(b, cursor + 4) + local z = buffer.readf32(b, cursor + 8) return Vector3.new( - if x == x and x ~= huge and x ~= -huge then x else 0, - if y == y and y ~= huge and y ~= -huge then y else 0, - if z == z and z ~= huge and z ~= -huge then z else 0 + if x == x and x ~= math.huge and x ~= -math.huge then x else 0, + if y == y and y ~= math.huge and y ~= -math.huge then y else 0, + if z == z and z ~= math.huge and z ~= -math.huge then z else 0 ) end, write = function(b: buffer, cursor: number, value: Vector3) local x, y, z = value.X, value.Y, value.Z - writef32(b, cursor, if x == x and x ~= huge and x ~= -huge then x else 0) - writef32(b, cursor + 4, if y == y and y ~= huge and y ~= -huge then y else 0) - writef32(b, cursor + 8, if z == z and z ~= huge and z ~= -huge then z else 0) + buffer.writef32(b, cursor, if x == x and x ~= math.huge and x ~= -math.huge then x else 0) + buffer.writef32(b, cursor + 4, if y == y and y ~= math.huge and y ~= -math.huge then y else 0) + buffer.writef32(b, cursor + 8, if z == z and z ~= math.huge and z ~= -math.huge then z else 0) end, size = 12, getSize = fixedSize(12), @@ -224,11 +195,11 @@ Types.Vector3 = { Types.Vector2 = { read = function(b: buffer, cursor: number) - return Vector2.new(sanitizeFloat(readf32(b, cursor)), sanitizeFloat(readf32(b, cursor + 4))) + return Vector2.new(sanitizeFloat(buffer.readf32(b, cursor)), sanitizeFloat(buffer.readf32(b, cursor + 4))) end, write = function(b: buffer, cursor: number, value: Vector2) - writef32(b, cursor, sanitizeFloat(value.X)) - writef32(b, cursor + 4, sanitizeFloat(value.Y)) + buffer.writef32(b, cursor, sanitizeFloat(value.X)) + buffer.writef32(b, cursor + 4, sanitizeFloat(value.Y)) end, size = 8, getSize = fixedSize(8), @@ -237,15 +208,15 @@ Types.Vector2 = { Types.Color3 = { read = function(b: buffer, cursor: number) - local r = readu8(b, cursor) / 255 - local g = readu8(b, cursor + 1) / 255 - local bl = readu8(b, cursor + 2) / 255 + local r = buffer.readu8(b, cursor) / 255 + local g = buffer.readu8(b, cursor + 1) / 255 + local bl = buffer.readu8(b, cursor + 2) / 255 return Color3.new(r, g, bl) end, write = function(b: buffer, cursor: number, value: Color3) - writeu8(b, cursor, clamp(round(value.R * 255), 0, 255)) - writeu8(b, cursor + 1, clamp(round(value.G * 255), 0, 255)) - writeu8(b, cursor + 2, clamp(round(value.B * 255), 0, 255)) + buffer.writeu8(b, cursor, math.clamp(math.round(value.R * 255), 0, 255)) + buffer.writeu8(b, cursor + 1, math.clamp(math.round(value.G * 255), 0, 255)) + buffer.writeu8(b, cursor + 2, math.clamp(math.round(value.B * 255), 0, 255)) end, size = 3, getSize = fixedSize(3), @@ -267,15 +238,15 @@ function Types.Vector3Quantized(range: number?): Type return { read = function(b: buffer, cursor: number) - local x = readi16(b, cursor) / SCALE - local y = readi16(b, cursor + 2) / SCALE - local z = readi16(b, cursor + 4) / SCALE + local x = buffer.readi16(b, cursor) / SCALE + local y = buffer.readi16(b, cursor + 2) / SCALE + local z = buffer.readi16(b, cursor + 4) / SCALE return Vector3.new(x, y, z) end, write = function(b: buffer, cursor: number, value: Vector3) - writei16(b, cursor, clamp(round(value.X * SCALE), -32767, 32767)) - writei16(b, cursor + 2, clamp(round(value.Y * SCALE), -32767, 32767)) - writei16(b, cursor + 4, clamp(round(value.Z * SCALE), -32767, 32767)) + buffer.writei16(b, cursor, math.clamp(math.round(value.X * SCALE), -32767, 32767)) + buffer.writei16(b, cursor + 2, math.clamp(math.round(value.Y * SCALE), -32767, 32767)) + buffer.writei16(b, cursor + 4, math.clamp(math.round(value.Z * SCALE), -32767, 32767)) end, size = 6, getSize = fixedSize(6), @@ -290,12 +261,12 @@ end ]] Types.f16 = { read = function(b: buffer, cursor: number) - local raw = readu16(b, cursor) + local raw = buffer.readu16(b, cursor) local sign = (bit32.band(raw, 0x8000) ~= 0) and -1 or 1 local exp = bit32.rshift(bit32.band(raw, 0x7C00), 10) local frac = bit32.band(raw, 0x03FF) if exp == 0x1F then - return (frac == 0) and (sign * huge) or 0 -- NaN as 0 for safety + return (frac == 0) and (sign * math.huge) or 0 -- NaN as 0 for safety elseif exp == 0 then return sign * (2 ^ -14) * (frac / 1024) else @@ -304,7 +275,7 @@ Types.f16 = { end, write = function(b: buffer, cursor: number, value: number) if value ~= value then - writeu16(b, cursor, 0) + buffer.writeu16(b, cursor, 0) return end local sign = 0 @@ -318,18 +289,18 @@ Types.f16 = { elseif value <= 2 ^ -24 then exp, frac = 0, 0 elseif value < 2 ^ -14 then - exp, frac = 0, round(value / (2 ^ -24)) + exp, frac = 0, math.round(value / (2 ^ -24)) else local log2 = math.log(value, 2) - exp = floor(log2) - frac = round((value / (2 ^ exp) - 1) * 1024) + exp = math.floor(log2) + frac = math.round((value / (2 ^ exp) - 1) * 1024) if frac == 1024 then exp += 1 frac = 0 end exp += 15 end - writeu16(b, cursor, bit32.bor(sign, bit32.lshift(exp, 10), frac)) + buffer.writeu16(b, cursor, bit32.bor(sign, bit32.lshift(exp, 10), frac)) end, size = 2, getSize = fixedSize(2), @@ -368,13 +339,13 @@ function Types.Vector2Quantized(range: number?): Type return { read = function(b: buffer, cursor: number) - local x = readi16(b, cursor) / SCALE - local y = readi16(b, cursor + 2) / SCALE + local x = buffer.readi16(b, cursor) / SCALE + local y = buffer.readi16(b, cursor + 2) / SCALE return Vector2.new(x, y) end, write = function(b: buffer, cursor: number, value: Vector2) - writei16(b, cursor, clamp(round(value.X * SCALE), -32767, 32767)) - writei16(b, cursor + 2, clamp(round(value.Y * SCALE), -32767, 32767)) + buffer.writei16(b, cursor, math.clamp(math.round(value.X * SCALE), -32767, 32767)) + buffer.writei16(b, cursor + 2, math.clamp(math.round(value.Y * SCALE), -32767, 32767)) end, size = 4, getSize = fixedSize(4), @@ -391,12 +362,12 @@ end ]] Types.CFrame = { read = function(b: buffer, cursor: number) - local px = sanitizeFloat(readf32(b, cursor)) - local py = sanitizeFloat(readf32(b, cursor + 4)) - local pz = sanitizeFloat(readf32(b, cursor + 8)) - local raw_a = readi16(b, cursor + 12) - local raw_b = readi16(b, cursor + 14) - local raw_c = readi16(b, cursor + 16) + local px = sanitizeFloat(buffer.readf32(b, cursor)) + local py = sanitizeFloat(buffer.readf32(b, cursor + 4)) + local pz = sanitizeFloat(buffer.readf32(b, cursor + 8)) + local raw_a = buffer.readi16(b, cursor + 12) + local raw_b = buffer.readi16(b, cursor + 14) + local raw_c = buffer.readi16(b, cursor + 16) local largest = bit32.band(bit32.rshift(bit32.band(raw_a, 0xFFFF), 14), 3) local va = bit32.band(raw_a, 0x3FFF) @@ -409,7 +380,7 @@ Types.CFrame = { local b_val = raw_b / 32767 * 0.7071067811865476 local c_val = raw_c / 32767 * 0.7071067811865476 - local d = sqrt(max(0, 1 - (a * a + b_val * b_val + c_val * c_val))) + local d = math.sqrt(math.max(0, 1 - (a * a + b_val * b_val + c_val * c_val))) local qx, qy, qz, qw if largest == 0 then qw, qx, qy, qz = d, a, b_val, c_val @@ -444,9 +415,9 @@ Types.CFrame = { write = function(b: buffer, cursor: number, value: CFrame) local components = { value:GetComponents() } - writef32(b, cursor, sanitizeFloat(components[1])) - writef32(b, cursor + 4, sanitizeFloat(components[2])) - writef32(b, cursor + 8, sanitizeFloat(components[3])) + buffer.writef32(b, cursor, sanitizeFloat(components[1])) + buffer.writef32(b, cursor + 4, sanitizeFloat(components[2])) + buffer.writef32(b, cursor + 8, sanitizeFloat(components[3])) local r00, r01, r02 = components[4], components[5], components[6] local r10, r11, r12 = components[7], components[8], components[9] @@ -455,23 +426,23 @@ Types.CFrame = { local trace = r00 + r11 + r22 local qw, qx, qy, qz if trace > 0 then - local s = sqrt(trace + 1) * 2 + local s = math.sqrt(trace + 1) * 2 qw, qx, qy, qz = 0.25 * s, (r21 - r12) / s, (r02 - r20) / s, (r10 - r01) / s elseif r00 > r11 and r00 > r22 then - local s = sqrt(1 + r00 - r11 - r22) * 2 + local s = math.sqrt(1 + r00 - r11 - r22) * 2 qw, qx, qy, qz = (r21 - r12) / s, 0.25 * s, (r01 + r10) / s, (r02 + r20) / s elseif r11 > r22 then - local s = sqrt(1 + r11 - r00 - r22) * 2 + local s = math.sqrt(1 + r11 - r00 - r22) * 2 qw, qx, qy, qz = (r02 - r20) / s, (r01 + r10) / s, 0.25 * s, (r12 + r21) / s else - local s = sqrt(1 + r22 - r00 - r11) * 2 + local s = math.sqrt(1 + r22 - r00 - r11) * 2 qw, qx, qy, qz = (r10 - r01) / s, (r02 + r20) / s, (r12 + r21) / s, 0.25 * s end - local mag = sqrt(qw * qw + qx * qx + qy * qy + qz * qz) + local mag = math.sqrt(qw * qw + qx * qx + qy * qy + qz * qz) qw, qx, qy, qz = qw / mag, qx / mag, qy / mag, qz / mag - local abs_w, abs_x, abs_y, abs_z = abs(qw), abs(qx), abs(qy), abs(qz) + local abs_w, abs_x, abs_y, abs_z = math.abs(qw), math.abs(qx), math.abs(qy), math.abs(qz) local largest, max_val = 0, abs_w if abs_x > max_val then largest, max_val = 1, abs_x @@ -500,13 +471,13 @@ Types.CFrame = { end local QSCALE_INV = 8191 / 0.7071067811865476 - local enc_a = clamp(round(a * QSCALE_INV), -8191, 8191) + local enc_a = math.clamp(math.round(a * QSCALE_INV), -8191, 8191) local packed_a = bit32.bor(bit32.band(enc_a, 0x3FFF), bit32.lshift(largest, 14)) - writeu16(b, cursor + 12, packed_a) + buffer.writeu16(b, cursor + 12, packed_a) local QSCALE_B = 32767 / 0.7071067811865476 - writei16(b, cursor + 14, clamp(round(b_val * QSCALE_B), -32767, 32767)) - writei16(b, cursor + 16, clamp(round(c_val * QSCALE_B), -32767, 32767)) + buffer.writei16(b, cursor + 14, math.clamp(math.round(b_val * QSCALE_B), -32767, 32767)) + buffer.writei16(b, cursor + 16, math.clamp(math.round(c_val * QSCALE_B), -32767, 32767)) end, size = 18, getSize = fixedSize(18), @@ -518,16 +489,16 @@ Types.CFrame = { function Types.optional(innerType: Type): Type return { read = function(b: buffer, cursor: number) - if readu8(b, cursor) == 0 then + if buffer.readu8(b, cursor) == 0 then return nil end return innerType.read(b, cursor + 1) end, write = function(b: buffer, cursor: number, value: any) if value == nil then - writeu8(b, cursor, 0) + buffer.writeu8(b, cursor, 0) else - writeu8(b, cursor, 1) + buffer.writeu8(b, cursor, 1) innerType.write(b, cursor + 1, value) end end, @@ -546,19 +517,19 @@ function Types.array(elementType: Type): Type<{ any }> return { read = function(b: buffer, cursor: number) local bufLen = buffer.len(b) - local len = readu16(b, cursor) + local len = buffer.readu16(b, cursor) -- Cap allocation: exploiter can't claim 65535 elements in a tiny buffer. - local maxPossible = floor((bufLen - cursor - 2) / 12) - len = min(len, max(maxPossible, 0)) + local maxPossible = math.floor((bufLen - cursor - 2) / 12) + len = math.min(len, math.max(maxPossible, 0)) local arr, pos = table.create(len), cursor + 2 for i = 1, len do - local x = readf32(b, pos) - local y = readf32(b, pos + 4) - local z = readf32(b, pos + 8) + local x = buffer.readf32(b, pos) + local y = buffer.readf32(b, pos + 4) + local z = buffer.readf32(b, pos + 8) arr[i] = Vector3.new( - if x == x and x ~= huge and x ~= -huge then x else 0, - if y == y and y ~= huge and y ~= -huge then y else 0, - if z == z and z ~= huge and z ~= -huge then z else 0 + if x == x and x ~= math.huge and x ~= -math.huge then x else 0, + if y == y and y ~= math.huge and y ~= -math.huge then y else 0, + if z == z and z ~= math.huge and z ~= -math.huge then z else 0 ) pos += 12 end @@ -566,14 +537,14 @@ function Types.array(elementType: Type): Type<{ any }> end, write = function(b: buffer, cursor: number, value: { any }) local len = #value - writeu16(b, cursor, len) + buffer.writeu16(b, cursor, len) local pos = cursor + 2 for i = 1, len do local val = value[i] local x, y, z = val.X, val.Y, val.Z - writef32(b, pos, if x == x and x ~= huge and x ~= -huge then x else 0) - writef32(b, pos + 4, if y == y and y ~= huge and y ~= -huge then y else 0) - writef32(b, pos + 8, if z == z and z ~= huge and z ~= -huge then z else 0) + buffer.writef32(b, pos, if x == x and x ~= math.huge and x ~= -math.huge then x else 0) + buffer.writef32(b, pos + 4, if y == y and y ~= math.huge and y ~= -math.huge then y else 0) + buffer.writef32(b, pos + 8, if z == z and z ~= math.huge and z ~= -math.huge then z else 0) pos += 12 end end, @@ -587,16 +558,16 @@ function Types.array(elementType: Type): Type<{ any }> return { read = function(b: buffer, cursor: number) local bufLen = buffer.len(b) - local len = readu16(b, cursor) + local len = buffer.readu16(b, cursor) local maxPossible = (bufLen - cursor - 2) * 8 - len = min(len, max(maxPossible, 0)) + len = math.min(len, math.max(maxPossible, 0)) local arr, pos = table.create(len), cursor + 2 - local byteLen = floor((len + 7) / 8) + local byteLen = math.floor((len + 7) / 8) for i = 0, byteLen - 1 do - local byte = readu8(b, pos + i) + local byte = buffer.readu8(b, pos + i) local startIdx = i * 8 + 1 - local endIdx = min(startIdx + 7, len) + local endIdx = math.min(startIdx + 7, len) for j = startIdx, endIdx do arr[j] = bit32.band(bit32.rshift(byte, (j - 1) % 8), 1) ~= 0 @@ -606,26 +577,26 @@ function Types.array(elementType: Type): Type<{ any }> end, write = function(b: buffer, cursor: number, value: { any }) local len = #value - writeu16(b, cursor, len) + buffer.writeu16(b, cursor, len) local pos = cursor + 2 - local byteLen = floor((len + 7) / 8) + local byteLen = math.floor((len + 7) / 8) for i = 0, byteLen - 1 do local byte = 0 local startIdx = i * 8 + 1 - local endIdx = min(startIdx + 7, len) + local endIdx = math.min(startIdx + 7, len) for j = startIdx, endIdx do if value[j] then byte = bit32.bor(byte, bit32.lshift(1, (j - 1) % 8)) end end - writeu8(b, pos + i, byte) + buffer.writeu8(b, pos + i, byte) end end, size = 2, getSize = function(value: { any }) - return 2 + floor((#value + 7) / 8) + return 2 + math.floor((#value + 7) / 8) end, isFixed = false, } @@ -633,26 +604,26 @@ function Types.array(elementType: Type): Type<{ any }> return { read = function(b: buffer, cursor: number) local bufLen = buffer.len(b) - local len = readu16(b, cursor) + local len = buffer.readu16(b, cursor) local maxPossible = bufLen - cursor - 2 - len = min(len, max(maxPossible, 0)) + len = math.min(len, math.max(maxPossible, 0)) local arr, pos = table.create(len), cursor + 2 for i = 1, len do - local strLen = readu8(b, pos) - arr[i] = readstring(b, pos + 1, strLen) + local strLen = buffer.readu8(b, pos) + arr[i] = buffer.readstring(b, pos + 1, strLen) pos += 1 + strLen end return arr end, write = function(b: buffer, cursor: number, value: { any }) local len = #value - writeu16(b, cursor, len) + buffer.writeu16(b, cursor, len) local pos = cursor + 2 for i = 1, len do local str = value[i] local strLen = #str - writeu8(b, pos, strLen) - writestring(b, pos + 1, str, strLen) + buffer.writeu8(b, pos, strLen) + buffer.writestring(b, pos + 1, str, strLen) pos += 1 + strLen end end, @@ -670,26 +641,26 @@ function Types.array(elementType: Type): Type<{ any }> return { read = function(b: buffer, cursor: number) local bufLen = buffer.len(b) - local len = readu16(b, cursor) + local len = buffer.readu16(b, cursor) local maxPossible = bufLen - cursor - 2 - len = min(len, max(maxPossible, 0)) + len = math.min(len, math.max(maxPossible, 0)) local arr, pos = table.create(len), cursor + 2 for i = 1, len do - local strLen = readu16(b, pos) - arr[i] = readstring(b, pos + 2, strLen) + local strLen = buffer.readu16(b, pos) + arr[i] = buffer.readstring(b, pos + 2, strLen) pos += 2 + strLen end return arr end, write = function(b: buffer, cursor: number, value: { any }) local len = #value - writeu16(b, cursor, len) + buffer.writeu16(b, cursor, len) local pos = cursor + 2 for i = 1, len do local str = value[i] local strLen = #str - writeu16(b, pos, strLen) - writestring(b, pos + 2, str, strLen) + buffer.writeu16(b, pos, strLen) + buffer.writestring(b, pos + 2, str, strLen) pos += 2 + strLen end end, @@ -707,22 +678,22 @@ function Types.array(elementType: Type): Type<{ any }> return { read = function(b: buffer, cursor: number) local bufLen = buffer.len(b) - local len = readu16(b, cursor) + local len = buffer.readu16(b, cursor) local maxPossible = bufLen - cursor - 2 - len = min(len, max(maxPossible, 0)) + len = math.min(len, math.max(maxPossible, 0)) local arr, pos = table.create(len), cursor + 2 for i = 1, len do - arr[i] = readu8(b, pos) + arr[i] = buffer.readu8(b, pos) pos += 1 end return arr end, write = function(b: buffer, cursor: number, value: { any }) local len = #value - writeu16(b, cursor, len) + buffer.writeu16(b, cursor, len) local pos = cursor + 2 for i = 1, len do - writeu8(b, pos, value[i]) + buffer.writeu8(b, pos, value[i]) pos += 1 end end, @@ -736,22 +707,22 @@ function Types.array(elementType: Type): Type<{ any }> return { read = function(b: buffer, cursor: number) local bufLen = buffer.len(b) - local len = readu16(b, cursor) - local maxPossible = floor((bufLen - cursor - 2) / 4) - len = min(len, max(maxPossible, 0)) + local len = buffer.readu16(b, cursor) + local maxPossible = math.floor((bufLen - cursor - 2) / 4) + len = math.min(len, math.max(maxPossible, 0)) local arr, pos = table.create(len), cursor + 2 for i = 1, len do - arr[i] = sanitizeFloat(readf32(b, pos)) + arr[i] = sanitizeFloat(buffer.readf32(b, pos)) pos += 4 end return arr end, write = function(b: buffer, cursor: number, value: { any }) local len = #value - writeu16(b, cursor, len) + buffer.writeu16(b, cursor, len) local pos = cursor + 2 for i = 1, len do - writef32(b, pos, sanitizeFloat(value[i])) + buffer.writef32(b, pos, sanitizeFloat(value[i])) pos += 4 end end, @@ -772,9 +743,9 @@ function Types.array(elementType: Type): Type<{ any }> return { read = function(b: buffer, cursor: number) local bufLen = buffer.len(b) - local len = readu16(b, cursor) - local maxPossible = floor((bufLen - cursor - 2) / step) - len = min(len, max(maxPossible, 0)) + local len = buffer.readu16(b, cursor) + local maxPossible = math.floor((bufLen - cursor - 2) / step) + len = math.min(len, math.max(maxPossible, 0)) local arr, pos = table.create(len), cursor + 2 for i = 1, len do arr[i] = elemRead(b, pos) @@ -784,7 +755,7 @@ function Types.array(elementType: Type): Type<{ any }> end, write = function(b: buffer, cursor: number, value: { any }) local len = #value - writeu16(b, cursor, len) + buffer.writeu16(b, cursor, len) local pos = cursor + 2 for i = 1, len do elemWrite(b, pos, value[i]) @@ -803,9 +774,9 @@ function Types.array(elementType: Type): Type<{ any }> return { read = function(b: buffer, cursor: number) local bufLen = buffer.len(b) - local len = readu16(b, cursor) + local len = buffer.readu16(b, cursor) local maxPossible = bufLen - cursor - 2 - len = min(len, max(maxPossible, 0)) + len = math.min(len, math.max(maxPossible, 0)) local arr, pos = table.create(len), cursor + 2 for i = 1, len do @@ -817,7 +788,7 @@ function Types.array(elementType: Type): Type<{ any }> end, write = function(b: buffer, cursor: number, value: { any }) local len = #value - writeu16(b, cursor, len) + buffer.writeu16(b, cursor, len) local pos = cursor + 2 for i = 1, len do @@ -842,10 +813,10 @@ function Types.map(keyType: Type, valueType: Type): Type<{ [any]: any return { read = function(b: buffer, cursor: number) local bufLen = buffer.len(b) - local count = readu16(b, cursor) + local count = buffer.readu16(b, cursor) -- Cap iteration count: can't have more entries than remaining bytes. local maxPossible = bufLen - cursor - 2 - count = min(count, max(maxPossible, 0)) + count = math.min(count, math.max(maxPossible, 0)) local result, pos = {}, cursor + 2 local kFixed, vFixed = keyType.isFixed, valueType.isFixed local kStep, vStep = keyType.size, valueType.size @@ -870,7 +841,7 @@ function Types.map(keyType: Type, valueType: Type): Type<{ [any]: any return tostring(a) < tostring(b_key) end) - writeu16(b, cursor, n) + buffer.writeu16(b, cursor, n) local pos = cursor + 2 local kFixed, vFixed = keyType.isFixed, valueType.isFixed local kStep, vStep = keyType.size, valueType.size @@ -912,11 +883,11 @@ function Types.enum(values: { string }): Type return { read = function(b: buffer, cursor: number) - local idx = readu8(b, cursor) + local idx = buffer.readu8(b, cursor) return toValue[idx] or values[1] end, write = function(b: buffer, cursor: number, value: string) - writeu8(b, cursor, toIndex[value] or 0) + buffer.writeu8(b, cursor, toIndex[value] or 0) end, size = 1, getSize = fixedSize(1), diff --git a/src/init.luau b/src/init.luau index fb30600..79c1ad9 100644 --- a/src/init.luau +++ b/src/init.luau @@ -2,19 +2,19 @@ -- Satset — high-performance hybrid networking for Roblox. -- "Sat set, sampai." -local Batcher = require(script.Core.Batcher) -local Bridge = require(script.Core.Bridge) -local Channel = require(script.Networking.Channel) -local Guard = require(script.Core.Guard) -local Packet = require(script.Networking.Packet) -local Types = require(script.Types) +local Batcher = require("./Core/Batcher") +local Bridge = require("./Core/Bridge") +local Channel = require("./Networking/Channel") +local Guard = require("./Core/Guard") +local Packet = require("./Networking/Packet") +local Types = require("./Types") local Players = game:GetService("Players") local RunService = game:GetService("RunService") -- [ Constants ] -local VERSION = "0.2.0" +local VERSION = "0.3.1-rc.1" local IS_SERVER = RunService:IsServer() local CHANNEL_RELIABLE_NAME = "__SatsetChannelReliable" @@ -37,9 +37,10 @@ Satset.Types = Types Satset.definePacket = Packet.definePacket Satset.defineChannel = Channel.defineChannel +-- We use explicit type references to avoid LSP resolution issues with relative requires. export type SatsetConfig = { - guard: Guard.GuardConfig?, - batching: Batcher.BatchingConfig?, + guard: any?, + batching: any?, } --[[