diff --git a/sourcemap.json b/sourcemap.json new file mode 100644 index 0000000..d577011 --- /dev/null +++ b/sourcemap.json @@ -0,0 +1 @@ +{"name":"Warp","className":"ModuleScript","filePaths":["src/init.luau","default.project.json"],"children":[{"name":"Client","className":"ModuleScript","filePaths":["src/Client/init.luau"]},{"name":"Server","className":"ModuleScript","filePaths":["src/Server/init.luau"]},{"name":"Buffer","className":"ModuleScript","filePaths":["src/Buffer.luau"]},{"name":"Thread","className":"ModuleScript","filePaths":["src/Thread.luau"]}]} \ No newline at end of file diff --git a/src/Buffer.luau b/src/Buffer.luau new file mode 100644 index 0000000..7547e9d --- /dev/null +++ b/src/Buffer.luau @@ -0,0 +1,964 @@ +--!native +--!optimize 2 +--@EternityDev + +-- 0x00-0x7F: + fixint (0-127) - single byte +-- 0x80-0x9F: - fixint (-32 to -1) - single byte +local T_NIL = 0xA0 +local T_FALSE = 0xA1 +local T_TRUE = 0xA2 +local T_U8 = 0xA3 +local T_U16 = 0xA4 +local T_U32 = 0xA5 +local T_I8 = 0xA6 +local T_I16 = 0xA7 +local T_I32 = 0xA8 +local T_F32 = 0xA9 +local T_F64 = 0xAA + +-- str: 0xB0-0xBF = inline len 0-15 +local T_STR_BASE = 0xB0 +local T_STR8 = 0xC0 +local T_STR16 = 0xC1 +local T_STRVAR = 0xC2 + +-- arrays: 0xC4-0xCB = inline len 0-7 +local T_ARR_BASE = 0xC4 +local T_ARR8 = 0xCC +local T_ARR16 = 0xCD +local T_ARRVAR = 0xCE + +-- maps: 0xD0-0xD7 = inline len 0-7 +local T_MAP_BASE = 0xD0 +local T_MAP8 = 0xD8 +local T_MAP16 = 0xD9 +local T_MAPVAR = 0xDA + +-- Typed arrays (homogeneous, no per-element tags) +local T_TYPED_ARR = 0xDB + +local T_VEC3 = 0xE0 -- f32 precision (12 bytes) +local T_VEC3_F16 = 0xE1 -- f16 precision (6 bytes) +local T_VEC2 = 0xE2 -- f32 precision (8 bytes) +local T_VEC2_F16 = 0xE3 -- f16 precision (4 bytes) +local T_CFRAME = 0xE4 -- f32 pos + f32 orient (24 bytes) +local T_CFRAME_F16 = 0xE5 -- f16 pos + f16 orient (12 bytes) +local T_COLOR3 = 0xE6 -- RGB bytes (3 bytes) +local T_COLOR3_F = 0xE7 -- RGB floats (12 bytes) +local T_BRICKCOLOR = 0xE8 +local T_INSTANCE = 0xE9 +local T_ENUMITEM = 0xEA +local T_ENUM = 0xEB +local T_UDIM2 = 0xEC +local T_UDIM = 0xED +local T_RECT = 0xEE +local T_NUMBERRANGE = 0xEF +local T_RAY = 0xF0 +local T_COLSEQ = 0xF2 -- ColorSequence +local T_NUMSEQ = 0xF3 -- NumberSequence + +local function f32ToF16(value: number): number + if value ~= value then return 0x7E00 end + if value == math.huge then return 0x7C00 end + if value == -math.huge then return 0xFC00 end + if value == 0 then return 0 end + + local sign = 0 + if value < 0 then + sign = 0x8000 + value = -value + end + + local mantissa, exponent = math.frexp(value) + exponent += 14 + + if exponent <= 0 then + if exponent < -10 then return sign end + mantissa = math.floor(mantissa * bit32.lshift(1, 10 + exponent) + 0.5) + return bit32.bor(sign, mantissa) + elseif exponent >= 31 then + return bit32.bor(sign, 0x7C00) + end + + mantissa = math.floor((mantissa * 2 - 1) * 1024 + 0.5) + if mantissa == 1024 then + mantissa = 0 + exponent += 1 + if exponent >= 31 then return bit32.bor(sign, 0x7C00) end + end + return bit32.bor(sign, bit32.lshift(exponent, 10), mantissa) +end + +local function f16ToF32(bits: number): number + local sign = bit32.band(bits, 0x8000) + local exponent = bit32.band(bit32.rshift(bits, 10), 0x1F) + local mantissa = bit32.band(bits, 0x03FF) + + local value: number + if exponent == 0 then + value = mantissa == 0 and 0 or math.ldexp(mantissa / 1024, -14) + elseif exponent == 31 then + value = mantissa == 0 and math.huge or 0/0 + else + value = math.ldexp(1 + mantissa / 1024, exponent - 15) + end + + return sign ~= 0 and -value or value +end + +local function varUIntSize(value: number): number + if value < 0x80 then return 1 end + if value < 0x4000 then return 2 end + if value < 0x200000 then return 3 end + if value < 0x10000000 then return 4 end + return 5 +end + +local function writeVarUInt(buf: buffer, offset: number, value: number): number + while value >= 0x80 do + buffer.writeu8(buf, offset, bit32.bor(bit32.band(value, 0x7F), 0x80)) + offset += 1 + value = bit32.rshift(value, 7) + end + buffer.writeu8(buf, offset, value) + return offset + 1 +end + +local function readVarUInt(buf: buffer, offset: number): (number, number) + local value = 0 + local shift = 0 + repeat + local byte = buffer.readu8(buf, offset) + offset += 1 + value = bit32.bor(value, bit32.lshift(bit32.band(byte, 0x7F), shift)) + shift += 7 + until byte < 0x80 + return value, offset +end + +export type Writer = { + buf: buffer, + cursor: number, + capacity: number, + refs: {any}, +} + +local DEFAULT_CAPACITY = 128 + +local function createWriter(capacity: number?): Writer + local cap = capacity or DEFAULT_CAPACITY + return { + buf = buffer.create(cap), + cursor = 0, + capacity = cap, + refs = {}, + } +end + +local function ensureSpace(w: Writer, bytes: number) + local needed = w.cursor + bytes + if needed <= w.capacity then return end + + local newCap = w.capacity + while newCap < needed do + newCap *= 2 + end + + local newBuf = buffer.create(newCap) + buffer.copy(newBuf, 0, w.buf, 0, w.cursor) + w.buf = newBuf + w.capacity = newCap +end + +local function wByte(w: Writer, v: number) + ensureSpace(w, 1) + buffer.writeu8(w.buf, w.cursor, v) + w.cursor += 1 +end + +local function wU16(w: Writer, v: number) + ensureSpace(w, 2) + buffer.writei16(w.buf, w.cursor, v) + w.cursor += 2 +end + +local function wI16(w: Writer, v: number) + ensureSpace(w, 2) + buffer.writei16(w.buf, w.cursor, v) + w.cursor += 2 +end + +local function wU32(w: Writer, v: number) + ensureSpace(w, 4) + buffer.writei32(w.buf, w.cursor, v) + w.cursor += 4 +end + +local function wI32(w: Writer, v: number) + ensureSpace(w, 4) + buffer.writei32(w.buf, w.cursor, v) + w.cursor += 4 +end + +local function wF32(w: Writer, v: number) + ensureSpace(w, 4) + buffer.writef32(w.buf, w.cursor, v) + w.cursor += 4 +end + +local function wF64(w: Writer, v: number) + ensureSpace(w, 8) + buffer.writef64(w.buf, w.cursor, v) + w.cursor += 8 +end + +local function wF16(w: Writer, v: number) + ensureSpace(w, 2) + buffer.writei16(w.buf, w.cursor, f32ToF16(v)) + w.cursor += 2 +end + +local function wVarUInt(w: Writer, v: number) + ensureSpace(w, varUIntSize(v)) + w.cursor = writeVarUInt(w.buf, w.cursor, v) +end + +local function wString(w: Writer, s: string) + local n = #s + ensureSpace(w, n) + buffer.writestring(w.buf, w.cursor, s) + w.cursor += n +end + +local packValue: (w: Writer, v: any) -> () + +local function packNumber(w: Writer, n: number) + if n ~= n then + wByte(w, T_F64) + wF64(w, n) + return + end + + local isInt = n == math.floor(n) + + if isInt then + -- + fixint: 0-127 in single byte + if n >= 0 and n <= 127 then + wByte(w, n) + return + end + + -- - fixint: -32 to -1 in single byte + if n >= -32 and n < 0 then + wByte(w, bit32.bor(0x80, n + 32)) + return + end + + -- larger ints + if n >= 0 then + if n <= 255 then + wByte(w, T_U8) + wByte(w, n) + elseif n <= 65535 then + wByte(w, T_U16) + wU16(w, n) + elseif n <= 4294967295 then + wByte(w, T_U32) + wU32(w, n) + else + wByte(w, T_F64) + wF64(w, n) + end + else + if n >= -128 then + wByte(w, T_I8) + ensureSpace(w, 1) + buffer.writei8(w.buf, w.cursor, n) + w.cursor += 1 + elseif n >= -32768 then + wByte(w, T_I16) + wI16(w, n) + elseif n >= -2147483648 then + wByte(w, T_I32) + wI32(w, n) + else + wByte(w, T_F64) + wF64(w, n) + end + end + else + -- float: try f32 first + local testBuf = buffer.create(4) + buffer.writef32(testBuf, 0, n) + local f32Val = buffer.readf32(testBuf, 0) + + if f32Val == n or math.abs(n - f32Val) < math.abs(n) * 1e-6 then + wByte(w, T_F32) + wF32(w, n) + else + wByte(w, T_F64) + wF64(w, n) + end + end +end + +local function packString(w: Writer, s: string) + local n = #s + + if n <= 15 then + wByte(w, T_STR_BASE + n) + elseif n <= 255 then + wByte(w, T_STR8) + wByte(w, n) + elseif n <= 65535 then + wByte(w, T_STR16) + wU16(w, n) + else + wByte(w, T_STRVAR) + wVarUInt(w, n) + end + wString(w, s) +end + +-- is the table a homogeneous number array +local function analyzeNumberArray(t: {any}): (boolean, string?, number) + local count = #t + if count == 0 then return false, nil, 0 end + + local minVal, maxVal = math.huge, -math.huge + local allInt = true + + for i = 1, count do + local v = t[i] + if type(v) ~= "number" then return false, nil, count end + if v ~= math.floor(v) then allInt = false end + if v < minVal then minVal = v end + if v > maxVal then maxVal = v end + end + + if not allInt then + return true, "f32", count + elseif minVal >= 0 and maxVal <= 255 then + return true, "u8", count + elseif minVal >= -128 and maxVal <= 127 then + return true, "i8", count + elseif minVal >= 0 and maxVal <= 65535 then + return true, "u16", count + elseif minVal >= -32768 and maxVal <= 32767 then + return true, "i16", count + elseif minVal >= 0 and maxVal <= 4294967295 then + return true, "u32", count + elseif minVal >= -2147483648 and maxVal <= 2147483647 then + return true, "i32", count + end + return true, "f64", count +end + +local TYPED_CODES = { u8 = 1, i8 = 2, u16 = 3, i16 = 4, u32 = 5, i32 = 6, f32 = 7, f64 = 8 } +local TYPED_SIZES = { u8 = 1, i8 = 1, u16 = 2, i16 = 2, u32 = 4, i32 = 4, f32 = 4, f64 = 8 } +local TYPED_WRITERS = { + u8 = buffer.writeu8, i8 = buffer.writei8, u16 = buffer.writeu16, i16 = buffer.writei16, + u32 = buffer.writeu32, i32 = buffer.writei32, f32 = buffer.writef32, f64 = buffer.writef64, +} + +local function packTable(w: Writer, t: {[any]: any}) + local count = 0 + local maxIdx = 0 + local isArray = true + + for k in t do + count += 1 + if type(k) == "number" and k > 0 and k == math.floor(k) then + if k > maxIdx then maxIdx = k end + else + isArray = false + end + end + + isArray = isArray and maxIdx == count + + if isArray then + -- typed array optimization (worth it for 4+ elements) + local isHomogeneous, dtype, arrCount = analyzeNumberArray(t) + if isHomogeneous and dtype and arrCount >= 4 then + local code = TYPED_CODES[dtype] + local size = TYPED_SIZES[dtype] + local writer = TYPED_WRITERS[dtype] + + wByte(w, T_TYPED_ARR) + wByte(w, code) + wVarUInt(w, arrCount) + ensureSpace(w, arrCount * size) + + for i = 1, arrCount do + writer(w.buf, w.cursor, t[i]) + w.cursor += size + end + return + end + + -- regular array + if count <= 7 then + wByte(w, T_ARR_BASE + count) + elseif count <= 255 then + wByte(w, T_ARR8) + wByte(w, count) + elseif count <= 65535 then + wByte(w, T_ARR16) + wU16(w, count) + else + wByte(w, T_ARRVAR) + wVarUInt(w, count) + end + + for i = 1, count do + packValue(w, t[i]) + end + else + -- map + if count <= 7 then + wByte(w, T_MAP_BASE + count) + elseif count <= 255 then + wByte(w, T_MAP8) + wByte(w, count) + elseif count <= 65535 then + wByte(w, T_MAP16) + wU16(w, count) + else + wByte(w, T_MAPVAR) + wVarUInt(w, count) + end + + for k, v in t do + packValue(w, k) + packValue(w, v) + end + end +end + +local function packVector3(w: Writer, v: Vector3) + local x, y, z = v.X, v.Y, v.Z + local maxComp = math.max(math.abs(x), math.abs(y), math.abs(z)) + + if maxComp < 65000 and maxComp > 0.00006 then + wByte(w, T_VEC3_F16) + wF16(w, x) + wF16(w, y) + wF16(w, z) + else + wByte(w, T_VEC3) + wF32(w, x) + wF32(w, y) + wF32(w, z) + end +end + +local function packVector2(w: Writer, v: Vector2) + local x, y = v.X, v.Y + local maxComp = math.max(math.abs(x), math.abs(y)) + + if maxComp < 65000 and maxComp > 0.00006 then + wByte(w, T_VEC2_F16) + wF16(w, x) + wF16(w, y) + else + wByte(w, T_VEC2) + wF32(w, x) + wF32(w, y) + end +end + +local function packCFrame(w: Writer, cf: CFrame) + local pos = cf.Position + local rx, ry, rz = cf:ToOrientation() + + local px, py, pz = pos.X, pos.Y, pos.Z + local maxPos = math.max(math.abs(px), math.abs(py), math.abs(pz)) + + -- f16 + if maxPos < 65000 and (maxPos > 0.00006 or maxPos == 0) then + wByte(w, T_CFRAME_F16) + wF16(w, px) + wF16(w, py) + wF16(w, pz) + wF16(w, rx) + wF16(w, ry) + wF16(w, rz) + else + wByte(w, T_CFRAME) + wF32(w, px) + wF32(w, py) + wF32(w, pz) + wF32(w, rx) + wF32(w, ry) + wF32(w, rz) + end +end + +local function packColor3(w: Writer, c: Color3) + local r, g, b = c.R, c.G, c.B + + if r > 1 or g > 1 or b > 1 then + wByte(w, T_COLOR3_F) + wF32(w, r) + wF32(w, g) + wF32(w, b) + else + wByte(w, T_COLOR3) + wByte(w, math.clamp(math.round(r * 255), 0, 255)) + wByte(w, math.clamp(math.round(g * 255), 0, 255)) + wByte(w, math.clamp(math.round(b * 255), 0, 255)) + end +end + +packValue = function(w: Writer, v: any): () + local t: string = typeof(v) + + if t == "nil" then + wByte(w, T_NIL) + elseif t == "boolean" then + wByte(w, v and T_TRUE or T_FALSE) + elseif t == "number" then + packNumber(w, v) + elseif t == "string" then + packString(w, v) + elseif t == "table" then + packTable(w, v) + elseif t == "Vector3" then + packVector3(w, v) + elseif t == "Vector2" then + packVector2(w, v) + elseif t == "CFrame" then + packCFrame(w, v) + elseif t == "Color3" then + packColor3(w, v) + elseif t == "Instance" then + wByte(w, T_INSTANCE) + table.insert(w.refs, v) + wVarUInt(w, #w.refs) + elseif t == "EnumItem" then + wByte(w, T_ENUMITEM) + local enumName = `{v.EnumType}` + wVarUInt(w, #enumName) + wString(w, enumName) + wU16(w, v.Value) + elseif t == "BrickColor" then + wByte(w, T_BRICKCOLOR) + wU16(w, v.Number) + elseif t == "Enum" then + wByte(w, T_ENUM) + local name = v.Name + wVarUInt(w, #name) + wString(w, name) + elseif t == "UDim2" then + local X, Y = v.X, v.Y + wByte(w, T_UDIM2) + wF32(w, X.Scale) + wI32(w, X.Offset) + wF32(w, Y.Scale) + wI32(w, Y.Offset) + elseif t == "UDim" then + wByte(w, T_UDIM) + wF32(w, v.Scale) + wI32(w, v.Offset) + elseif t == "Rect" then + local Min, Max = v.Min, v.Max + wByte(w, T_RECT) + wF32(w, Min.X) + wF32(w, Min.Y) + wF32(w, Max.X) + wF32(w, Max.Y) + elseif t == "NumberRange" then + wByte(w, T_NUMBERRANGE) + wF32(w, v.Min) + wF32(w, v.Max) + elseif t == "Ray" then + local Origin, Direction = v.Origin, v.Direction + wByte(w, T_RAY) + wF32(w, Origin.X) + wF32(w, Origin.Y) + wF32(w, Origin.Z) + wF32(w, Direction.X) + wF32(w, Direction.Y) + wF32(w, Direction.Z) + elseif t == "ColorSequence" then + local keypoints = v.Keypoints + wByte(w, T_COLSEQ) + wByte(w, #keypoints) + for _, kp in keypoints do + wF32(w, kp.Time) + local c = kp.Value + wByte(w, math.clamp(math.round(c.R * 255), 0, 255)) + wByte(w, math.clamp(math.round(c.G * 255), 0, 255)) + wByte(w, math.clamp(math.round(c.B * 255), 0, 255)) + end + elseif t == "NumberSequence" then + local keypoints = v.Keypoints + wByte(w, T_NUMSEQ) + wByte(w, #keypoints) + for _, kp in keypoints do + wF32(w, kp.Time) + wF32(w, kp.Value) + wF32(w, kp.Envelope) + end + else + error(`Unsupported type: {t}`) + end +end + +local TYPED_READERS = { + [1] = function(b, o) return buffer.readu8(b, o), o + 1 end, + [2] = function(b, o) return buffer.readi8(b, o), o + 1 end, + [3] = function(b, o) return buffer.readu16(b, o), o + 2 end, + [4] = function(b, o) return buffer.readi16(b, o), o + 2 end, + [5] = function(b, o) return buffer.readu32(b, o), o + 4 end, + [6] = function(b, o) return buffer.readi32(b, o), o + 4 end, + [7] = function(b, o) return buffer.readf32(b, o), o + 4 end, + [8] = function(b, o) return buffer.readf64(b, o), o + 8 end, +} + +local function readF16(b: buffer, o: number): number + return f16ToF32(buffer.readu16(b, o)) +end + +local unpackValue: (buf: buffer, pos: number, refs: {any}?) -> (any, number) + +unpackValue = function(buf: buffer, pos: number, refs: {any}?): (any, number) + local t = buffer.readu8(buf, pos) + pos += 1 + + -- + fixint (0-127) + if t <= 0x7F then + return t, pos + end + + -- - fixint (-32 to -1) + if t >= 0x80 and t <= 0x9F then + return t - 0x80 - 32, pos + end + + if t == T_NIL then return nil, pos end + if t == T_FALSE then return false, pos end + if t == T_TRUE then return true, pos end + + if t == T_U8 then return buffer.readu8(buf, pos), pos + 1 end + if t == T_U16 then return buffer.readu16(buf, pos), pos + 2 end + if t == T_U32 then return buffer.readu32(buf, pos), pos + 4 end + if t == T_I8 then return buffer.readi8(buf, pos), pos + 1 end + if t == T_I16 then return buffer.readi16(buf, pos), pos + 2 end + if t == T_I32 then return buffer.readi32(buf, pos), pos + 4 end + if t == T_F32 then return buffer.readf32(buf, pos), pos + 4 end + if t == T_F64 then return buffer.readf64(buf, pos), pos + 8 end + + -- inline str len (0-15) + if t >= T_STR_BASE and t <= T_STR_BASE + 15 then + local n = t - T_STR_BASE + return buffer.readstring(buf, pos, n), pos + n + end + + if t == T_STR8 then + local n = buffer.readu8(buf, pos) + return buffer.readstring(buf, pos + 1, n), pos + 1 + n + end + if t == T_STR16 then + local n = buffer.readu16(buf, pos) + return buffer.readstring(buf, pos + 2, n), pos + 2 + n + end + if t == T_STRVAR then + local n; n, pos = readVarUInt(buf, pos) + return buffer.readstring(buf, pos, n), pos + n + end + + -- inline array len (0-7) + if t >= T_ARR_BASE and t <= T_ARR_BASE + 7 then + local count = t - T_ARR_BASE + local arr = table.create(count) + for i = 1, count do + arr[i], pos = unpackValue(buf, pos, refs) + end + return arr, pos + end + + if t == T_ARR8 then + local count = buffer.readu8(buf, pos); pos += 1 + local arr = table.create(count) + for i = 1, count do + arr[i], pos = unpackValue(buf, pos, refs) + end + return arr, pos + end + if t == T_ARR16 then + local count = buffer.readu16(buf, pos); pos += 2 + local arr = table.create(count) + for i = 1, count do + arr[i], pos = unpackValue(buf, pos, refs) + end + return arr, pos + end + if t == T_ARRVAR then + local count; count, pos = readVarUInt(buf, pos) + local arr = table.create(count) + for i = 1, count do + arr[i], pos = unpackValue(buf, pos, refs) + end + return arr, pos + end + + -- inline map len (0-7) + if t >= T_MAP_BASE and t <= T_MAP_BASE + 7 then + local count = t - T_MAP_BASE + local map = {} + for _ = 1, count do + local k, v + k, pos = unpackValue(buf, pos, refs) + v, pos = unpackValue(buf, pos, refs) + map[k] = v + end + return map, pos + end + + if t == T_MAP8 then + local count = buffer.readu8(buf, pos); pos += 1 + local map = {} + for _ = 1, count do + local k, v + k, pos = unpackValue(buf, pos, refs) + v, pos = unpackValue(buf, pos, refs) + map[k] = v + end + return map, pos + end + if t == T_MAP16 then + local count = buffer.readu16(buf, pos); pos += 2 + local map = {} + for _ = 1, count do + local k, v + k, pos = unpackValue(buf, pos, refs) + v, pos = unpackValue(buf, pos, refs) + map[k] = v + end + return map, pos + end + if t == T_MAPVAR then + local count; count, pos = readVarUInt(buf, pos) + local map = {} + for _ = 1, count do + local k, v + k, pos = unpackValue(buf, pos, refs) + v, pos = unpackValue(buf, pos, refs) + map[k] = v + end + return map, pos + end + + -- typed array + if t == T_TYPED_ARR then + local subtype = buffer.readu8(buf, pos); pos += 1 + local count; count, pos = readVarUInt(buf, pos) + local reader = TYPED_READERS[subtype] + local arr = table.create(count) + for i = 1, count do + arr[i], pos = reader(buf, pos) + end + return arr, pos + end + + -- Vector3 + if t == T_VEC3 then + local x = buffer.readf32(buf, pos) + local y = buffer.readf32(buf, pos + 4) + local z = buffer.readf32(buf, pos + 8) + return Vector3.new(x, y, z), pos + 12 + end + if t == T_VEC3_F16 then + local x = readF16(buf, pos) + local y = readF16(buf, pos + 2) + local z = readF16(buf, pos + 4) + return Vector3.new(x, y, z), pos + 6 + end + + -- Vector2 + if t == T_VEC2 then + local x = buffer.readf32(buf, pos) + local y = buffer.readf32(buf, pos + 4) + return Vector2.new(x, y), pos + 8 + end + if t == T_VEC2_F16 then + local x = readF16(buf, pos) + local y = readF16(buf, pos + 2) + return Vector2.new(x, y), pos + 4 + end + + -- CFrame + if t == T_CFRAME then + local px = buffer.readf32(buf, pos) + local py = buffer.readf32(buf, pos + 4) + local pz = buffer.readf32(buf, pos + 8) + local rx = buffer.readf32(buf, pos + 12) + local ry = buffer.readf32(buf, pos + 16) + local rz = buffer.readf32(buf, pos + 20) + return CFrame.new(px, py, pz) * CFrame.fromOrientation(rx, ry, rz), pos + 24 + end + if t == T_CFRAME_F16 then + local px = readF16(buf, pos) + local py = readF16(buf, pos + 2) + local pz = readF16(buf, pos + 4) + local rx = readF16(buf, pos + 6) + local ry = readF16(buf, pos + 8) + local rz = readF16(buf, pos + 10) + return CFrame.new(px, py, pz) * CFrame.fromOrientation(rx, ry, rz), pos + 12 + end + + -- Color3 + if t == T_COLOR3 then + local r = buffer.readu8(buf, pos) + local g = buffer.readu8(buf, pos + 1) + local b = buffer.readu8(buf, pos + 2) + return Color3.fromRGB(r, g, b), pos + 3 + end + if t == T_COLOR3_F then + local r = buffer.readf32(buf, pos) + local g = buffer.readf32(buf, pos + 4) + local b = buffer.readf32(buf, pos + 8) + return Color3.new(r, g, b), pos + 12 + end + + if t == T_BRICKCOLOR then + return BrickColor.new(buffer.readu16(buf, pos)), pos + 2 + end + + if t == T_INSTANCE then + local idx; idx, pos = readVarUInt(buf, pos) + return refs and refs[idx] or nil, pos + end + + if t == T_ENUMITEM then + local nameLen; nameLen, pos = readVarUInt(buf, pos) + local enumName = buffer.readstring(buf, pos, nameLen) + pos += nameLen + local val = buffer.readu16(buf, pos) + return (Enum :: any)[enumName]:FromValue(val), pos + 2 + end + + if t == T_ENUM then + local nameLen; nameLen, pos = readVarUInt(buf, pos) + local enumName = buffer.readstring(buf, pos, nameLen) + return (Enum :: any)[enumName], pos + nameLen + end + + if t == T_UDIM2 then + local xs = buffer.readf32(buf, pos) + local xo = buffer.readi32(buf, pos + 4) + local ys = buffer.readf32(buf, pos + 8) + local yo = buffer.readi32(buf, pos + 12) + return UDim2.new(xs, xo, ys, yo), pos + 16 + end + + if t == T_UDIM then + local s = buffer.readf32(buf, pos) + local o = buffer.readi32(buf, pos + 4) + return UDim.new(s, o), pos + 8 + end + + if t == T_RECT then + return Rect.new( + buffer.readf32(buf, pos), buffer.readf32(buf, pos + 4), + buffer.readf32(buf, pos + 8), buffer.readf32(buf, pos + 12) + ), pos + 16 + end + + if t == T_NUMBERRANGE then + return NumberRange.new(buffer.readf32(buf, pos), buffer.readf32(buf, pos + 4)), pos + 8 + end + + if t == T_RAY then + return Ray.new( + Vector3.new(buffer.readf32(buf, pos), buffer.readf32(buf, pos + 4), buffer.readf32(buf, pos + 8)), + Vector3.new(buffer.readf32(buf, pos + 12), buffer.readf32(buf, pos + 16), buffer.readf32(buf, pos + 20)) + ), pos + 24 + end + + if t == T_COLSEQ then + local count = buffer.readu8(buf, pos); pos += 1 + local keypoints = table.create(count) + for i = 1, count do + local time = buffer.readf32(buf, pos) + local r = buffer.readu8(buf, pos + 4) + local g = buffer.readu8(buf, pos + 5) + local b = buffer.readu8(buf, pos + 6) + keypoints[i] = ColorSequenceKeypoint.new(time, Color3.fromRGB(r, g, b)) + pos += 7 + end + return ColorSequence.new(keypoints), pos + end + + if t == T_NUMSEQ then + local count = buffer.readu8(buf, pos); pos += 1 + local keypoints = table.create(count) + for i = 1, count do + local time = buffer.readf32(buf, pos) + local value = buffer.readf32(buf, pos + 4) + local envelope = buffer.readf32(buf, pos + 8) + keypoints[i] = NumberSequenceKeypoint.new(time, value, envelope) + pos += 12 + end + return NumberSequence.new(keypoints), pos + end + + error(`(serdes) unknown type: {t}`) +end + +local function build(w: Writer): buffer + local result = buffer.create(w.cursor) + buffer.copy(result, 0, w.buf, 0, w.cursor) + return result +end + +local function buildWithRefs(w: Writer): (buffer, {any}?) + local result = buffer.create(w.cursor) + buffer.copy(result, 0, w.buf, 0, w.cursor) + return result, #w.refs > 0 and table.clone(w.refs) or nil +end + +local function reset(w: Writer) + w.cursor = 0 + table.clear(w.refs) +end + +local BufferSerdes = {} + +function BufferSerdes.write(data: any): (buffer, {any}?) + local w = createWriter() + packValue(w, data) + return buildWithRefs(w) +end + +function BufferSerdes.read(buf: buffer, refs: {any}?): any + return (unpackValue(buf, 0, refs)) +end + +function BufferSerdes.readAll(buf: buffer, refs: {any}?): {any} + local bufLen = buffer.len(buf) + local pos = 0 + local results = {} + while pos < bufLen do + local value + value, pos = unpackValue(buf, pos, refs) + table.insert(results, value) + end + return results +end + +BufferSerdes.createWriter = createWriter +BufferSerdes.pack = packValue +BufferSerdes.build = build +BufferSerdes.buildWithRefs = buildWithRefs +BufferSerdes.reset = reset + +BufferSerdes.varUIntSize = varUIntSize +BufferSerdes.writeVarUInt = writeVarUInt +BufferSerdes.readVarUInt = readVarUInt +BufferSerdes.f32ToF16 = f32ToF16 +BufferSerdes.f16ToF32 = f16ToF32 + +return BufferSerdes :: typeof(BufferSerdes) diff --git a/src/Client/init.luau b/src/Client/init.luau new file mode 100644 index 0000000..9e05011 --- /dev/null +++ b/src/Client/init.luau @@ -0,0 +1,169 @@ +--!optimize 2 +--!strict +--@EternityDev +local Client = {} + +local RunService = game:GetService("RunService") +local Thread = require("./Thread") +local Buffer = require("./Buffer") +local Event: RemoteEvent = script.Parent:WaitForChild("Event") +local Function: RemoteFunction = script.Parent:WaitForChild("Function") +local deltaT: number, cycle: number = 0, 1 / 61 +local writer: Buffer.Writer = Buffer.createWriter() + +type Connection = { + Connected: boolean, + Disconnect: (self: Connection) -> (), +} +type Event = { + remote: string, + fn: (Player, ...any?) -> (...any?), +} + +local queueEvent: { { any } } = {} +local eventListeners: { Event } = {} + +local pendingInvokes: { [string]: thread } = {} +local invokeHandlers: { [string]: (...any?) -> ...any? } = {} +local invokeId = 0 + +Client.Connect = function(remoteName: string, fn: (Player, ...any?) -> (...any?)): Connection + local detail = { + remote = remoteName, + fn = fn, + } + table.insert(eventListeners, detail) + return { + Connected = true, + Disconnect = function(self: Connection) + if not self.Connected then return end + self.Connected = false + local idx = table.find(eventListeners, detail) + if idx then + table.remove(eventListeners, idx) + end + end, + } :: Connection +end + +Client.Once = function(remoteName: string, fn: (...any?) -> ()): Connection + local connection + connection = Client.Connect(remoteName, function(...: any?) + if connection then + connection:Disconnect() + end + fn(...) + end) + return connection +end + +Client.Wait = function(remoteName: string): (number, ...any?) + local thread, t = coroutine.running(), os.clock() + Client.Once(remoteName, function(...: any?) + task.spawn(thread, os.clock()-t, ...) + end) + return coroutine.yield() +end + +Client.DisconnectAll = function(remoteName: string) + for idx = #eventListeners, 1, -1 do + if eventListeners[idx].remote == remoteName then + table.remove(eventListeners, idx) + end + end +end + +Client.Destroy = function(remoteName: string) + Client.DisconnectAll(remoteName) +end + +Client.Fire = function(remoteName: string, ...: any?) + table.insert(queueEvent, { + remoteName, + { ... } :: any + }) +end + +Client.Invoke = function(remoteName: string, timeout: number?, ...: any?): ...any? + invokeId += 1 + local invokeId, thread = `{invokeId}`, coroutine.running() + + pendingInvokes[invokeId] = thread + task.delay(timeout or 2, function() + local pending = pendingInvokes[invokeId] + if not pending then + return + end + task.spawn(pending, nil) + pendingInvokes[invokeId] = nil + end) + + table.insert(queueEvent, { + "\0", + { remoteName, invokeId, { ... } :: any } :: any + }) + return coroutine.yield() +end + +if RunService:IsClient() then + Event.OnClientEvent:Connect(function(b: buffer, ref: { Instance? }) + if type(b) ~= "buffer" then return end + local contents = Buffer.read(b, ref) + for _, content in contents do + local remote = content[1] + local content = content[2] + if remote == "\1" then + local invokeId = content[1] + local results = content[2] + local pending = pendingInvokes[invokeId] + if pending then + task.spawn(pending :: any, table.unpack(results)) + pendingInvokes[invokeId] = nil + end + continue + end + if #eventListeners == 0 then continue end + if remote == "\0" then + local remoteName = content[1] + local invokeId = content[2] + local args = content[3] + for _, connection in eventListeners do + if connection.remote == remoteName then + Thread(function() + local results = { connection.fn(table.unpack(args)) } + table.insert(queueEvent, { + "\1", + { invokeId, results } :: any + }) + end) + break + end + end + continue + end + for _, connection in eventListeners do + if connection.remote ~= remote then continue end + Thread(connection.fn, table.unpack(content)) + end + end + end) + RunService.PostSimulation:Connect(function(d: number) + deltaT += d + if deltaT < cycle then return end + deltaT = 0 + if #queueEvent == 0 then return end + Buffer.pack(writer, queueEvent) + do + local buf, ref = Buffer.buildWithRefs(writer) + Buffer.reset(writer) + if not ref or #ref == 0 then + Event:FireServer(buf) + else + Event:FireServer(buf, ref) + end + end + table.clear(queueEvent) + end) +end + +return Client :: typeof(Client) diff --git a/src/Index/Client/ClientProcess/init.luau b/src/Index/Client/ClientProcess/init.luau deleted file mode 100644 index 7712826..0000000 --- a/src/Index/Client/ClientProcess/init.luau +++ /dev/null @@ -1,264 +0,0 @@ ---!native ---!strict ---!optimize 2 -local ClientProcess = {} - -local RunService = game:GetService("RunService") -local Util = script.Parent.Parent.Util - -local Type = require(script.Parent.Parent.Type) -local Event = require(script.Parent.Parent.Event) -local Spawn = require(Util.Spawn) -local Key = require(Util.Key) -local RateLimit = require(Util.RateLimit) -local Buffer = require(Util.Buffer) - -local clientRatelimit: Type.StoredRatelimit = {} -local clientQueue: Type.QueueMap = {} -local unreliableClientQueue: Type.QueueMap = {} -local clientCallback: Type.CallbackMap = {} -local clientRequestQueue: Type.QueueMap = {} -local registeredIdentifier: { [string]: boolean } = {} - -local queueInRequest: { - [number]: { - [string]: { - any - } - } -} = {} -local queueOutRequest: { - [number]: { - [string]: { - any - } - } -} = {} - -queueInRequest[1] = {} -queueInRequest[2] = {} -queueOutRequest[1] = {} -queueOutRequest[2] = {} - -local ReliableEvent = Event.Reliable -local UnreliableEvent = Event.Unreliable -local RequestEvent = Event.Request - -function ClientProcess.insertQueue(Identifier: string, reliable: boolean, ...: any) - if not reliable then - if not unreliableClientQueue[Identifier] then - unreliableClientQueue[Identifier] = {} - end - table.insert(unreliableClientQueue[Identifier], { ... }) - return - end - if not clientQueue[Identifier] then - clientQueue[Identifier] = {} - end - table.insert(clientQueue[Identifier], { ... }) -end - -function ClientProcess.insertRequest(Identifier: string, timeout: number, ...: any) - if not clientRequestQueue[Identifier] then - clientRequestQueue[Identifier] = {} - end - local yieldThread: thread, start = coroutine.running(), os.clock() - local cancel = task.delay(timeout, function() - task.spawn(yieldThread, nil) - end) - table.insert(clientRequestQueue[Identifier], { tostring(Key()), function(...: any) - if (os.clock() - start) > timeout then return end - task.cancel(cancel) - task.spawn(yieldThread, ...) - end :: any, { ... } :: any }) - return coroutine.yield() -end - -function ClientProcess.add(Identifier: any, originId: string, conf: Type.ClientConf) - if not registeredIdentifier[Identifier] then - registeredIdentifier[Identifier] = true - - if not clientRatelimit[Identifier] then - clientRatelimit[Identifier] = RateLimit.create(originId) - end - if not clientQueue[Identifier] then - clientQueue[Identifier] = {} - end - if not unreliableClientQueue[Identifier] then - unreliableClientQueue[Identifier] = {} - end - if not clientRequestQueue[Identifier] then - clientRequestQueue[Identifier] = {} - end - if not clientCallback[Identifier] then - clientCallback[Identifier] = {} - end - - if not queueOutRequest[1][Identifier] then - queueOutRequest[1][Identifier] = {} - end - if not queueOutRequest[2][Identifier] then - queueOutRequest[2][Identifier] = {} - end - if not queueInRequest[1][Identifier] then - queueInRequest[1][Identifier] = {} - end - if not queueInRequest[2][Identifier] then - queueInRequest[2][Identifier] = {} - end - end -end - -function ClientProcess.remove(Identifier: string) - if not registeredIdentifier[Identifier] then return end - registeredIdentifier[Identifier] = nil - clientQueue[Identifier] = nil - unreliableClientQueue[Identifier] = nil - clientRequestQueue[Identifier] = nil - clientCallback[Identifier] = nil - clientRatelimit[Identifier] = nil - queueOutRequest[1][Identifier] = nil - queueOutRequest[2][Identifier] = nil - queueInRequest[1][Identifier] = nil - queueInRequest[2][Identifier] = nil -end - -function ClientProcess.addCallback(Identifier: string, key: string, callback) - clientCallback[Identifier][key] = callback -end - -function ClientProcess.removeCallback(Identifier: string, key: string) - clientCallback[Identifier][key] = nil -end - -function ClientProcess.start() - debug.setmemorycategory("Warp") - RunService.PostSimulation:Connect(function() - -- Unreliable - for Identifier: string, data: any in unreliableClientQueue do - if #data == 0 then continue end - if clientRatelimit[Identifier](#data) then - for _, unpacked in data do - UnreliableEvent:FireServer(Buffer.revert(Identifier), Buffer.write(unpacked)) - end - end - unreliableClientQueue[Identifier] = nil - end - -- Reliable - for Identifier: string, data: any in clientQueue do - if #data > 0 then - if clientRatelimit[Identifier](#data) then - for _, unpacked in data do - ReliableEvent:FireServer(Buffer.revert(Identifier), Buffer.write(unpacked)) - end - end - clientQueue[Identifier] = nil - end - end - -- Sent new invokes - for Identifier: string, requestsData in queueOutRequest[1] do - if #requestsData == 0 then continue end - RequestEvent:FireServer(Buffer.revert(Identifier), "\1", requestsData) - queueOutRequest[1][Identifier] = nil - end - -- Sent returning invokes - for Identifier: string, toReturnDatas in queueOutRequest[2] do - if #toReturnDatas == 0 then continue end - RequestEvent:FireServer(Buffer.revert(Identifier), "\0", toReturnDatas) - queueOutRequest[2][Identifier] = nil - end - - for Identifier: string in registeredIdentifier do - if clientRequestQueue[Identifier] then - for _, requestData in clientRequestQueue[Identifier] do - if not requestData[3] then continue end - if not queueOutRequest[1][Identifier] then - queueOutRequest[1][Identifier] = {} - end - table.insert(queueOutRequest[1][Identifier], { requestData[1], requestData[3] }) - requestData[3] = nil - end - end - - -- Unreliable & Reliable - local callback = clientCallback[Identifier] or nil - if not callback then continue end - - -- Return Invoke - if queueInRequest[1][Identifier] then - for _, packetDatas: any in queueInRequest[1][Identifier] do - if #packetDatas == 0 then continue end - for _, fn: any in callback do - for i=1,#packetDatas do - if not packetDatas[i] then continue end - local packetData1 = packetDatas[i][1] - local packetData2 = packetDatas[i][2] - Spawn(function() - local requestReturn = { fn(table.unpack(packetData2)) } - if not queueOutRequest[2][Identifier] then - queueOutRequest[2][Identifier] = {} - end - table.insert(queueOutRequest[2][Identifier], { packetData1, requestReturn }) - packetData1 = nil - packetData2 = nil - end) - end - end - end - queueInRequest[1][Identifier] = nil - end - - -- Call to Invoke - if queueInRequest[2][Identifier] then - if clientRequestQueue[Identifier] then - for _, packetDatas: any in queueInRequest[2][Identifier] do - for _, packetData in packetDatas do - if #packetData == 1 then continue end - for y=1,#clientRequestQueue[Identifier] do - local clientRequest = clientRequestQueue[Identifier][y] - if not clientRequest then continue end - if clientRequest[1] == packetData[1] then - Spawn(clientRequest[2], table.unpack(packetData[2])) - table.remove(clientRequestQueue[Identifier], y) - break - end - end - end - end - end - queueInRequest[2][Identifier] = nil - end - end - end) - local function onClientNetworkReceive(Identifier: buffer | string, data: buffer, ref: { any }?) - if not Identifier or typeof(Identifier) ~= "buffer" or not data or typeof(data) ~= "buffer" then return end - Identifier = Buffer.convert(Identifier) - if not registeredIdentifier[Identifier :: string] then return end - local read = Buffer.read(data, ref) - if not read then return end - local callback = clientCallback[Identifier :: string] - if not callback then return end - for _, fn: any in callback do - Spawn(fn, table.unpack(read)) - end - end - ReliableEvent.OnClientEvent:Connect(onClientNetworkReceive) - UnreliableEvent.OnClientEvent:Connect(onClientNetworkReceive) - RequestEvent.OnClientEvent:Connect(function(Identifier: any, action: string, data) - if not Identifier or not data then return end - Identifier = Buffer.convert(Identifier) - if action == "\1" then - if not queueInRequest[1][Identifier] then - queueInRequest[1][Identifier] = {} - end - table.insert(queueInRequest[1][Identifier], data) - else - if not queueInRequest[2][Identifier] then - queueInRequest[2][Identifier] = {} - end - table.insert(queueInRequest[2][Identifier], data) - end - end) -end - -return ClientProcess \ No newline at end of file diff --git a/src/Index/Client/Index.luau b/src/Index/Client/Index.luau deleted file mode 100644 index 7fe5e9e..0000000 --- a/src/Index/Client/Index.luau +++ /dev/null @@ -1,90 +0,0 @@ ---!strict ---!native ---!optimize 2 -local Client = {} -Client.__index = Client - -local Players = game:GetService("Players") -local Util = script.Parent.Parent.Util - -local Type = require(script.Parent.Parent.Type) -local ClientProcess = require(script.Parent.ClientProcess) -local Assert = require(Util.Assert) -local Key = require(Util.Key) -local Serdes = require(Util.Serdes) -local Buffer = require(Util.Buffer) - -function Client.new(Identifier: string, conf: Type.ClientConf?) - local self = setmetatable({}, Client) - - self._buffer = Buffer.new() - self._buffer:wu8(Serdes.increment(Identifier, conf and conf.yieldWait)) - self.id = Buffer.convert(self._buffer:build()) - self.fn = {} - self._conf = table.freeze(conf or {}) - self.IsConnected = false - - ClientProcess.add(self.id, Identifier, conf or { yieldWait = 10 }) - self._buffer:remove() - - return self -end - -function Client:Fire(reliable: boolean,...: any) - ClientProcess.insertQueue(self.id, reliable, ...) -end - -function Client:Invoke(timeout: number, ...: any): any - return ClientProcess.insertRequest(self.id, timeout, ...) -end - -function Client:Connect(callback: (args: any) -> ()): string - local key = tostring(Key()) - table.insert(self.fn, key) - self.IsConnected = #self.fn > 0 - ClientProcess.addCallback(self.id, key, callback) - return key -end - -function Client:Once(callback: (args: any) -> ()): string - local key = tostring(Key()) - table.insert(self.fn, key) - self.IsConnected = #self.fn > 0 - ClientProcess.addCallback(self.id, key, function(...: any?) - self:Disconnect(key) - task.spawn(callback, ...) - end) - return key -end - -function Client:Wait() - local thread: thread, t = coroutine.running(), os.clock() - self:Once(function() - task.spawn(thread, os.clock()-t) - end) - return coroutine.yield() -end - -function Client:DisconnectAll() - for _, key: string in self.fn do - self:Disconnect(key) - end -end - -function Client:Disconnect(key: string) - Assert(typeof(key) == "string", "Key must be a string type.") - ClientProcess.removeCallback(self.id, key) - table.remove(self.fn, table.find(self.fn, key)) - self.IsConnected = #self.fn > 0 -end - -function Client:Destroy() - self:DisconnectAll() - self._buffer:remove() - ClientProcess.remove(self.id) - Serdes.decrement() - table.clear(self) - setmetatable(self, nil) -end - -return Client.new \ No newline at end of file diff --git a/src/Index/Event.luau b/src/Index/Event.luau deleted file mode 100644 index 909aed9..0000000 --- a/src/Index/Event.luau +++ /dev/null @@ -1,24 +0,0 @@ ---!strict ---!optimize 2 -local RunService = game:GetService("RunService") -local Type = require(script.Parent.Type) - -if RunService:IsServer() then - if not script:FindFirstChild("Reliable") then - Instance.new("RemoteEvent", script).Name = "Reliable" - end - if not script:FindFirstChild("Unreliable") then - Instance.new("UnreliableRemoteEvent", script).Name = "Unreliable" - end - if not script:FindFirstChild("Request") then - Instance.new("RemoteEvent", script).Name = "Request" - end -elseif not script:FindFirstChild("Reliable") or not script:FindFirstChild("Unreliable") or not script:FindFirstChild("Request") then - repeat task.wait() until script:FindFirstChild("Reliable") and script:FindFirstChild("Unreliable") and script:FindFirstChild("Request") -end - -return { - Reliable = script.Reliable, - Unreliable = script.Unreliable, - Request = script.Request -} :: Type.Event \ No newline at end of file diff --git a/src/Index/Server/Index.luau b/src/Index/Server/Index.luau deleted file mode 100644 index 9890f42..0000000 --- a/src/Index/Server/Index.luau +++ /dev/null @@ -1,111 +0,0 @@ ---!strict ---!native ---!optimize 2 -local Server = {} -Server.__index = Server - -local Players = game:GetService("Players") -local Util = script.Parent.Parent.Util - -local Type = require(script.Parent.Parent.Type) -local ServerProcess = require(script.Parent.ServerProcess) -local Assert = require(Util.Assert) -local Key = require(Util.Key) -local Serdes = require(Util.Serdes) -local Buffer = require(Util.Buffer) - -function Server.new(Identifier: string, conf: Type.ServerConf?) - local self = setmetatable({}, Server) - - self._buffer = Buffer.new() - self._buffer:wu8(Serdes.increment(Identifier)) - self.id = Buffer.convert(self._buffer:build()) - self.fn = {} - self._conf = table.freeze(conf or {}) - self.IsConnected = false - - ServerProcess.add(self.id, Identifier, conf or { rateLimit = { maxEntrance = 200, interval = 2 } }) - self._buffer:remove() - - return self -end - -function Server:Fire(reliable: boolean, player: Player, ...: any) - ServerProcess.insertQueue(self.id, reliable, player, ...) -end - -function Server:Fires(reliable: boolean, ...: any) - for _, player: Player in ipairs(Players:GetPlayers()) do - ServerProcess.insertQueue(self.id, reliable, player, ...) - end -end - -function Server:FireExcept(reliable: boolean, except: { Player }, ...: any) - for _, player: Player in ipairs(Players:GetPlayers()) do - if table.find(except, player) then continue end - ServerProcess.insertQueue(self.id, reliable, player, ...) - end -end - -function Server:FireIn(reliable: boolean, range: number, from: Vector3, data: { any }, except: { Player }?) - for _, player: Player in ipairs(Players:GetPlayers()) do - if (except and table.find(except, player)) or not player.Character or not player.Character.PrimaryPart or (player.Character.PrimaryPart.Position - from).Magnitude < range then continue end - ServerProcess.insertQueue(self.id, reliable, player, table.unpack(data)) - end -end - -function Server:Invoke(timeout: number, player: Player, ...: any): any - return ServerProcess.insertRequest(self.id, timeout, player, ...) -end - -function Server:Connect(callback: (plyer: Player, args: any) -> ()): string - local key = tostring(Key()) - table.insert(self.fn, key) - ServerProcess.addCallback(self.id, key, callback) - self.IsConnected = #self.fn > 0 - return key -end - -function Server:Once(callback: (plyer: Player, args: any) -> ()): string - local key = tostring(Key()) - table.insert(self.fn, key) - self.IsConnected = #self.fn > 0 - ServerProcess.addCallback(self.id, key, function(player: Player, ...: any?) - self:Disconnect(key) - task.spawn(callback, player, ...) - end) - return key -end - -function Server:Wait() - local thread: thread, t = coroutine.running(), os.clock() - self:Once(function() - task.spawn(thread, os.clock()-t) - end) - return coroutine.yield() -end - -function Server:DisconnectAll() - for _, key: string in self.fn do - self:Disconnect(key) - end -end - -function Server:Disconnect(key: string): boolean - Assert(typeof(key) == "string", "Key must be a string type.") - ServerProcess.removeCallback(self.id, key) - table.remove(self.fn, table.find(self.fn, key)) - self.IsConnected = #self.fn > 0 - return table.find(self.fn, key) == nil -end - -function Server:Destroy() - self:DisconnectAll() - self._buffer:remove() - ServerProcess.remove(self.id) - Serdes.decrement() - table.clear(self) - setmetatable(self, nil) -end - -return Server.new \ No newline at end of file diff --git a/src/Index/Server/ServerProcess/init.luau b/src/Index/Server/ServerProcess/init.luau deleted file mode 100644 index 4a5385c..0000000 --- a/src/Index/Server/ServerProcess/init.luau +++ /dev/null @@ -1,371 +0,0 @@ ---!native ---!strict ---!optimize 2 -local ServerProcess = {} - -local RunService = game:GetService("RunService") -local Players = game:GetService("Players") -local Util = script.Parent.Parent.Util - -local Type = require(script.Parent.Parent.Type) -local Event = require(script.Parent.Parent.Event) -local Spawn = require(Util.Spawn) -local Key = require(Util.Key) -local RateLimit = require(Util.RateLimit) -local Buffer = require(Util.Buffer) - -local serverQueue: Type.QueueMap = {} -local unreliableServerQueue: Type.QueueMap = {} -local serverCallback: Type.CallbackMap = {} -local serverRequestQueue: Type.QueueMap = {} -local registeredIdentifier: { [string]: boolean } = {} - -local queueOut: { - [Player]: { - [string]: {any}, - } -} = {} -local queueInRequest: { - [number]: { - [string]: { - [Player]: {any} - } - } -} = {} -local queueOutRequest: { - [number]: { - [string]: { - [Player]: {any} - } - } -} = {} - -queueInRequest[1] = {} -queueInRequest[2] = {} -queueOutRequest[1] = {} -queueOutRequest[2] = {} - -local ReliableEvent = Event.Reliable -local UnreliableEvent = Event.Unreliable -local RequestEvent = Event.Request - -RateLimit.Protect() - -local function initializeEachPlayer(player: Player) - if not player then return end - if not queueOut[player] then - queueOut[player] = {} - end - for Identifier: string in registeredIdentifier do - if not player then break end - if not queueOut[player][Identifier] then - queueOut[player][Identifier] = {} - end - if not serverRequestQueue[Identifier] then - serverRequestQueue[Identifier] = {} - end - if not serverRequestQueue[Identifier][player] then - serverRequestQueue[Identifier][player] = {} - end - if not queueOutRequest[1][Identifier] then - queueOutRequest[1][Identifier] = {} - end - if not queueOutRequest[2][Identifier] then - queueOutRequest[2][Identifier] = {} - end - if not queueInRequest[1][Identifier][player] then - queueInRequest[1][Identifier][player] = {} - queueInRequest[2][Identifier][player] = {} - end - if not queueOutRequest[1][Identifier][player] then - queueOutRequest[1][Identifier][player] = {} - queueOutRequest[2][Identifier][player] = {} - end - end -end - -Players.PlayerAdded:Connect(initializeEachPlayer) -Players.PlayerRemoving:Connect(function(player: Player) - if not player then return end - if queueOut[player] then - queueOut[player] = nil - end - for _, map in { serverQueue, unreliableServerQueue, serverRequestQueue } do - for Identifier: string in map do - map[Identifier][player] = nil - end - end - for i=1,2 do - for Identifier: string in queueInRequest[i] do - if queueInRequest[i][Identifier][player] then - queueInRequest[i][Identifier][player] = nil - end - end - for Identifier: string in queueOutRequest[i] do - if queueOutRequest[i][Identifier][player] then - queueOutRequest[i][Identifier][player] = nil - end - end - end -end) - -function ServerProcess.insertQueue(Identifier: string, reliable: boolean, player: Player, ...: any) - if not reliable then - if not unreliableServerQueue[Identifier] then - unreliableServerQueue[Identifier] = {} - end - if not unreliableServerQueue[Identifier][player] then - unreliableServerQueue[Identifier][player] = {} - end - table.insert(unreliableServerQueue[Identifier][player], { ... }) - return - end - if not serverQueue[Identifier] then - serverQueue[Identifier] = {} - end - if not serverQueue[Identifier][player] then - serverQueue[Identifier][player] = {} - end - table.insert(serverQueue[Identifier][player], { ... }) -end - -function ServerProcess.insertRequest(Identifier: string, timeout: number, player: Player, ...: any) - if not serverRequestQueue[Identifier] then - serverRequestQueue[Identifier] = {} - end - if not serverRequestQueue[Identifier][player] then - serverRequestQueue[Identifier][player] = {} - end - local yieldThread: thread, start = coroutine.running(), os.clock() - local cancel = task.delay(timeout, function() - task.spawn(yieldThread, nil) - end) - table.insert(serverRequestQueue[Identifier][player], { tostring(Key()), function(...: any) - if (os.clock() - start) > timeout then return end - task.cancel(cancel) - task.spawn(yieldThread, ...) - end :: any, { ... } :: any }) - return coroutine.yield() -end - -function ServerProcess.add(Identifier: string, originId: string, conf: Type.ServerConf) - if not registeredIdentifier[Identifier] then - registeredIdentifier[Identifier] = true - - RateLimit.create(originId, conf.rateLimit and conf.rateLimit.maxEntrance or 200, conf.rateLimit and conf.rateLimit.interval or 2) - - if not serverQueue[Identifier] then - serverQueue[Identifier] = {} - end - if not serverRequestQueue[Identifier] then - serverRequestQueue[Identifier] = {} - end - if not serverCallback[Identifier] then - serverCallback[Identifier] = {} - end - if not unreliableServerQueue[Identifier] then - unreliableServerQueue[Identifier] = {} - end - - if not queueInRequest[1][Identifier] then - queueInRequest[1][Identifier] = {} - end - if not queueInRequest[2][Identifier] then - queueInRequest[2][Identifier] = {} - end - if not queueOutRequest[1][Identifier] then - queueOutRequest[1][Identifier] = {} - end - if not queueOutRequest[2][Identifier] then - queueOutRequest[2][Identifier] = {} - end - - for _, player: Player in ipairs(Players:GetPlayers()) do - task.spawn(initializeEachPlayer, player) - end - end -end - -function ServerProcess.remove(Identifier: string) - if not registeredIdentifier[Identifier] then return end - registeredIdentifier[Identifier] = nil - serverQueue[Identifier] = nil - serverRequestQueue[Identifier] = nil - serverCallback[Identifier] = nil - unreliableServerQueue[Identifier] = nil - queueInRequest[1][Identifier] = nil - queueInRequest[2][Identifier] = nil - queueOutRequest[1][Identifier] = nil - queueOutRequest[2][Identifier] = nil -end - -function ServerProcess.addCallback(Identifier: string, key: string, callback) - serverCallback[Identifier][key] = callback -end - -function ServerProcess.removeCallback(Identifier: string, key: string) - serverCallback[Identifier][key] = nil -end - -function ServerProcess.start() - debug.setmemorycategory("Warp") - RunService.PostSimulation:Connect(function() - -- Unreliable - for Identifier: string, players in unreliableServerQueue do - for player: Player, content: any in players do - if #content == 0 then continue end - for _, unpacked in content do - UnreliableEvent:FireClient(player, Buffer.revert(Identifier), Buffer.write(unpacked)) - end - unreliableServerQueue[Identifier][player] = nil - end - unreliableServerQueue[Identifier] = nil - end - -- Reliable - for Identifier: string, contents: { [Player]: { any } } in serverQueue do - for player, content: any in contents do - if #content > 0 and queueOut[player] then - for _, unpacked in content do - ReliableEvent:FireClient(player, Buffer.revert(Identifier), Buffer.write(unpacked)) - end - end - serverQueue[Identifier][player] = nil - end - serverQueue[Identifier] = nil - end - -- Sent new invokes - for Identifier: string, contents in queueOutRequest[1] do - for player: Player, requestsData: any in contents do - if #requestsData > 0 then - RequestEvent:FireClient(player, Buffer.revert(Identifier), "\1", requestsData) - end - queueOutRequest[1][Identifier][player] = nil - end - queueOutRequest[1][Identifier] = nil - end - -- Sent returning invokes - for Identifier: string, contents in queueOutRequest[2] do - for player: Player, toReturnDatas: any in contents do - if #toReturnDatas > 0 then - RequestEvent:FireClient(player, Buffer.revert(Identifier), "\0", toReturnDatas) - end - queueOutRequest[2][Identifier][player] = nil - end - queueOutRequest[2][Identifier] = nil - end - - for Identifier: string in registeredIdentifier do - if serverRequestQueue[Identifier] then - for player, content in serverRequestQueue[Identifier] do - if #content == 0 then serverRequestQueue[Identifier][player] = nil continue end - for _, requestData in content do - if not requestData[3] then continue end - if not queueOutRequest[1][Identifier] then - queueOutRequest[1][Identifier] = {} - end - if not queueOutRequest[1][Identifier][player] then - queueOutRequest[1][Identifier][player] = {} - end - table.insert(queueOutRequest[1][Identifier][player], { requestData[1], requestData[3] }) - requestData[3] = nil - end - end - end - - local callback = serverCallback[Identifier] or nil - if not callback then continue end - - -- Return Invoke - for player, content in queueInRequest[1][Identifier] do - if not callback then break end - for _, packetDatas in content do - if not callback then break end - if #packetDatas == 0 then continue end - for _, fn: any in callback do - for i=1,#packetDatas do - if not packetDatas[i] then continue end - local packetData1 = packetDatas[i][1] - local packetData2 = packetDatas[i][2] - Spawn(function() - local requestReturn = { fn(player, table.unpack(packetData2)) } - if not queueOutRequest[2][Identifier] then - queueOutRequest[2][Identifier] = {} - end - if not queueOutRequest[2][Identifier][player] then - queueOutRequest[2][Identifier][player] = {} - end - table.insert(queueOutRequest[2][Identifier][player], { packetData1, requestReturn }) - packetData1 = nil - packetData2 = nil - end) - end - end - end - queueInRequest[1][Identifier][player] = nil - end - - -- Call to Invoke - for player, content in queueInRequest[2][Identifier] do - if not callback then break end - for _, packetDatas in content do - for _, packetData in packetDatas do - if not callback then break end - if #packetData == 1 then continue end - local data = serverRequestQueue[Identifier][player] - for i=1,#data do - local serverRequest = data[i] - if not serverRequest then continue end - if serverRequest[1] == packetData[1] then - Spawn(serverRequest[2], table.unpack(packetData[2])) - table.remove(data, i) - break - end - end - end - end - queueInRequest[2][Identifier][player] = nil - end - end - end) - local function onServerNetworkReceive(player: Player, Identifier: buffer | string, data: buffer, ref: { any }?) - if not Identifier or typeof(Identifier) ~= "buffer" or not data or typeof(data) ~= "buffer" then return end - Identifier = Buffer.convert(Identifier :: buffer) - if not registeredIdentifier[Identifier :: string] then return end - local read = Buffer.read(data, ref) - if not read then return end - local callback = serverCallback[Identifier :: string] - if not callback then return end - for _, fn: any in callback do - Spawn(fn, player, table.unpack(read)) - end - end - ReliableEvent.OnServerEvent:Connect(onServerNetworkReceive) - UnreliableEvent.OnServerEvent:Connect(onServerNetworkReceive) - RequestEvent.OnServerEvent:Connect(function(player: Player, Identifier: any, action: string, data: any) - if not Identifier or not data then return end - Identifier = Buffer.convert(Identifier) - if not queueInRequest[1][Identifier][player] then - queueInRequest[1][Identifier][player] = {} - end - if not queueInRequest[2][Identifier][player] then - queueInRequest[2][Identifier][player] = {} - end - if not serverQueue[Identifier] then - serverQueue[Identifier] = {} - end - if not serverQueue[Identifier][player] then - serverQueue[Identifier][player] = {} - end - if action == "\1" then - table.insert(queueInRequest[1][Identifier][player], data) - else - table.insert(queueInRequest[2][Identifier][player], data) - end - end) -end - -for _, player: Player in ipairs(Players:GetPlayers()) do - task.spawn(initializeEachPlayer, player) -end - -return ServerProcess \ No newline at end of file diff --git a/src/Index/Signal/Dedicated.luau b/src/Index/Signal/Dedicated.luau deleted file mode 100644 index 8ffcf52..0000000 --- a/src/Index/Signal/Dedicated.luau +++ /dev/null @@ -1,19 +0,0 @@ ---!strict ---!native ---!optimize 2 -local Dedicated = {} -Dedicated.__index = Dedicated - -function Dedicated.new(signal: any, handler: (...any) -> ()) - return setmetatable({ - fn = handler, - root = signal, - }, Dedicated) -end - -function Dedicated:Disconnect() - table.clear(self) - setmetatable(self, nil) -end - -return Dedicated.new :: typeof(Dedicated.new) \ No newline at end of file diff --git a/src/Index/Signal/init.luau b/src/Index/Signal/init.luau deleted file mode 100644 index 858fb8e..0000000 --- a/src/Index/Signal/init.luau +++ /dev/null @@ -1,92 +0,0 @@ ---!strict ---!native ---!optimize 2 -local Signal = {} -Signal.__index = Signal - -local DedicatedSignal = require(script.Dedicated) - -local Util = script.Parent.Util -local Key = require(Util.Key) -local Assert = require(Util.Assert) - -local Signals = {} - -function Signal.new(Identifier: string) - Assert(typeof(Identifier) == "string", `[Signal]: Identifier must be a string type, got {typeof(Identifier)}`) - if not Signals[Identifier] then - local signal = setmetatable({}, Signal) - Signals[Identifier] = signal - return signal - end - return Signals[Identifier] -end - -function Signal:Connect(fn: (...any) -> (), optKey: string?): string - local key: typeof(Signal) = optKey or tostring(Key()) :: any - self[key] = DedicatedSignal(self, fn) - return key :: any -end - -function Signal:Once(fn: (...any) -> ()): string - local key: string - key = self:Connect(function(...: any) - self:Disconnect(key) - task.spawn(fn, ...) - end) - return key -end - -function Signal:Disconnect(key: string) - if not self[key] then return end - self[key]:Disconnect() - self[key] = nil -end - -function Signal:DisconnectAll(): () - table.clear(self) -end - -function Signal:Wait(): number - local t, thread = os.clock(), coroutine.running() - self:Once(function() - task.spawn(thread, os.clock()-t) - end) - return coroutine.yield() -end - -function Signal:DeferFire(...: any): () - for _, handle in self do - task.defer(handle.fn, ...) - end -end - -function Signal:Fire(...: any): () - for _, handle in self do - task.spawn(handle.fn, ...) - end -end - -function Signal:FireTo(signal: string, ...: any): () - local to = Signals[signal] - if not to then return end - Signal.Fire(to, ...) -end - -function Signal:Invoke(key: string, ...: any): () - local to = self[key] - if not to then return end - return to.fn(...) -end - -function Signal:InvokeTo(signal: string, key: string, ...: any): () - if not Signals[signal] then return end - return Signal.Invoke(Signals[signal], key, ...) -end - -function Signal:Destroy(): () - self:DisconnectAll() - setmetatable(self, nil) -end - -return Signal.new :: typeof(Signal.new) \ No newline at end of file diff --git a/src/Index/Type.luau b/src/Index/Type.luau deleted file mode 100644 index 779b627..0000000 --- a/src/Index/Type.luau +++ /dev/null @@ -1,89 +0,0 @@ ---!strict -type rateLimitArg = { - maxEntrance: number?, - interval: number?, -} - -export type ServerConf = { - rateLimit: rateLimitArg?, -} - -export type ClientConf = { - yieldWait: number?, -} - -export type Middleware = { - middleware: (self: Middleware, middleware: (...any) -> (...any)) -> (), - key: (self: Middleware) -> string, - destroy: (self: Middleware) -> (), -} - -export type Client = { - Fire: (self: Client, reliable: boolean, ...any) -> (), - Invoke: (self: Client, timeout: number, ...any) -> any, - Connect: (self: Client, callback: (...any) -> ()) -> Middleware, - Once: (self: Client, callback: (player: Player, ...any) -> ()) -> Middleware, - Disconnect: (self: Client, key: string) -> (), - DisconnectAll: (self: Client) -> (), - Wait: (self: Client) -> number, - Destroy: (self: Client) -> (), -} - -export type Server = { - Fire: (self: Server, reliable: boolean, player: Player, ...any) -> (), - Fires: (self: Server, reliable: boolean, ...any) -> (), - Invoke: (self: Server, timeout: number, player: Player, ...any) -> any, - Connect: (self: Server, callback: (player: Player, ...any) -> ()) -> Middleware, - Once: (self: Server, callback: (player: Player, ...any) -> ()) -> Middleware, - Disconnect: (self: Server, key: string) -> (), - DisconnectAll: (self: Server) -> (), - Wait: (self: Server) -> number, - Destroy: (self: Server) -> (), -} - -export type Signal = { - Fire: (self: Signal, ...any) -> (), - FireTo: (self: Signal, Identifier: string, ...any) -> (), - Invoke: (self: Signal, ...any) -> (...any?), - InvokeTo: (self: Signal, Identifier: string, ...any) -> (...any?), - Connect: (self: Signal, callback: (...any) -> ()) -> string, - Once: (self: Signal, callback: (...any) -> ()) -> string, - Disconnect: (self: Signal, key: string) -> (), - DisconnectAll: (self: Signal) -> (), - Wait: (self: Signal) -> number, - Destroy: (self: Signal) -> (), -} - -export type Event = { - Reliable: RemoteEvent, - Unreliable: UnreliableRemoteEvent, - Request: RemoteEvent, -} - -export type QueueMap = { - [string]: { - [any]: any, - } -} - -export type CallbackMap = { - [string]: (any), -} - -export type StoredRatelimit = { - [string]: any -} - -export type fromServerArray = { - [string]: Server -} - -export type fromClientArray = { - [string]: Client -} - -export type fromSignalArray = { - [string]: Signal -} - -return nil \ No newline at end of file diff --git a/src/Index/Util/Assert.luau b/src/Index/Util/Assert.luau deleted file mode 100644 index 770df1a..0000000 --- a/src/Index/Util/Assert.luau +++ /dev/null @@ -1,6 +0,0 @@ ---!strict ---!native ---!optimize 2 -return function(condition: (any), errorMessage: string, level: number?): () - if not (condition) then error(`Warp: {errorMessage}`, level or 2) end -end \ No newline at end of file diff --git a/src/Index/Util/Buffer/Dedicated.luau b/src/Index/Util/Buffer/Dedicated.luau deleted file mode 100644 index 7b3dcc4..0000000 --- a/src/Index/Util/Buffer/Dedicated.luau +++ /dev/null @@ -1,265 +0,0 @@ ---!strict ---!native ---!optimize 2 -local DedicatedBuffer = {} -DedicatedBuffer.__index = DedicatedBuffer - -local create = buffer.create -local copy = buffer.copy -local writei8 = buffer.writei8 -local writei16 = buffer.writei16 -local writei32 = buffer.writei32 -local writeu8 = buffer.writeu8 -local writeu16 = buffer.writeu16 -local writeu32 = buffer.writeu32 -local writef32 = buffer.writef32 -local writef64 = buffer.writef64 -local writestring = buffer.writestring - -local default: { [string]: number } = { - point = 0, - next = 0, - size = 128, - bufferSize = 128, -} - -function DedicatedBuffer.copy(self: any, offset: number, b: buffer?, src: buffer?, srcOffset: number?, count: number?) - if not b then - copy(create(count or default.size), offset, src or self.buffer, srcOffset, count) - else - copy(b, offset, src or self.buffer, srcOffset, count) - end -end - -function DedicatedBuffer.alloc(self: any, byte: number) - local size: number = self.size - local b: buffer = self.buffer - - while self.next + byte >= size do - size = math.floor(size * 1.25) -- +25% increase - end - local newBuffer: buffer = create(size) - copy(newBuffer, 0, b) - - b = newBuffer - - self.point = self.next - self.buffer = b - self.next += byte -end - -function DedicatedBuffer.build(self: any): buffer - local p: number = self.next > self.point and self.next or self.point - local build: buffer = create(p) - - copy(build, 0, self.buffer, 0, p) - return build -end - -function DedicatedBuffer.buildAndRemove(self: any): (buffer, (any)?) - local p: number = self.next > self.point and self.next or self.point - local build: buffer = create(p) - local ref = #self.ref > 0 and table.clone(self.ref) or nil - - copy(build, 0, self.buffer, 0, p) - - self:remove() - return build, ref -end - -function DedicatedBuffer.wi8(self: any, val: number, alloc: number?) - if not val then return end - self:alloc(alloc or 1) - writei8(self.buffer, self.point, val) -end - -function DedicatedBuffer.wi16(self: any, val: number, alloc: number?) - if not val then return end - self:alloc(alloc or 2) - writei16(self.buffer, self.point, val) -end - -function DedicatedBuffer.wi32(self: any, val: number, alloc: number?) - if not val then return end - self:alloc(alloc or 4) - writei32(self.buffer, self.point, val) -end - -function DedicatedBuffer.wu8(self: any, val: number, alloc: number?) - if not val then return end - self:alloc(alloc or 1) - writeu8(self.buffer, self.point, val) -end - -function DedicatedBuffer.wu16(self: any, val: number, alloc: number?) - if not val then return end - self:alloc(alloc or 2) - writeu16(self.buffer, self.point, val) -end - -function DedicatedBuffer.wu32(self: any, val: number, alloc: number?) - if not val then return end - self:alloc(alloc or 4) - writeu32(self.buffer, self.point, val) -end - -function DedicatedBuffer.wf32(self: any, val: number, alloc: number?) - if not val then return end - self:alloc(alloc or 4) - writef32(self.buffer, self.point, val) -end - -function DedicatedBuffer.wf64(self: any, val: number, alloc: number?) - if not val then return end - self:alloc(alloc or 8) - writef64(self.buffer, self.point, val) -end - -function DedicatedBuffer.wstring(self: any, val: string) - if not val then return end - self:alloc(#val) - writestring(self.buffer, self.point, val) -end - -function DedicatedBuffer.wType(self: any, ref: number) - writeu8(self.buffer, self.point, ref) - self.point += 1 -end - -function DedicatedBuffer.wRef(self: any, value: any, alloc: number?) - if not value then return end - self:alloc(alloc or 1) - table.insert(self.ref, value) - local index = #self.ref - writeu8(self.buffer, self.point, index) - self.point += 1 -end - -function DedicatedBuffer.pack(self: any, data: {any}) - if typeof(data) == "nil" then - self:wi8(0) - elseif typeof(data) == "Instance" then - self:wi8(-1) -- Instance marker - self:wRef(data) - elseif typeof(data) == "table" then - --local isArray = (next(data) ~= nil and #data > 0) and true or false - local isArray = true - local count = 0 - for k in data do - count += 1 - if typeof(k) ~= "number" or math.floor(k) ~= k then - isArray = false - end - end - if isArray then - self:wi8(-2) -- array marker - self:wu16(count) -- use 32-bit length - for _, v in data do - self:pack(v) - end - else - self:wi8(-3) -- dictionary marker - self:wu16(count) -- number of key-value pairs - for k, v in data do - self:pack(k) -- pack the key - self:pack(v) -- pack the value - end - end - elseif typeof(data) == "EnumItem" then - self:wi8(-4) - self:wi8(#`{data.EnumType}`) - self:wstring(`{data.EnumType}`) - self:wu8(data.Value) - elseif typeof(data) == "BrickColor" then - self:wi8(-5) - self:wi16(data.Number) - elseif typeof(data) == "Enum" then - self:wi8(-6) - self:wi8(#`{data}`) - self:wstring(`{data}`) - elseif typeof(data) == "number" then - if math.floor(data) == data then -- Integer - if data >= 0 and data <= 255 then - self:wi8(1) -- u8 marker - self:wu8(data) - elseif data >= -32768 and data <= 32767 then - self:wi8(2) -- i16 marker - self:wi16(data) - elseif data >= -2147483647 and data <= 2147483647 then - self:wi8(3) -- i32 marker - self:wi32(data) - else - self:wi8(4) -- f64 marker - self:wf64(data) - end - else - self:wi8(4) -- f64 marker - self:wf64(data) - end - elseif typeof(data) == "boolean" then - self:wi8(5) -- boolean marker - self:wu8(data and 1 or 0) - elseif typeof(data) == "string" then - local length = #data - if length <= 255 then - self:wi8(6) - self:wu8(length) - elseif length <= 65535 then - self:wi8(7) - self:wu16(length) - else - self:wi8(8) - self:wi32(length) - end - self:wstring(data) - elseif typeof(data) == "Vector3" then - self:wi8(9) -- Vector3 marker - self:wf32(data.X) - self:wf32(data.Y) - self:wf32(data.Z) - elseif typeof(data) == "Vector2" then - self:wi8(10) -- Vector2 marker - self:wf32(data.X) - self:wf32(data.Y) - elseif typeof(data) == "CFrame" then - self:wi8(11) -- CFrame marker - for _, v in {data:GetComponents()} do - self:wf32(v) - end - elseif typeof(data) == "Color3" then - self:wi8(12) -- Color3 marker - self:wu8(data.R * 255) - self:wu8(data.G * 255) - self:wu8(data.B * 255) - else - warn(`Unsupported data type: {typeof(data)} value: {data}`) - end -end - -function DedicatedBuffer.flush(self: any) - self.point = default.point - self.next = default.next - self.size = default.size - self.buffer = create(default.bufferSize) - table.clear(self.ref) -end - -function DedicatedBuffer.new() - return setmetatable({ - point = default.point, - next = default.next, - size = default.size, - buffer = create(default.bufferSize), - ref = {}, - }, DedicatedBuffer) -end - -function DedicatedBuffer.remove(self: any) - self:flush() - table.clear(self) - setmetatable(self, nil) -end - -export type DedicatedType = typeof(DedicatedBuffer.new()) - -return DedicatedBuffer.new :: typeof(DedicatedBuffer.new) \ No newline at end of file diff --git a/src/Index/Util/Buffer/init.luau b/src/Index/Util/Buffer/init.luau deleted file mode 100644 index 0d84811..0000000 --- a/src/Index/Util/Buffer/init.luau +++ /dev/null @@ -1,149 +0,0 @@ ---!strict ---!native ---!optimize 2 -local Buffer = {} -Buffer.__index = Buffer - -local Dedicated = require(script.Dedicated) - -local tostring = buffer.tostring -local fromstring = buffer.fromstring -local readu8 = buffer.readu8 -local readi8 = buffer.readi8 -local readu16 = buffer.readu16 -local readi16 = buffer.readi16 -local readi32 = buffer.readi32 -local readf32 = buffer.readf32 -local readf64 = buffer.readf64 -local readstring = buffer.readstring -local len = buffer.len - -local function readValue(b: buffer, position: number, ref: { any }?): (any, number) - local typeByte = readi8(b, position) - position += 1 - if typeByte == 0 then -- nil - return nil, position - elseif typeByte == -1 then -- Instance - if not ref or #ref == 0 then - return nil, position + 1 - end - local value = ref[readu8(b, position)] - if typeof(value) == "Instance" then - return value, position + 1 - end - return nil, position + 1 - elseif typeByte == -2 then -- array - local length = readu16(b, position) - position += 2 - local array = {} - for _ = 1, length do - local value - value, position = readValue(b, position, ref) - table.insert(array, value) - end - return array, position - elseif typeByte == -3 then -- dictionary - local length = readu16(b, position) - position += 2 - local dict = {} - for _ = 1, length do - local key, value - key, position = readValue(b, position, ref) - value, position = readValue(b, position, ref) - dict[key] = value - end - return dict, position - elseif typeByte == -4 then -- EnumItem - local length = readi8(b, position) - local value = readstring(b, position + 1, length) - local value2 = readu8(b, position + 1 + length) - return Enum[value]:FromValue(value2), position + 2 + length - elseif typeByte == -5 then -- BrickColor - local value = readi16(b, position) - return BrickColor.new(value), position + 2 - elseif typeByte == -6 then -- Enum - local length = readi8(b, position) - local value = readstring(b, position + 1, length) - return Enum[value], position + 1 + length - elseif typeByte == 1 then -- int u8 - local value = readu8(b, position) - return value, position + 1 - elseif typeByte == 2 then -- int i16 - local value = readi16(b, position) - return value, position + 2 - elseif typeByte == 3 then -- int i32 - local value = readi32(b, position) - return value, position + 4 - elseif typeByte == 4 then -- f64 - local value = readf64(b, position) - return value, position + 8 - elseif typeByte == 5 then -- boolean - local value = readu8(b, position) == 1 - return value, position + 1 - elseif typeByte == 6 then -- string u8 - local length = readu8(b, position) - local value = readstring(b, position + 1, length) - return value, position + length + 1 - elseif typeByte == 7 then -- string u16 - local length = readu16(b, position) - local value = readstring(b, position + 2, length) - return value, position + length + 2 - elseif typeByte == 8 then -- string i32 - local length = readi32(b, position) - local value = readstring(b, position + 4, length) - return value, position + length + 4 - elseif typeByte == 9 then -- Vector3 - local x = readf32(b, position) - local y = readf32(b, position + 4) - local z = readf32(b, position + 8) - return Vector3.new(x, y, z), position + 12 - elseif typeByte == 10 then -- Vector2 - local x = readf32(b, position) - local y = readf32(b, position + 8) - return Vector2.new(x, y), position + 8 - elseif typeByte == 11 then -- CFrame - local components = {} - for i = 1, 12 do - table.insert(components, readf32(b, position + (i - 1) * 4)) - end - return CFrame.new(unpack(components)), position + 48 - elseif typeByte == 12 then -- Color3 - local r = readu8(b, position) - local g = readu8(b, position + 1) - local b = readu8(b, position + 2) - return Color3.fromRGB(r, g, b), position + 3 - end - error(`Unsupported type marker: {typeByte}`) -end - -function Buffer.new(): Dedicated.DedicatedType - return Dedicated() -end - -function Buffer.convert(b: buffer): string - return tostring(b) -end - -function Buffer.revert(s: string): buffer - return fromstring(s) -end - -function Buffer.write(data: { any }): (buffer, (any)?) - local newBuffer = Dedicated() - newBuffer:pack(data) - return newBuffer:buildAndRemove() -end - -function Buffer.read(b: buffer, ref: { any }?): any? - local position = 0 - local result = {} - while position < len(b) do - local value - value, position = readValue(b, position, ref) - table.insert(result, value) - end - ref = nil - return table.unpack(result) -end - -return Buffer :: typeof(Buffer) \ No newline at end of file diff --git a/src/Index/Util/Key.luau b/src/Index/Util/Key.luau deleted file mode 100644 index 46a40c4..0000000 --- a/src/Index/Util/Key.luau +++ /dev/null @@ -1,5 +0,0 @@ ---!strict ---!optimize 2 -return function(): number? - return tonumber(string.sub(tostring(Random.new():NextNumber()), 3, 8)) -- 6 digits -end \ No newline at end of file diff --git a/src/Index/Util/RateLimit.luau b/src/Index/Util/RateLimit.luau deleted file mode 100644 index c259650..0000000 --- a/src/Index/Util/RateLimit.luau +++ /dev/null @@ -1,75 +0,0 @@ ---!strict ---!optimize 2 -local RateLimit = {} - -local RunService = game:GetService("RunService") -local Assert = require(script.Parent.Assert) -local Events = require(script.Parent.Parent.Event) -local Reliable, Unreliable, Request = Events.Reliable, Events.Unreliable, Events.Request -local Signal = require(script.Parent.Parent.Signal)("Warp_OnSpamSignal") - -local map, activity, meta = {}, {}, {} -setmetatable(meta , { - __index = map, - __newindex = function(self, key, value) - if not activity[key] then - activity[key] = os.clock() - end - if (os.clock()-activity[key]) >= 1 then - activity[key] = os.clock() - map[key] = 1 - return - end - if value >= 1e2 then -- 100 - Signal:Fire(key) - return - end - map[key] = value - end, -}) - -local function onReceived(player: Player) - if not meta[player] then - meta[player] = 1 - return - end - meta[player] += 1 -end - -function RateLimit.create(Identifier: string, entrance: number?, interval: number?) - Assert(typeof(Identifier) == "string", "Identifier must a string type.") - if RunService:IsServer() then - Assert(typeof(entrance) == "number", "entrance must a number type.") - Assert(entrance :: number > 0, "entrance must above 0.") - Reliable:SetAttribute(Identifier.."_ent", entrance) - Reliable:SetAttribute(Identifier.."_int", interval) - else - while (not Reliable:GetAttribute(Identifier.."_ent")) or (not Reliable:GetAttribute(Identifier.."_int")) do - task.wait(0.1) - end - entrance = tonumber(Reliable:GetAttribute(Identifier.."_ent")) - interval = tonumber(Reliable:GetAttribute(Identifier.."_int")) - end - local entrances: number = 0 - return function(incoming: number?): boolean - if entrances == 0 then - task.delay(interval, function() - entrances = 0 - end) - end - entrances += incoming or 1 - return (entrances <= entrance :: number) - end -end - -function RateLimit.Protect() - if not RunService:IsServer() or Reliable:GetAttribute("Protected") or Unreliable:GetAttribute("Protected") or Request:GetAttribute("Protected") then return end - Reliable:SetAttribute("Protected", true) - Unreliable:SetAttribute("Protected", true) - Request:SetAttribute("Protected", true) - Reliable.OnServerEvent:Connect(onReceived) - Unreliable.OnServerEvent:Connect(onReceived) - Request.OnServerEvent:Connect(onReceived) -end - -return RateLimit :: typeof(RateLimit) \ No newline at end of file diff --git a/src/Index/Util/Serdes.luau b/src/Index/Util/Serdes.luau deleted file mode 100644 index 1029139..0000000 --- a/src/Index/Util/Serdes.luau +++ /dev/null @@ -1,50 +0,0 @@ ---!strict ---!optimize 2 -local SerDes = {} -local RunService = game:GetService("RunService") -local SerInt = 0 - -local Event = require(script.Parent.Parent.Event).Reliable -local Assert = require(script.Parent.Assert) - -function SerDes.increment(Identifier: string, timeout: number?): number - Assert(typeof(Identifier) == "string", "Identifier must be a string type.") - if RunService:IsServer() then - Assert(SerInt < 255, "reached max 255 identifiers.") - if not Event:GetAttribute(Identifier) then - SerInt += 1 - Event:SetAttribute(`{SerInt}`, Identifier) - Event:SetAttribute(Identifier, SerInt) - --Event:SetAttribute(Identifier, string.pack("I1", SerInt)) -- I1 -> 255 max, I2 -> ~ 6.5e4 max. (SerInt), removed/disabled for buffer migration. - end - else - local yieldThread: thread = coroutine.running() - local cancel = task.delay(timeout or 10, function() -- yield cancelation (timerout) - task.spawn(yieldThread, nil) - error(`Serdes: {Identifier} is taking too long to retrieve, seems like it's not replicated on server.`, 2) - end) - task.spawn(function() - while coroutine.status(cancel) ~= "dead" and task.wait(0.04) do -- let it loop for yields! 1/24 - if Event:GetAttribute(Identifier) then - task.cancel(cancel) - task.spawn(yieldThread, Event:GetAttribute(Identifier)) - break - end - end - end) - return coroutine.yield() -- yield - end - return Event:GetAttribute(Identifier) -end - -function SerDes.decrement() - if not RunService:IsServer() or SerInt <= 0 then return end - local Identifier = Event:GetAttribute(`{SerInt}`) - if not Identifier then return end - Event:SetAttribute(`{Identifier}`, nil) - Event:SetAttribute(`{SerInt}`, nil) - SerInt -= 1 - Identifier = nil -end - -return SerDes :: typeof(SerDes) \ No newline at end of file diff --git a/src/Index/init.luau b/src/Index/init.luau deleted file mode 100644 index 13fa54e..0000000 --- a/src/Index/init.luau +++ /dev/null @@ -1,80 +0,0 @@ ---!strict ---!native ---!optimize 2 -local Index = {} - -local RunService = game:GetService("RunService") -local IsServer = RunService:IsServer() - -local Util = script.Util -local Server = script.Server -local Client = script.Client - -local Type = require(script.Type) -local Assert = require(Util.Assert) -local Signal = require(script.Signal) -local Buffer = require(Util.Buffer) - -if IsServer then - require(Server.ServerProcess).start() -else - require(Client.ClientProcess).start() -end - -function Index.Server(Identifier: string, conf: Type.ServerConf?): Type.Server - Assert(IsServer, `[Warp]: Calling .Server({Identifier}) on client side (expected server side)`) - Assert(typeof(Identifier) == "string", `[Warp]: Identifier must be a string type, got {typeof(Identifier)}`) - return require(Server.Index)(Identifier, conf) :: Type.Server -end -function Index.Client(Identifier: string, conf: Type.ClientConf?): Type.Client - Assert(not IsServer, `[Warp]: Calling .Client({Identifier}) on server side (expected client side)`) - Assert(typeof(Identifier) == "string", `[Warp]: Identifier must be a string type, got {typeof(Identifier)}`) - return require(Client.Index)(Identifier, conf) :: Type.Client -end - -function Index.fromServerArray(arrays: { string } | { [string]: Type.ServerConf }): Type.fromServerArray - Assert(IsServer, `[Warp]: Calling .fromServerArray({arrays}) on client side (expected server side)`) - Assert(typeof(arrays) == "table", "[Warp]: Array must be a table type, got {typeof(arrays)}") - local copy: { [string]: Type.Server } = {} - for param1, param2: string | Type.ServerConf in arrays do - if typeof(param2) == "table" then - copy[param1] = Index.Server(param1, param2) - else - copy[param2] = Index.Server(param2) - end - end - return copy -end - -function Index.fromClientArray(arrays: { string } | { [string]: Type.ClientConf }): Type.fromClientArray - Assert(not IsServer, `[Warp]: Calling .fromClientArray({arrays}) on server side (expected client side)`) - Assert(typeof(arrays) == "table", `[Warp]: Array must be a table type, got {typeof(arrays)}`) - local copy = {} - for param1, param2: string | Type.ClientConf in arrays do - if typeof(param2) == "table" then - copy[param1] = Index.Client(param1, param2) - else - copy[param2] = Index.Client(param2) - end - end - return copy -end - -function Index.Signal(Identifier: string) - return Signal(Identifier) -end - -function Index.fromSignalArray(arrays: { any }) - Assert(typeof(arrays) == "table", `[Warp]: Array must be a table type, got {typeof(arrays)}`) - local copy = {} - for _, identifier: string in arrays do - copy[identifier] = Index.Signal(identifier) - end - return copy :: typeof(copy) -end - -function Index.buffer() - return Buffer.new() -end - -return table.freeze(Index) :: typeof(Index) diff --git a/src/Server/init.luau b/src/Server/init.luau new file mode 100644 index 0000000..521aaca --- /dev/null +++ b/src/Server/init.luau @@ -0,0 +1,203 @@ +--!optimize 2 +--!strict +--@EternityDev +local Server = {} + +local Players = game:GetService("Players") +local RunService = game:GetService("RunService") +local Thread = require("./Thread") +local Buffer = require("./Buffer") +local Event: RemoteEvent = script.Parent:WaitForChild("Event") +local Function: RemoteFunction = script.Parent:WaitForChild("Function") +local deltaT: number, cycle: number = 0, 1 / 61 +local writer: Buffer.Writer = Buffer.createWriter() + +type Connection = { + Connected: boolean, + Disconnect: (self: Connection) -> (), +} +type Event = { + remote: string, + fn: (Player, ...any?) -> (...any?), +} + +local queueEvent: { + [Player]: { { any } }, +} = {} +local eventListeners: { Event } = {} +local players_ready: { Player } = {} + +local pendingInvokes: { [string]: thread } = {} +local invokeHandlers: { [string]: (...any?) -> ...any? } = {} +local invokeId = 0 + +Server.Connect = function(remoteName: string, fn: (Player, ...any?) -> (...any?)): Connection + local detail = { + remote = remoteName, + fn = fn, + } + table.insert(eventListeners, detail) + return { + Connected = true, + Disconnect = function(self: Connection) + if not self.Connected then return end + self.Connected = false + local idx = table.find(eventListeners, detail) + if idx then + table.remove(eventListeners, idx) + end + end, + } :: Connection +end + +Server.Once = function(remoteName: string, fn: (...any?) -> ()): Connection + local connection + connection = Server.Connect(remoteName, function(...: any?) + if connection then + connection:Disconnect() + end + fn(...) + end) + return connection +end + +Server.Wait = function(remoteName: string): (number, ...any?) + local thread, t = coroutine.running(), os.clock() + Server.Once(remoteName, function(...: any?) + task.spawn(thread, os.clock()-t, ...) + end) + return coroutine.yield() +end + +Server.DisconnectAll = function(remoteName: string) + for idx = #eventListeners, 1, -1 do + if eventListeners[idx].remote == remoteName then + table.remove(eventListeners, idx) + end + end +end + +Server.Destroy = function(remoteName: string) + Server.DisconnectAll(remoteName) +end + +Server.Fire = function(remoteName: string, reliable: boolean, player: Player, ...: any?) + if not queueEvent[player] then + queueEvent[player] = {} :: any + end + table.insert(queueEvent[player], { + remoteName, + { ... } :: any + }) +end + +Server.Fires = function(remoteName: string, reliable: boolean, ...: any?) + for _, player: Player in players_ready do + Server.Fire(remoteName, reliable, player, ...) + end +end + +Server.Invoke = function(remoteName: string, player: Player, timeout: number?, ...: any?): ...any? + invokeId += 1 + local invokeId, thread = `{invokeId}`, coroutine.running() + + pendingInvokes[invokeId] = thread + task.delay(timeout or 2, function() + local pending = pendingInvokes[invokeId] + if not pending then + return + end + task.spawn(pending, nil) + pendingInvokes[invokeId] = nil + end) + + table.insert(queueEvent[player], { + "\0", + { remoteName, invokeId, { ... } :: any } :: any + }) + return coroutine.yield() +end + +if RunService:IsServer() then + Event.OnServerEvent:Connect(function(player: Player, b: buffer, ref: { Instance? }) + if type(b) ~= "buffer" then return end + local contents = Buffer.read(b, ref) + for _, content in contents do + local remote = content[1] + local content = content[2] + if remote == "\1" then + local invokeId = content[1] + local results = content[2] + local pending = pendingInvokes[invokeId] + if pending then + task.spawn(pending :: any, table.unpack(results)) + pendingInvokes[invokeId] = nil + end + continue + end + if #eventListeners == 0 then continue end + if remote == "\0" then + local remoteName = content[1] + local invokeId = content[2] + local args = content[3] + for _, connection in eventListeners do + if connection.remote == remoteName then + Thread(function() + local results = { connection.fn(table.unpack(args)) } + table.insert(queueEvent[player], { + "\1", + { invokeId, results } :: any + }) + end) + break + end + end + continue + end + for _, connection in eventListeners do + if connection.remote ~= remote then continue end + Thread(connection.fn, player, table.unpack(content)) + end + end + end) + RunService.PostSimulation:Connect(function(d: number) + deltaT += d + if deltaT < cycle then return end + deltaT = 0 + for player: Player, content in queueEvent do + if #content == 0 or player.Parent ~= Players then continue end + Buffer.pack(writer, content) + do + local buf, ref = Buffer.buildWithRefs(writer) + Buffer.reset(writer) + if not ref or #ref == 0 then + Event:FireClient(player, buf) + else + Event:FireClient(player, buf, ref) + end + end + table.clear(queueEvent[player]) + end + end) + local function onAdded(player: Player) + if not table.find(players_ready, player) then + table.insert(players_ready, player) + end + if not queueEvent[player] then + queueEvent[player] = {} :: any + end + end + Players.PlayerAdded:Connect(onAdded) + Players.PlayerRemoving:Connect(function(player: Player) + table.remove(players_ready, table.find(players_ready, player)) + if queueEvent[player] then + table.clear(queueEvent[player]) + queueEvent[player] = nil + end + end) + for _, player: Player in ipairs(Players:GetPlayers()) do + onAdded(player) + end +end + +return Server :: typeof(Server) diff --git a/src/Index/Util/Spawn.luau b/src/Thread.luau similarity index 96% rename from src/Index/Util/Spawn.luau rename to src/Thread.luau index 29e6183..bffb8f5 100644 --- a/src/Index/Util/Spawn.luau +++ b/src/Thread.luau @@ -1,6 +1,6 @@ ---!native --!strict --!optimize 2 +--@EternityDev local thread: thread? local function passer(func: (T...) -> (), ...: T...): () @@ -18,4 +18,4 @@ end return function(func: (T...) -> (), ...: T...): () if not thread then task.spawn(newThread) end task.spawn(thread :: thread, func, ...) -end \ No newline at end of file +end diff --git a/src/init.luau b/src/init.luau index e4cb501..67f7973 100644 --- a/src/init.luau +++ b/src/init.luau @@ -1,20 +1,25 @@ --- Warp Library (@Eternity_Devs) --- version 1.0.14 --!strict ---!native ---!optimize 2 -local Index = require(script.Index) +--@EternityDev +local Remote = {} -return { - Server = Index.Server, - Client = Index.Client, - fromServerArray = Index.fromServerArray, - fromClientArray = Index.fromClientArray, - - OnSpamSignal = Index.OnSpamSignal, +if game.RunService:IsServer() then + if not script:FindFirstChild("Event") then + Instance.new("RemoteEvent", script).Name = "Event" + end + if not script:FindFirstChild("Function") then + Instance.new("RemoteFunction", script).Name = "Function" + end +end - Signal = Index.Signal, - fromSignalArray = Index.fromSignalArray, +local Client = require("@self/Client") +local Server = require("@self/Server") - buffer = Index.buffer, -} \ No newline at end of file +Remote.Client = function() + return Client +end + +Remote.Server = function() + return Server +end + +return Remote :: typeof(Remote)