diff --git a/sourcemap.json b/sourcemap.json index db9e1f3..3f2de71 100644 --- a/sourcemap.json +++ b/sourcemap.json @@ -1 +1 @@ -{"name":"warp","className":"ModuleScript","filePaths":["src/init.luau","default.project.json"],"children":[{"name":"Client","className":"ModuleScript","filePaths":["src/Client/init.luau"]},{"name":"Replication","className":"ModuleScript","filePaths":["src/Replication/init.luau"]},{"name":"Server","className":"ModuleScript","filePaths":["src/Server/init.luau"]},{"name":"Util","className":"Folder","children":[{"name":"Buffer","className":"ModuleScript","filePaths":["src/Util/Buffer/init.luau"]},{"name":"Identifier","className":"ModuleScript","filePaths":["src/Util/Identifier.luau"]},{"name":"Thread","className":"ModuleScript","filePaths":["src/Util/Thread.luau"]}]}]} \ No newline at end of file +{"name":"warp","className":"ModuleScript","filePaths":["src/init.luau","default.project.json"],"children":[{"name":"Client","className":"ModuleScript","filePaths":["src/Client/init.luau"]},{"name":"Replication","className":"ModuleScript","filePaths":["src/Replication/init.luau"]},{"name":"Server","className":"ModuleScript","filePaths":["src/Server/init.luau"]},{"name":"Util","className":"Folder","children":[{"name":"Buffer","className":"ModuleScript","filePaths":["src/Util/Buffer/init.luau"]},{"name":"Identifier","className":"ModuleScript","filePaths":["src/Util/Identifier.luau"]},{"name":"Thread","className":"ModuleScript","filePaths":["src/Util/Thread.luau"]},{"name":"Xor","className":"ModuleScript","filePaths":["src/Util/Xor.luau"]}]}]} \ No newline at end of file diff --git a/src/Client/init.luau b/src/Client/init.luau index 4d5f74c..2090a56 100644 --- a/src/Client/init.luau +++ b/src/Client/init.luau @@ -3,10 +3,12 @@ --@EternityDev local Client = {} -local RunService = game:GetService("RunService") local Thread = require("./Util/Thread") local Buffer = require("./Util/Buffer") local Replication = require("./Replication") +local Xor = require("./Util/Xor") + +local RunService = game:GetService("RunService") local Event: RemoteEvent = script.Parent:WaitForChild("Event") local UnreliableEvent: UnreliableRemoteEvent = script.Parent:WaitForChild("UnreliableEvent") local deltaT: number, cycle: number = 0, 1 / 61 @@ -26,6 +28,7 @@ local queueUnreliableEvent: { { any } } = {} local eventListeners: { [number]: { Event } } = {} local eventSchemas: { [number]: Buffer.SchemaType } = {} +local lastDelta: { [number]: { any } } = {} local pendingInvokes: { [string]: thread } = {} local invokeId = 0 @@ -165,13 +168,12 @@ end if RunService:IsClient() then local function processIncoming(b: buffer, ref: { Instance }?, handleInvokes: boolean) - if type(b) ~= "buffer" then - return - end - local contents = Buffer.readEvents(b, ref, eventSchemas) + if type(b) ~= "buffer" then return end + local decoded: buffer = handleInvokes and Xor.decodeClient(b) or b + local contents = Buffer.readEvents(decoded, ref, eventSchemas) for _, content in contents do local remote = content[1] - local content = content[2] + local args = content[2] if handleInvokes then if remote == 0 then local id = content[1] @@ -184,17 +186,12 @@ if RunService:IsClient() then continue end if remote == 1 then - local remoteName = content[1] - local id = content[2] - local args = content[3] + local remoteName, id, rargs = args[1], args[2], args[3] local connections = eventListeners[remoteName] if connections and #connections > 0 then Thread(function() - local results = { connections[1].c(table.unpack(args)) } - table.insert(queueEvent, { - 1, - { id, results } :: any, - }) + local results = { connections[1].c(table.unpack(rargs)) } + table.insert(queueEvent, { 1, { id, results } :: any }) end) end continue @@ -203,7 +200,7 @@ if RunService:IsClient() then local connections = eventListeners[remote] if connections then for _, connection in connections do - Thread(connection.c, table.unpack(content)) + Thread(connection.c, table.unpack(args)) end end end @@ -219,21 +216,20 @@ if RunService:IsClient() then RunService.PostSimulation:Connect(function(d: number) deltaT += d - if deltaT < cycle then - return - end + if deltaT < cycle then return end deltaT = 0 - - -- reliable + + --reliable if #queueEvent > 0 then Buffer.writeEvents(writer, queueEvent, eventSchemas) do local buf, ref = Buffer.buildWithRefs(writer) + local encoded = Xor.encodeClient(buf) Buffer.reset(writer) if not ref or #ref == 0 then - Event:FireServer(buf) + Event:FireServer(encoded) else - Event:FireServer(buf, ref) + Event:FireServer(encoded, ref) end end table.clear(queueEvent) diff --git a/src/Server/init.luau b/src/Server/init.luau index c7727f5..903e460 100644 --- a/src/Server/init.luau +++ b/src/Server/init.luau @@ -3,12 +3,14 @@ --@EternityDev local Server = {} -local Players = game:GetService("Players") -local RunService = game:GetService("RunService") local Thread = require("./Util/Thread") local Buffer = require("./Util/Buffer") local Identifier = require("./Util/Identifier") local Replication = require("./Replication") +local Xor = require("./Util/Xor") + +local Players = game:GetService("Players") +local RunService = game:GetService("RunService") local Event: RemoteEvent = script.Parent:WaitForChild("Event") local _repl: RemoteEvent = script.Parent:WaitForChild("_repl") local UnreliableEvent: UnreliableRemoteEvent = script.Parent:WaitForChild("UnreliableEvent") @@ -198,14 +200,15 @@ if RunService:IsServer() then player_bytes[player] = bytes end - local contents = Buffer.readEvents(b, ref, eventSchemas) + local decoded = handleInvokes and Xor.decodeServer(player, b) or b + local contents = Buffer.readEvents(decoded, ref, eventSchemas) for _, content in contents do local remote = content[1] - local content = content[2] + local d = content[2] if handleInvokes then if remote == 1 then - local id = content[1] - local results = content[2] + local id = d[1] + local results = d[2] local pending = pendingInvokes[id] if pending then task.spawn(pending :: any, table.unpack(results)) @@ -217,9 +220,9 @@ if RunService:IsServer() then if #eventListeners == 0 then continue end - local remoteName = content[1] - local id = content[2] - local args = content[3] + local remoteName = d[1] + local id = d[2] + local args = d[3] local connections = eventListeners[remoteName] if connections and #connections > 0 then Thread(function() @@ -239,7 +242,7 @@ if RunService:IsServer() then local connections = eventListeners[remote] if connections then for _, connection in connections do - Thread(connection.c, player, table.unpack(content)) + Thread(connection.c, player, table.unpack(d)) end end end @@ -268,11 +271,12 @@ if RunService:IsServer() then Buffer.writeEvents(writer, content, eventSchemas) do local buf, ref = Buffer.buildWithRefs(writer) + local encoded = Xor.encodeServer(player, buf) Buffer.reset(writer) if not ref or #ref == 0 then - Event:FireClient(player, buf) + Event:FireClient(player, encoded) else - Event:FireClient(player, buf, ref) + Event:FireClient(player, encoded, ref) end end player_bytes[player] = 0 @@ -324,6 +328,7 @@ if RunService:IsServer() then table.clear(queueUnreliableEvent[player]) queueUnreliableEvent[player] = nil end + Xor.remove(player) end) for _, player: Player in ipairs(Players:GetPlayers()) do onAdded(player) @@ -331,9 +336,9 @@ if RunService:IsServer() then end --[[ - @class Server - @schema - define a schema for your data and use a strict packing + @class Server + @schema + define a schema for your data and use a strict packing ]] Server.Schema = Buffer.Schema diff --git a/src/Util/Xor.luau b/src/Util/Xor.luau new file mode 100644 index 0000000..980a239 --- /dev/null +++ b/src/Util/Xor.luau @@ -0,0 +1,107 @@ +--!strict +--!native +--!optimize 2 +local Xor = {} + +type XorEntry = { buf: buffer, len: number } + +local serverPrev: { [Player]: XorEntry } = {} +local clientPrev: { [Player]: XorEntry } = {} + +local clientOutPrev: XorEntry? = nil +local clientInPrev: XorEntry? = nil + +local function xorApply(current: buffer, curLen: number, prev: buffer, prevLen: number): buffer + local out = buffer.create(curLen) + local common = if curLen < prevLen then curLen else prevLen + + local wordEnd = (common // 4) * 4 + for off = 0, wordEnd - 4, 4 do + buffer.writeu32(out, off, bit32.bxor(buffer.readu32(current, off), buffer.readu32(prev, off))) + end + for i = wordEnd, common - 1 do + buffer.writeu8(out, i, bit32.bxor(buffer.readu8(current, i), buffer.readu8(prev, i))) + end + + if curLen > common then + buffer.copy(out, common, current, common, curLen - common) + end + return out +end + +local function storeEntry(entry: XorEntry?, src: buffer, len: number): XorEntry + local e: XorEntry = entry or { buf = buffer.create(len > 64 and len or 64), len = 0 } + + if buffer.len(e.buf) < len then + e.buf = buffer.create(len * 2) + end + + buffer.copy(e.buf, 0, src, 0, len) + e.len = len + return e +end + +function Xor.remove(player: Player) + serverPrev[player] = nil + clientPrev[player] = nil +end + +function Xor.encodeServer(player: Player, buf: buffer): buffer + local len = buffer.len(buf) + local entry = serverPrev[player] + + if not entry then + serverPrev[player] = storeEntry(nil, buf, len) + return buf + end + + local encoded = xorApply(buf, len, entry.buf, entry.len) + serverPrev[player] = storeEntry(entry, buf, len) + return encoded +end + +function Xor.decodeClient(buf: buffer): buffer + local len = buffer.len(buf) + local prev = clientInPrev + + if not prev then + clientInPrev = storeEntry(nil, buf, len) + return buf + end + + local recovered = xorApply(buf, len, prev.buf, prev.len) + clientInPrev = storeEntry(prev, recovered, len) + return recovered +end + +function Xor.encodeClient(buf: buffer): buffer + local len = buffer.len(buf) + local prev = clientOutPrev + + if not prev then + clientOutPrev = storeEntry(nil, buf, len) + return buf + end + + local encoded = xorApply(buf, len, prev.buf, prev.len) + clientOutPrev = storeEntry(prev, buf, len) + return encoded +end + +function Xor.decodeServer(player: Player, buf: buffer): buffer + local len = buffer.len(buf) + local entry = clientPrev[player] + + if not entry then + clientPrev[player] = storeEntry(nil, buf, len) + return buf + end + + local recovered = xorApply(buf, len, entry.buf, entry.len) + clientPrev[player] = storeEntry(entry, recovered, len) + return recovered +end + +Xor._apply = xorApply + +return Xor