feat: XOR encoding

This commit is contained in:
khtsly 2026-04-11 16:56:20 +07:00
parent 39143db390
commit ce8cfeef5e
4 changed files with 146 additions and 38 deletions

View file

@ -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"]}]}]} {"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"]}]}]}

View file

@ -3,10 +3,12 @@
--@EternityDev --@EternityDev
local Client = {} local Client = {}
local RunService = game:GetService("RunService")
local Thread = require("./Util/Thread") local Thread = require("./Util/Thread")
local Buffer = require("./Util/Buffer") local Buffer = require("./Util/Buffer")
local Replication = require("./Replication") local Replication = require("./Replication")
local Xor = require("./Util/Xor")
local RunService = game:GetService("RunService")
local Event: RemoteEvent = script.Parent:WaitForChild("Event") local Event: RemoteEvent = script.Parent:WaitForChild("Event")
local UnreliableEvent: UnreliableRemoteEvent = script.Parent:WaitForChild("UnreliableEvent") local UnreliableEvent: UnreliableRemoteEvent = script.Parent:WaitForChild("UnreliableEvent")
local deltaT: number, cycle: number = 0, 1 / 61 local deltaT: number, cycle: number = 0, 1 / 61
@ -26,6 +28,7 @@ local queueUnreliableEvent: { { any } } = {}
local eventListeners: { [number]: { Event } } = {} local eventListeners: { [number]: { Event } } = {}
local eventSchemas: { [number]: Buffer.SchemaType } = {} local eventSchemas: { [number]: Buffer.SchemaType } = {}
local lastDelta: { [number]: { any } } = {}
local pendingInvokes: { [string]: thread } = {} local pendingInvokes: { [string]: thread } = {}
local invokeId = 0 local invokeId = 0
@ -165,13 +168,12 @@ end
if RunService:IsClient() then if RunService:IsClient() then
local function processIncoming(b: buffer, ref: { Instance }?, handleInvokes: boolean) local function processIncoming(b: buffer, ref: { Instance }?, handleInvokes: boolean)
if type(b) ~= "buffer" then if type(b) ~= "buffer" then return end
return local decoded: buffer = handleInvokes and Xor.decodeClient(b) or b
end local contents = Buffer.readEvents(decoded, ref, eventSchemas)
local contents = Buffer.readEvents(b, ref, eventSchemas)
for _, content in contents do for _, content in contents do
local remote = content[1] local remote = content[1]
local content = content[2] local args = content[2]
if handleInvokes then if handleInvokes then
if remote == 0 then if remote == 0 then
local id = content[1] local id = content[1]
@ -184,17 +186,12 @@ if RunService:IsClient() then
continue continue
end end
if remote == 1 then if remote == 1 then
local remoteName = content[1] local remoteName, id, rargs = args[1], args[2], args[3]
local id = content[2]
local args = content[3]
local connections = eventListeners[remoteName] local connections = eventListeners[remoteName]
if connections and #connections > 0 then if connections and #connections > 0 then
Thread(function() Thread(function()
local results = { connections[1].c(table.unpack(args)) } local results = { connections[1].c(table.unpack(rargs)) }
table.insert(queueEvent, { table.insert(queueEvent, { 1, { id, results } :: any })
1,
{ id, results } :: any,
})
end) end)
end end
continue continue
@ -203,7 +200,7 @@ if RunService:IsClient() then
local connections = eventListeners[remote] local connections = eventListeners[remote]
if connections then if connections then
for _, connection in connections do for _, connection in connections do
Thread(connection.c, table.unpack(content)) Thread(connection.c, table.unpack(args))
end end
end end
end end
@ -219,9 +216,7 @@ if RunService:IsClient() then
RunService.PostSimulation:Connect(function(d: number) RunService.PostSimulation:Connect(function(d: number)
deltaT += d deltaT += d
if deltaT < cycle then if deltaT < cycle then return end
return
end
deltaT = 0 deltaT = 0
--reliable --reliable
@ -229,11 +224,12 @@ if RunService:IsClient() then
Buffer.writeEvents(writer, queueEvent, eventSchemas) Buffer.writeEvents(writer, queueEvent, eventSchemas)
do do
local buf, ref = Buffer.buildWithRefs(writer) local buf, ref = Buffer.buildWithRefs(writer)
local encoded = Xor.encodeClient(buf)
Buffer.reset(writer) Buffer.reset(writer)
if not ref or #ref == 0 then if not ref or #ref == 0 then
Event:FireServer(buf) Event:FireServer(encoded)
else else
Event:FireServer(buf, ref) Event:FireServer(encoded, ref)
end end
end end
table.clear(queueEvent) table.clear(queueEvent)

View file

@ -3,12 +3,14 @@
--@EternityDev --@EternityDev
local Server = {} local Server = {}
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local Thread = require("./Util/Thread") local Thread = require("./Util/Thread")
local Buffer = require("./Util/Buffer") local Buffer = require("./Util/Buffer")
local Identifier = require("./Util/Identifier") local Identifier = require("./Util/Identifier")
local Replication = require("./Replication") 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 Event: RemoteEvent = script.Parent:WaitForChild("Event")
local _repl: RemoteEvent = script.Parent:WaitForChild("_repl") local _repl: RemoteEvent = script.Parent:WaitForChild("_repl")
local UnreliableEvent: UnreliableRemoteEvent = script.Parent:WaitForChild("UnreliableEvent") local UnreliableEvent: UnreliableRemoteEvent = script.Parent:WaitForChild("UnreliableEvent")
@ -198,14 +200,15 @@ if RunService:IsServer() then
player_bytes[player] = bytes player_bytes[player] = bytes
end 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 for _, content in contents do
local remote = content[1] local remote = content[1]
local content = content[2] local d = content[2]
if handleInvokes then if handleInvokes then
if remote == 1 then if remote == 1 then
local id = content[1] local id = d[1]
local results = content[2] local results = d[2]
local pending = pendingInvokes[id] local pending = pendingInvokes[id]
if pending then if pending then
task.spawn(pending :: any, table.unpack(results)) task.spawn(pending :: any, table.unpack(results))
@ -217,9 +220,9 @@ if RunService:IsServer() then
if #eventListeners == 0 then if #eventListeners == 0 then
continue continue
end end
local remoteName = content[1] local remoteName = d[1]
local id = content[2] local id = d[2]
local args = content[3] local args = d[3]
local connections = eventListeners[remoteName] local connections = eventListeners[remoteName]
if connections and #connections > 0 then if connections and #connections > 0 then
Thread(function() Thread(function()
@ -239,7 +242,7 @@ if RunService:IsServer() then
local connections = eventListeners[remote] local connections = eventListeners[remote]
if connections then if connections then
for _, connection in connections do for _, connection in connections do
Thread(connection.c, player, table.unpack(content)) Thread(connection.c, player, table.unpack(d))
end end
end end
end end
@ -268,11 +271,12 @@ if RunService:IsServer() then
Buffer.writeEvents(writer, content, eventSchemas) Buffer.writeEvents(writer, content, eventSchemas)
do do
local buf, ref = Buffer.buildWithRefs(writer) local buf, ref = Buffer.buildWithRefs(writer)
local encoded = Xor.encodeServer(player, buf)
Buffer.reset(writer) Buffer.reset(writer)
if not ref or #ref == 0 then if not ref or #ref == 0 then
Event:FireClient(player, buf) Event:FireClient(player, encoded)
else else
Event:FireClient(player, buf, ref) Event:FireClient(player, encoded, ref)
end end
end end
player_bytes[player] = 0 player_bytes[player] = 0
@ -324,6 +328,7 @@ if RunService:IsServer() then
table.clear(queueUnreliableEvent[player]) table.clear(queueUnreliableEvent[player])
queueUnreliableEvent[player] = nil queueUnreliableEvent[player] = nil
end end
Xor.remove(player)
end) end)
for _, player: Player in ipairs(Players:GetPlayers()) do for _, player: Player in ipairs(Players:GetPlayers()) do
onAdded(player) onAdded(player)

107
src/Util/Xor.luau Normal file
View file

@ -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