From 94177373b5e55731a6d77ea05cedd46805ec7e32 Mon Sep 17 00:00:00 2001 From: ffrostfall <80861876+ffrostflame@users.noreply.github.com> Date: Tue, 6 Feb 2024 21:38:26 -0500 Subject: [PATCH 1/9] Gutting the old code to make space for new processor --- sourcemap.json | 2 +- src/logging/formatter.luau | 3 - src/logging/packetReporter.luau | 31 --- src/packets/packet.luau | 83 +------- src/process/channel.luau | 30 --- src/process/client/clientProcess.luau | 30 +-- src/process/mergeBufferArray.luau | 32 ---- src/process/parsing/receive.luau | 51 ----- src/process/parsing/send.luau | 30 --- src/process/server/serverProcess.luau | 74 +------ src/storage/dataTypeList.luau | 266 -------------------------- 11 files changed, 9 insertions(+), 623 deletions(-) delete mode 100644 src/logging/formatter.luau delete mode 100644 src/logging/packetReporter.luau delete mode 100644 src/process/channel.luau delete mode 100644 src/process/mergeBufferArray.luau delete mode 100644 src/process/parsing/receive.luau delete mode 100644 src/process/parsing/send.luau delete mode 100644 src/storage/dataTypeList.luau diff --git a/sourcemap.json b/sourcemap.json index 080fe4f..608815d 100644 --- a/sourcemap.json +++ b/sourcemap.json @@ -1 +1 @@ -{"name":"bytenet-dev","className":"DataModel","filePaths":["dev.project.json"],"children":[{"name":"ReplicatedStorage","className":"ReplicatedStorage","children":[{"name":"Packages","className":"Folder","children":[{"name":"BoatTEST","className":"ModuleScript","filePaths":["Packages\\BoatTEST.lua"]},{"name":"LuauSignal","className":"ModuleScript","filePaths":["Packages\\LuauSignal.lua"]},{"name":"_Index","className":"Folder","children":[{"name":"boatbomber_boattest@0.1.1","className":"Folder","children":[{"name":"boattest","className":"ModuleScript","filePaths":["Packages\\_Index\\boatbomber_boattest@0.1.1\\boattest\\src\\init.lua","Packages\\_Index\\boatbomber_boattest@0.1.1\\boattest\\default.project.json"],"children":[{"name":"output","className":"ModuleScript","filePaths":["Packages\\_Index\\boatbomber_boattest@0.1.1\\boattest\\src\\output.lua"]},{"name":"run","className":"ModuleScript","filePaths":["Packages\\_Index\\boatbomber_boattest@0.1.1\\boattest\\src\\run.lua"]},{"name":"this","className":"ModuleScript","filePaths":["Packages\\_Index\\boatbomber_boattest@0.1.1\\boattest\\src\\this.lua"]}]}]},{"name":"ffrostflame_luausignal@0.1.3","className":"Folder","children":[{"name":"luausignal","className":"ModuleScript","filePaths":["Packages\\_Index\\ffrostflame_luausignal@0.1.3\\luausignal\\src\\init.luau","Packages\\_Index\\ffrostflame_luausignal@0.1.3\\luausignal\\default.project.json"],"children":[{"name":"typings","className":"ModuleScript","filePaths":["Packages\\_Index\\ffrostflame_luausignal@0.1.3\\luausignal\\src\\typings.luau"]}]}]},{"name":"ffrostflame_wally-instance-manager@0.1.0","className":"Folder","children":[{"name":"wally-instance-manager","className":"ModuleScript","filePaths":["Packages\\_Index\\ffrostflame_wally-instance-manager@0.1.0\\wally-instance-manager\\src\\init.luau","Packages\\_Index\\ffrostflame_wally-instance-manager@0.1.0\\wally-instance-manager\\default.project.json"]}]}]},{"name":"wallyInstanceManager","className":"ModuleScript","filePaths":["Packages\\wallyInstanceManager.lua"]},{"name":"ByteNet","className":"ModuleScript","filePaths":["src\\init.luau"],"children":[{"name":"logging","className":"Folder","children":[{"name":"formatter","className":"ModuleScript","filePaths":["src\\logging\\formatter.luau"]},{"name":"packetReporter","className":"ModuleScript","filePaths":["src\\logging\\packetReporter.luau"]}]},{"name":"packets","className":"Folder","children":[{"name":"getUnique","className":"ModuleScript","filePaths":["src\\packets\\getUnique.luau"]},{"name":"identifiers","className":"Folder","children":[{"name":"clientPacketIDs","className":"ModuleScript","filePaths":["src\\packets\\identifiers\\clientPacketIDs.luau"]},{"name":"serverPacketIDs","className":"ModuleScript","filePaths":["src\\packets\\identifiers\\serverPacketIDs.luau"]}]},{"name":"packet","className":"ModuleScript","filePaths":["src\\packets\\packet.luau"]}]},{"name":"process","className":"Folder","children":[{"name":"byteNetInstance","className":"ModuleScript","filePaths":["src\\process\\byteNetInstance.luau"]},{"name":"channel","className":"ModuleScript","filePaths":["src\\process\\channel.luau"]},{"name":"client","className":"Folder","children":[{"name":"clientProcess","className":"ModuleScript","filePaths":["src\\process\\client\\clientProcess.luau"]}]},{"name":"mergeBufferArray","className":"ModuleScript","filePaths":["src\\process\\mergeBufferArray.luau"]},{"name":"parsing","className":"Folder","children":[{"name":"receive","className":"ModuleScript","filePaths":["src\\process\\parsing\\receive.luau"]},{"name":"send","className":"ModuleScript","filePaths":["src\\process\\parsing\\send.luau"]}]},{"name":"retrievePacketFromID","className":"ModuleScript","filePaths":["src\\process\\retrievePacketFromID.luau"]},{"name":"server","className":"Folder","children":[{"name":"serverProcess","className":"ModuleScript","filePaths":["src\\process\\server\\serverProcess.luau"]}]}]},{"name":"storage","className":"Folder","children":[{"name":"dataTypeList","className":"ModuleScript","filePaths":["src\\storage\\dataTypeList.luau"]},{"name":"reliabilityTypeList","className":"ModuleScript","filePaths":["src\\storage\\reliabilityTypeList.luau"]},{"name":"currentRunContext","className":"ModuleScript","filePaths":["src\\storage\\currentRunContext.luau"]}]},{"name":"types","className":"ModuleScript","filePaths":["src\\types.luau"]}]}]},{"name":"shared","className":"Folder","children":[{"name":"testPackets","className":"ModuleScript","filePaths":["dev/shared\\testPackets.luau"]}]}]},{"name":"ServerScriptService","className":"ServerScriptService","children":[{"name":"server","className":"Folder","children":[{"name":"serverTests","className":"Script","filePaths":["dev/server\\serverTests.server.luau"]}]}]},{"name":"StarterPlayer","className":"StarterPlayer","children":[{"name":"StarterPlayerScripts","className":"StarterPlayerScripts","children":[{"name":"clientTests","className":"LocalScript","filePaths":["dev/client\\clientTests.client.luau"]}]}]}]} \ No newline at end of file +{"name":"bytenet-dev","className":"DataModel","filePaths":["dev.project.json"],"children":[{"name":"ReplicatedStorage","className":"ReplicatedStorage","children":[{"name":"Packages","className":"Folder","children":[{"name":"BoatTEST","className":"ModuleScript","filePaths":["Packages\\BoatTEST.lua"]},{"name":"LuauSignal","className":"ModuleScript","filePaths":["Packages\\LuauSignal.lua"]},{"name":"_Index","className":"Folder","children":[{"name":"boatbomber_boattest@0.1.1","className":"Folder","children":[{"name":"boattest","className":"ModuleScript","filePaths":["Packages\\_Index\\boatbomber_boattest@0.1.1\\boattest\\src\\init.lua","Packages\\_Index\\boatbomber_boattest@0.1.1\\boattest\\default.project.json"],"children":[{"name":"output","className":"ModuleScript","filePaths":["Packages\\_Index\\boatbomber_boattest@0.1.1\\boattest\\src\\output.lua"]},{"name":"run","className":"ModuleScript","filePaths":["Packages\\_Index\\boatbomber_boattest@0.1.1\\boattest\\src\\run.lua"]},{"name":"this","className":"ModuleScript","filePaths":["Packages\\_Index\\boatbomber_boattest@0.1.1\\boattest\\src\\this.lua"]}]}]},{"name":"ffrostflame_luausignal@0.1.3","className":"Folder","children":[{"name":"luausignal","className":"ModuleScript","filePaths":["Packages\\_Index\\ffrostflame_luausignal@0.1.3\\luausignal\\src\\init.luau","Packages\\_Index\\ffrostflame_luausignal@0.1.3\\luausignal\\default.project.json"],"children":[{"name":"typings","className":"ModuleScript","filePaths":["Packages\\_Index\\ffrostflame_luausignal@0.1.3\\luausignal\\src\\typings.luau"]}]}]},{"name":"ffrostflame_wally-instance-manager@0.1.0","className":"Folder","children":[{"name":"wally-instance-manager","className":"ModuleScript","filePaths":["Packages\\_Index\\ffrostflame_wally-instance-manager@0.1.0\\wally-instance-manager\\src\\init.luau","Packages\\_Index\\ffrostflame_wally-instance-manager@0.1.0\\wally-instance-manager\\default.project.json"]}]}]},{"name":"wallyInstanceManager","className":"ModuleScript","filePaths":["Packages\\wallyInstanceManager.lua"]},{"name":"ByteNet","className":"ModuleScript","filePaths":["src\\init.luau"],"children":[{"name":"packets","className":"Folder","children":[{"name":"getUnique","className":"ModuleScript","filePaths":["src\\packets\\getUnique.luau"]},{"name":"identifiers","className":"Folder","children":[{"name":"clientPacketIDs","className":"ModuleScript","filePaths":["src\\packets\\identifiers\\clientPacketIDs.luau"]},{"name":"serverPacketIDs","className":"ModuleScript","filePaths":["src\\packets\\identifiers\\serverPacketIDs.luau"]}]},{"name":"packet","className":"ModuleScript","filePaths":["src\\packets\\packet.luau"]}]},{"name":"process","className":"Folder","children":[{"name":"byteNetInstance","className":"ModuleScript","filePaths":["src\\process\\byteNetInstance.luau"]},{"name":"client","className":"Folder","children":[{"name":"clientProcess","className":"ModuleScript","filePaths":["src\\process\\client\\clientProcess.luau"]}]},{"name":"retrievePacketFromID","className":"ModuleScript","filePaths":["src\\process\\retrievePacketFromID.luau"]},{"name":"server","className":"Folder","children":[{"name":"serverProcess","className":"ModuleScript","filePaths":["src\\process\\server\\serverProcess.luau"]}]}]},{"name":"storage","className":"Folder","children":[{"name":"currentRunContext","className":"ModuleScript","filePaths":["src\\storage\\currentRunContext.luau"]},{"name":"reliabilityTypeList","className":"ModuleScript","filePaths":["src\\storage\\reliabilityTypeList.luau"]}]},{"name":"types","className":"ModuleScript","filePaths":["src\\types.luau"]}]}]},{"name":"shared","className":"Folder","children":[{"name":"testPackets","className":"ModuleScript","filePaths":["dev/shared\\testPackets.luau"]}]}]},{"name":"ServerScriptService","className":"ServerScriptService","children":[{"name":"server","className":"Folder","children":[{"name":"serverTests","className":"Script","filePaths":["dev/server\\serverTests.server.luau"]}]}]},{"name":"StarterPlayer","className":"StarterPlayer","children":[{"name":"StarterPlayerScripts","className":"StarterPlayerScripts","children":[{"name":"clientTests","className":"LocalScript","filePaths":["dev/client\\clientTests.client.luau"]}]}]}]} \ No newline at end of file diff --git a/src/logging/formatter.luau b/src/logging/formatter.luau deleted file mode 100644 index 4c3e796..0000000 --- a/src/logging/formatter.luau +++ /dev/null @@ -1,3 +0,0 @@ -return function(message) - return `[ByteNet] {message}` -end diff --git a/src/logging/packetReporter.luau b/src/logging/packetReporter.luau deleted file mode 100644 index 7b6ecde..0000000 --- a/src/logging/packetReporter.luau +++ /dev/null @@ -1,31 +0,0 @@ -local formatter = require(script.Parent.formatter) -local packetReporterPrototype = {} -local packetReporterMetatable = { __index = packetReporterPrototype } -export type packetReporterType = typeof(setmetatable({} :: { - _id: number, -}, packetReporterMetatable)) - -function packetReporterPrototype.error(self: packetReporterType, message: string) - error(formatter(`{"packet" .. self._id} {message}`), 3) -end - -function packetReporterPrototype.raiseTypeError( - self: packetReporterType, - key: string, - expectedType: string, - actualType: string -) - self:error(`Expected key {key} to be {expectedType}, but got {actualType} instead.`) -end - -function packetReporterPrototype.raiseBoundsError(self: packetReporterType, key, min, max, value) - self:error(`Expected key {key} to be within the bounds of {min} and {max}, but got {value} instead.`) -end - -return function(packetID: number): packetReporterType - local self = setmetatable({}, packetReporterMetatable) - - self._id = packetID - - return self -end diff --git a/src/packets/packet.luau b/src/packets/packet.luau index 74d5c32..965a567 100644 --- a/src/packets/packet.luau +++ b/src/packets/packet.luau @@ -1,13 +1,6 @@ -local RunService = game:GetService("RunService") - local currentRunContext = require(script.Parent.Parent.storage.currentRunContext) -local packetReporter = require(script.Parent.Parent.logging.packetReporter) local clientPacketIDs = require(script.Parent.Parent.packets.identifiers.clientPacketIDs) local serverPacketIDs = require(script.Parent.Parent.packets.identifiers.serverPacketIDs) -local clientProcess = require(script.Parent.Parent.process.client.clientProcess) -local send = require(script.Parent.Parent.process.parsing.send) -local serverProcess = require(script.Parent.Parent.process.server.serverProcess) -local dataTypeList = require(script.Parent.Parent.storage.dataTypeList) local reliabilityTypeList = require(script.Parent.Parent.storage.reliabilityTypeList) local getUnique = require(script.Parent.getUnique) @@ -16,91 +9,21 @@ local packetMetatable = { __index = packetPrototype } export type packetType = typeof(setmetatable( {} :: { _id: number, - _order: { [number]: { string } }, _reliabilityType: reliabilityTypeList.reliabilityType, _listeners: { [number]: (data: {}, player: Player) -> () }, - _reporter: packetReporter.packetReporterType, }, packetMetatable )) -function packetPrototype.sendToAll(self: packetType, data) - local packetBuffer = send(self._id, data, self._order) - - serverProcess.sendEveryone(self._reliabilityType, packetBuffer) -end - -function packetPrototype.send(self: packetType, data: { [string]: any }, target: Player?) - for _, v in self._order do - local key = v[1] - local dataType = v[2] - - local value = data[key] - if value == nil then - self._reporter:error("missing key " .. key) - return - else - local dataTypeInfo = dataTypeList[dataType] - if not dataTypeInfo.typecheck(value) then - self._reporter:raiseTypeError(key, dataType, typeof(value)) - return - end - - if dataTypeInfo.checkBounds then - if not dataTypeInfo.checkBounds(value) then - self._reporter:raiseBoundsError(key, dataTypeInfo.min, dataTypeInfo.max, value) - return - end - end - end - end - - local packetBuffer = send(self._id, data, self._order) - - if RunService:IsServer() then - if target then - serverProcess.sendTo(target, self._reliabilityType, packetBuffer) - else - warn("no target specified") - end - elseif RunService:IsClient() then - clientProcess.send(self._reliabilityType, packetBuffer) - end -end - -function packetPrototype.listen(self: packetType, callback: (data: {}, player: Player?) -> ()) - table.insert(self._listeners, callback) -end - return function(packetStructure: { [string]: any }, reliabilityType: reliabilityTypeList.reliabilityType): packetType local self = setmetatable({}, packetMetatable) self._reliabilityType = reliabilityType or "reliable" - - self._id = 0 self._unique = getUnique(packetStructure) - if currentRunContext == "server" then - self._id = serverPacketIDs.assignPacket(self._unique, self) - else - self._id = clientPacketIDs.assignPacket(self._unique, self) - end - - self._reporter = packetReporter(self._id) - - -- structure stuff - self._order = {} - for key, value in packetStructure do - table.insert(self._order, { - key, - value, - }) - end - - table.sort(self._order, function(a, b) - return a[1] < b[1] - end) - + self._id = if currentRunContext == "server" + then serverPacketIDs.assignPacket(self._unique, self) + else clientPacketIDs.assignPacket(self._unique, self) self._listeners = {} return self diff --git a/src/process/channel.luau b/src/process/channel.luau deleted file mode 100644 index ccd8683..0000000 --- a/src/process/channel.luau +++ /dev/null @@ -1,30 +0,0 @@ -local mergeBufferArray = require(script.Parent.mergeBufferArray) - -local channelPrototype = {} -local channelMetatable = { __index = channelPrototype } -export type channelType = typeof(setmetatable({} :: { - _queue: { [number]: buffer }, -}, channelMetatable)) - -function channelPrototype.add(self: channelType, packetBuffer: buffer) - table.insert(self._queue, packetBuffer) -end - -function channelPrototype.flush(self: channelType): buffer? - if #self._queue == 0 then - return nil - end - - local merged = mergeBufferArray(self._queue) - table.clear(self._queue) - - return merged -end - -return function(): channelType - local self = setmetatable({}, channelMetatable) - - self._queue = {} - - return self -end diff --git a/src/process/client/clientProcess.luau b/src/process/client/clientProcess.luau index 50f90f7..9cc63a8 100644 --- a/src/process/client/clientProcess.luau +++ b/src/process/client/clientProcess.luau @@ -1,17 +1,10 @@ local RunService = game:GetService("RunService") local byteNetInstance = require(script.Parent.Parent.Parent.process.byteNetInstance) -local channel = require(script.Parent.Parent.Parent.process.channel) local reliabilityTypeList = require(script.Parent.Parent.Parent.storage.reliabilityTypeList) -local receive = require(script.Parent.Parent.parsing.receive) local wallyInstanceManager = require(script.Parent.Parent.Parent.Parent.wallyInstanceManager) -local reliableChannel = channel() -local unreliableChannel = channel() - -local function onClientEvent(receivedBuffer) - receive(receivedBuffer) -end +local function onClientEvent(receivedBuffer) end local clientProcess = {} @@ -33,26 +26,7 @@ function clientProcess.start() reliable.OnClientEvent:Connect(onClientEvent) unreliable.OnClientEvent:Connect(onClientEvent) - RunService.Heartbeat:Connect(function() - local reliableQueue = reliableChannel:flush() - local unreliableQueue = unreliableChannel:flush() - - if reliableQueue then - reliable:FireServer(reliableQueue) - end - - if unreliableQueue then - unreliable:FireServer(unreliableQueue) - end - end) -end - -function clientProcess.send(reliabilityType: reliabilityTypeList.reliabilityType, packetBuffer: buffer) - if reliabilityType == reliabilityTypeList.reliable then - reliableChannel:add(packetBuffer) - elseif reliabilityType == reliabilityTypeList.unreliable then - unreliableChannel:add(packetBuffer) - end + RunService.Heartbeat:Connect(function() end) end return clientProcess diff --git a/src/process/mergeBufferArray.luau b/src/process/mergeBufferArray.luau deleted file mode 100644 index 00014bb..0000000 --- a/src/process/mergeBufferArray.luau +++ /dev/null @@ -1,32 +0,0 @@ -return function(bufferArray: { [number]: buffer }) - -- Optimization for when there's only 2 buffers to merge - if #bufferArray == 2 then - local buffer1 = bufferArray[1] - local buffer2 = bufferArray[2] - - local buffer1Length = buffer.len(buffer1) - local buffer2Length = buffer.len(buffer2) - - local mergedBuffer = buffer.create(buffer1Length + buffer2Length) - - buffer.copy(mergedBuffer, 0, buffer1) - buffer.copy(mergedBuffer, buffer1Length, buffer2) - - return mergedBuffer - end - - local totalSize = 0 - for _, targetBuffer in bufferArray do - totalSize += buffer.len(targetBuffer) - end - - local mergedBuffer = buffer.create(totalSize) - - local bufferCursor = 0 - for _, targetBuffer in bufferArray do - buffer.copy(mergedBuffer, bufferCursor, targetBuffer) - bufferCursor += buffer.len(targetBuffer) - end - - return mergedBuffer -end diff --git a/src/process/parsing/receive.luau b/src/process/parsing/receive.luau deleted file mode 100644 index 37c56a4..0000000 --- a/src/process/parsing/receive.luau +++ /dev/null @@ -1,51 +0,0 @@ -local dataTypeList = require(script.Parent.Parent.Parent.storage.dataTypeList) -local retrievePacketFromID = require(script.Parent.Parent.retrievePacketFromID) - -local freeThread: thread? -- Thread reusage - -local function passer(callback, data, player): () - local acquiredThread = freeThread - freeThread = nil - callback(data, player) - freeThread = acquiredThread -end - -local function yielder(): () - while true do - passer(coroutine.yield()) - end -end - -return function(receivedBuffer: buffer, player: Player?) - local bufferLength = buffer.len(receivedBuffer) - - local cursor = 0 - while cursor < bufferLength do - local packetID = buffer.readu8(receivedBuffer, cursor) - cursor += 1 - - local packet = retrievePacketFromID(packetID) - local packetStructure = {} - - for _, keyDataTypePair in packet._order do - local key = keyDataTypePair[1] - local dataType = keyDataTypePair[2] - - local dataTypeInfo = dataTypeList[dataType] - - local length: number? - packetStructure[key], length = dataTypeInfo.read(receivedBuffer, cursor) - - cursor += dataTypeInfo.size or length or 0 - end - - for _, callback in packet._listeners do - if freeThread == nil then - freeThread = coroutine.create(yielder) - task.spawn(freeThread :: thread) - end - - task.spawn(freeThread :: thread, callback, packetStructure, player) - end - end -end diff --git a/src/process/parsing/send.luau b/src/process/parsing/send.luau deleted file mode 100644 index 3330f97..0000000 --- a/src/process/parsing/send.luau +++ /dev/null @@ -1,30 +0,0 @@ -local dataTypeList = require(script.Parent.Parent.Parent.storage.dataTypeList) - -return function(packetID: number, data: {}, structure: { [number]: { string } }) - local size = 1 - for _, keyDataPair in structure do - if keyDataPair[2] == "string" then - local length = string.len(data[keyDataPair[1]]) - size += length + 1 - elseif keyDataPair[2] == "buff" then - local length = buffer.len(data[keyDataPair[1]]) - size += length + 4 - else - local dataType = dataTypeList[keyDataPair[2]] - - size += dataType.size or 0 - end - end - - local packetBuffer = buffer.create(size) - buffer.writeu8(packetBuffer, 0, packetID) - - local cursor = 1 - for _, keyDataPair in structure do - local dataType = dataTypeList[keyDataPair[2]] - - cursor += dataType.write(packetBuffer, cursor, data[keyDataPair[1]]) or dataType.size or 0 - end - - return packetBuffer -end diff --git a/src/process/server/serverProcess.luau b/src/process/server/serverProcess.luau index 0ccf13e..bd0c782 100644 --- a/src/process/server/serverProcess.luau +++ b/src/process/server/serverProcess.luau @@ -1,42 +1,22 @@ local Players = game:GetService("Players") local RunService = game:GetService("RunService") -local mergeBufferArray = require(script.Parent.Parent.Parent.process.mergeBufferArray) local byteNetInstance = require(script.Parent.Parent.byteNetInstance) local wallyInstanceManager = require(script.Parent.Parent.Parent.Parent.wallyInstanceManager) -local reliabilityTypeList = require(script.Parent.Parent.Parent.storage.reliabilityTypeList) -local channel = require(script.Parent.Parent.channel) -local receive = require(script.Parent.Parent.parsing.receive) local remoteInstances = { reliable = Instance.new("RemoteEvent"), unreliable = Instance.new("UnreliableRemoteEvent"), } -local playerChannels = {} -local globalChannels = { - [reliabilityTypeList.reliable] = channel(), - [reliabilityTypeList.unreliable] = channel(), -} local function onServerEvent(player: Player, data) if buffer.len(data) >= 100000 then warn("over 100K byte limit from player: " .. player.UserId) return end - - receive(data, player) end -local function onPlayerAdded(player: Player) - if playerChannels[player] then - return - end - - playerChannels[player] = { - [reliabilityTypeList.reliable] = channel(), - [reliabilityTypeList.unreliable] = channel(), - } -end +local function onPlayerAdded(player: Player) end local serverProcess = {} @@ -53,60 +33,12 @@ function serverProcess.start() Players.PlayerAdded:Connect(onPlayerAdded) - Players.PlayerRemoving:Connect(function(player: Player) - playerChannels[player] = nil - end) + Players.PlayerRemoving:Connect(function(player: Player) end) remoteInstances.reliable.OnServerEvent:Connect(onServerEvent) remoteInstances.unreliable.OnServerEvent:Connect(onServerEvent) - RunService.Heartbeat:Connect(function() - local globalReliableQueue = globalChannels[reliabilityTypeList.reliable]:flush() - local globalUnreliableQueue = globalChannels[reliabilityTypeList.unreliable]:flush() - - for player, channels in playerChannels do - if not player:IsDescendantOf(Players) then - continue - end - - local reliableQueue = channels[reliabilityTypeList.reliable]:flush() - local unreliableQueue = channels[reliabilityTypeList.unreliable]:flush() - - if reliableQueue or globalReliableQueue then - if globalReliableQueue then - reliableQueue = mergeBufferArray({ reliableQueue or buffer.create(0), globalReliableQueue }) - end - - remoteInstances.reliable:FireClient(player, reliableQueue) - end - - if unreliableQueue or globalUnreliableQueue then - if globalUnreliableQueue then - unreliableQueue = mergeBufferArray({ unreliableQueue or buffer.create(0), globalUnreliableQueue }) - end - - remoteInstances.unreliable:FireClient(player, unreliableQueue) - end - end - end) -end - -function serverProcess.sendTo( - player: Player, - reliabilityType: reliabilityTypeList.reliabilityType, - packetBuffer: buffer -) - if not playerChannels[player] then - playerChannels[player] = { - [reliabilityTypeList.reliable] = channel(), - [reliabilityTypeList.unreliable] = channel(), - } - end - playerChannels[player][reliabilityType]:add(packetBuffer) -end - -function serverProcess.sendEveryone(reliabilityType: reliabilityTypeList.reliabilityType, packetBuffer: buffer) - globalChannels[reliabilityType]:add(packetBuffer) + RunService.Heartbeat:Connect(function() end) end return serverProcess diff --git a/src/storage/dataTypeList.luau b/src/storage/dataTypeList.luau deleted file mode 100644 index 74643b4..0000000 --- a/src/storage/dataTypeList.luau +++ /dev/null @@ -1,266 +0,0 @@ --- all hail Copilot - -return { - uint8 = { - size = 1, - - min = 0, - max = 255, - - typecheck = function(value) - return typeof(value) == "number" - end, - - checkBounds = function(value) - return value >= 0 and value <= 255 - end, - - write = function(targetBuffer, offset, value) - buffer.writeu8(targetBuffer, offset, value) - return 1 - end, - read = function(targetBuffer, offset) - return buffer.readu8(targetBuffer, offset) - end, - }, - int8 = { - size = 1, - - min = -128, - max = 127, - - checkBounds = function(value) - return value >= -128 and value <= 127 - end, - - typecheck = function(value) - return typeof(value) == "number" - end, - - write = function(targetBuffer, offset, value) - buffer.writei8(targetBuffer, offset, value) - return 1 - end, - read = function(targetBuffer, offset) - return buffer.readi8(targetBuffer, offset) - end, - }, - - uint16 = { - size = 2, - - min = 0, - max = 65535, - - checkBounds = function(value) - return value >= 0 and value <= 65535 - end, - - typecheck = function(value) - return typeof(value) == "number" - end, - - write = function(targetBuffer, offset, value) - buffer.writeu16(targetBuffer, offset, value) - return 2 - end, - read = function(targetBuffer, offset) - return buffer.readu16(targetBuffer, offset) - end, - }, - - int16 = { - size = 2, - - min = -32768, - max = 32767, - - checkBounds = function(value) - return value >= -32768 and value <= 32767 - end, - - typecheck = function(value) - return typeof(value) == "number" - end, - - write = function(targetBuffer, offset, value) - buffer.writei16(targetBuffer, offset, value) - return 2 - end, - read = function(targetBuffer, offset) - return buffer.readi16(targetBuffer, offset) - end, - }, - - uint32 = { - size = 4, - - min = 0, - max = 4294967295, - - checkBounds = function(value) - return value >= 0 and value <= 4294967295 - end, - - typecheck = function(value) - return typeof(value) == "number" - end, - - write = function(targetBuffer, offset, value) - buffer.writeu32(targetBuffer, offset, value) - return 4 - end, - read = function(targetBuffer, offset) - return buffer.readu32(targetBuffer, offset) - end, - }, - - int32 = { - size = 4, - - min = -2147483648, - max = 2147483647, - - checkBounds = function(value) - return value >= -2147483648 and value <= 2147483647 - end, - - typecheck = function(value) - return typeof(value) == "number" - end, - - write = function(targetBuffer, offset, value) - buffer.writei32(targetBuffer, offset, value) - return 4 - end, - read = function(targetBuffer, offset) - return buffer.readi32(targetBuffer, offset) - end, - }, - - float64 = { - size = 8, - - min = -1.7976931348623157e+308, - max = 1.7976931348623157e+308, - - typecheck = function(value) - return typeof(value) == "number" - end, - - write = function(targetBuffer, offset, value) - buffer.writef64(targetBuffer, offset, value) - return 8 - end, - read = function(targetBuffer, offset) - return buffer.readf64(targetBuffer, offset) - end, - }, - - float32 = { - size = 4, - - min = -3.4028234663852886e+38, - max = 3.4028234663852886e+38, - - typecheck = function(value) - return typeof(value) == "number" - end, - - write = function(targetBuffer, offset, value) - buffer.writef32(targetBuffer, offset, value) - return 4 - end, - read = function(targetBuffer, offset) - return buffer.readf32(targetBuffer, offset) - end, - }, - - vec3 = { - size = 12, - - typecheck = function(value) - return typeof(value) == "Vector3" - end, - - write = function(targetBuffer, offset, value) - buffer.writef32(targetBuffer, offset, value.X) - buffer.writef32(targetBuffer, offset + 4, value.Y) - buffer.writef32(targetBuffer, offset + 8, value.Z) - return 12 - end, - read = function(targetBuffer, offset) - return Vector3.new( - buffer.readf32(targetBuffer, offset), - buffer.readf32(targetBuffer, offset + 4), - buffer.readf32(targetBuffer, offset + 8) - ) - end, - }, - - bool = { - size = 1, - - typecheck = function(value) - return typeof(value) == "boolean" - end, - - write = function(targetBuffer, offset, value) - buffer.writeu8(targetBuffer, offset, value and 1 or 0) - return 1 - end, - read = function(targetBuffer, offset) - return buffer.readu8(targetBuffer, offset) == 1 - end, - }, - - string = { - size = nil, -- indeterminate - - typecheck = function(value) - return typeof(value) == "string" - end, - - write = function(targetBuffer, offset, targetString) - local length = #targetString - buffer.writeu8(targetBuffer, offset, length) - buffer.writestring(targetBuffer, offset + 1, targetString) - return length + 1 - end, - read = function(targetBuffer: buffer, offset) - local length = buffer.readu8(targetBuffer, offset) - return buffer.readstring(targetBuffer, offset + 1, length), length + 1 - end, - }, - - buff = { - size = nil, - - typecheck = function(value) - return typeof(value) == "buffer" - end, - - write = function(targetBuffer, offset, targetString) - local length = #buffer.tostring(targetString) - buffer.writeu32(targetBuffer, offset, length) - buffer.writestring(targetBuffer, offset + 4, buffer.tostring(targetString)) - return length + 4 - end, - read = function(targetBuffer: buffer, offset) - local length = buffer.readu32(targetBuffer, offset) - return buffer.fromstring(buffer.readstring(targetBuffer, offset + 4, length)), length + 4 - end, - }, -} :: { - [string]: { - size: number?, - - min: number?, - max: number?, - - read: (targetBuffer: buffer, offset: number) -> (any, number?), - write: (targetBuffer: buffer, offset: number, data: any) -> number?, - typecheck: (value: any) -> boolean, - checkBounds: ((value: number) -> boolean)?, - }, -} From e95b0976e2d3ea588f6e05f355a9d23e03aab7b1 Mon Sep 17 00:00:00 2001 From: ffrostfall <80861876+ffrostflame@users.noreply.github.com> Date: Tue, 6 Feb 2024 22:46:53 -0500 Subject: [PATCH 2/9] Cleanup somewhat --- dev/concept.luau | 116 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 102 insertions(+), 14 deletions(-) diff --git a/dev/concept.luau b/dev/concept.luau index 60e3cb8..b1e5e22 100644 --- a/dev/concept.luau +++ b/dev/concept.luau @@ -1,30 +1,118 @@ -local cursor = 0 +local writeCursor = 0 +local readCursor = 0 local packetBuffer = buffer.create(0) local writers = {} -local function writePacket(dict: { [string]: number }) - for key, value in dict do - local length = 1 +local dataTypeWriters = { + u8 = buffer.writeu8, +} +local dataTypeReaders = { + u8 = function(b: buffer) + local value = buffer.readu8(b, readCursor) + readCursor += 1 + return value + end, +} +local dataTypeLengths = { + u8 = function() + return 1 + end, +} - -- Copy the current position of the cursor - local position = cursor +local dataTypes = { + u8 = "u8", + dynString = "dynString", + dynArray = "dynArray", +} + +local function createPacketStructure(structure: { [string]: string }) + local structured = {} + for key, value in structure do + local accessIndex = dataTypes[value] + local reader, writer, length = + dataTypeReaders[accessIndex], dataTypeWriters[accessIndex], dataTypeLengths[accessIndex] + + table.insert(structured, { + reader = reader, + writer = writer, + length = length, + key = key, + }) + end + return structured +end + +local allPackets = { + [1] = createPacketStructure({ + a = dataTypes.u8, + b = dataTypes.u8, + }), +} + +local function writePacket( + id: number, + structure: { + { + writer: (b: buffer, offset: number, value: any) -> (), + reader: (b: buffer, offset: number) -> (), + key: string, + length: () -> number, + } + }, + dict: { [string]: number } +) + local idPosition = writeCursor + table.insert(writers, function() + buffer.writeu8(packetBuffer, idPosition, id) + end) + writeCursor += 1 + + for _, value in structure do + local currentCursor = writeCursor table.insert(writers, function() - buffer.writeu8(packetBuffer, position, value) + value.writer(packetBuffer, currentCursor, dict[value.key]) end) - - cursor += length + writeCursor += value.length() end end -- The user sends data -writePacket({ - a = 1, - b = 2, -}) +local start = os.clock() +for _ = 1, 1000 do + writePacket(1, allPackets[1], { + a = 1, + b = 2, + }) +end +print(os.clock() - start) +print(allPackets[1]) -- This would be run every frame -packetBuffer = buffer.create(cursor) +local start2 = os.clock() +packetBuffer = buffer.create(writeCursor) for _, writer in writers do writer() end table.clear(writers) +print(os.clock() - start2) + +-- Reading + +local function read(incomingBuffer: buffer) + local length = buffer.len(incomingBuffer) + + while readCursor < length do + local id = buffer.readu8(incomingBuffer, readCursor) + readCursor += 1 + + local structure = allPackets[id] + local deserialized = {} + for _, item in structure do + local value = item.reader(incomingBuffer) + + deserialized[item.key] = value + end + end +end + +read(packetBuffer) From c04b4054e8b88d6f9dfacb6f983bbb5cf8081546 Mon Sep 17 00:00:00 2001 From: ffrostfall <80861876+ffrostflame@users.noreply.github.com> Date: Wed, 7 Feb 2024 07:49:46 -0500 Subject: [PATCH 3/9] Refactoring + rewrites --- .vscode/settings.json | 3 +- CHANGELOG.md | 7 + dev/client/clientTests.client.luau | 1 - dev/concept.luau | 11 +- dev/server/serverTests.server.luau | 2 +- dev/shared/testPackets.luau | 2 +- sourcemap.json | 2 +- src/init.luau | 34 +- src/packets/packet.luau | 54 ++- .../retrievePacketFromID.luau | 0 src/process/byteNetInstance.luau | 1 - src/process/client.luau | 57 +++ src/process/client/clientProcess.luau | 32 -- src/process/dataTypes.luau | 372 ++++++++++++++++++ src/process/read.luau | 28 ++ src/process/sendChannel.luau | 59 +++ src/process/server.luau | 112 ++++++ src/process/server/serverProcess.luau | 44 --- src/storage/currentRunContext.luau | 3 - src/storage/reliabilityTypeList.luau | 9 - src/types.luau | 41 +- wally.lock | 14 +- wally.toml | 2 +- 23 files changed, 736 insertions(+), 154 deletions(-) rename src/{process => packets}/retrievePacketFromID.luau (100%) delete mode 100644 src/process/byteNetInstance.luau create mode 100644 src/process/client.luau delete mode 100644 src/process/client/clientProcess.luau create mode 100644 src/process/dataTypes.luau create mode 100644 src/process/read.luau create mode 100644 src/process/sendChannel.luau create mode 100644 src/process/server.luau delete mode 100644 src/process/server/serverProcess.luau delete mode 100644 src/storage/currentRunContext.luau delete mode 100644 src/storage/reliabilityTypeList.luau diff --git a/.vscode/settings.json b/.vscode/settings.json index e38c3f4..f90dfdd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "luau-lsp.sourcemap.rojoProjectFile": "dev.project.json" + "luau-lsp.sourcemap.rojoProjectFile": "dev.project.json", + "luau-lsp.completion.imports.requireStyle": "alwaysRelative" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 20e7258..4932452 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ This project uses [semantic versioning](https://semver.org/spec/v2.0.0.html). --- +## version 0.3.0 + +### Improvements +- Rewrote client/server processing. Should drastically improve stability and performance. + +--- + ## version 0.2.1 ### Fixes diff --git a/dev/client/clientTests.client.luau b/dev/client/clientTests.client.luau index 5e3e845..74dab24 100644 --- a/dev/client/clientTests.client.luau +++ b/dev/client/clientTests.client.luau @@ -10,6 +10,5 @@ end) task.wait(2) testPackets.a:send({ - first = "hello world", second = 5, }) diff --git a/dev/concept.luau b/dev/concept.luau index b1e5e22..d48de17 100644 --- a/dev/concept.luau +++ b/dev/concept.luau @@ -7,11 +7,7 @@ local dataTypeWriters = { u8 = buffer.writeu8, } local dataTypeReaders = { - u8 = function(b: buffer) - local value = buffer.readu8(b, readCursor) - readCursor += 1 - return value - end, + u8 = buffer.readu8, } local dataTypeLengths = { u8 = function() @@ -21,8 +17,6 @@ local dataTypeLengths = { local dataTypes = { u8 = "u8", - dynString = "dynString", - dynArray = "dynArray", } local function createPacketStructure(structure: { [string]: string }) @@ -108,7 +102,8 @@ local function read(incomingBuffer: buffer) local structure = allPackets[id] local deserialized = {} for _, item in structure do - local value = item.reader(incomingBuffer) + local value = item.reader(incomingBuffer, readCursor) + readCursor += item.length() deserialized[item.key] = value end diff --git a/dev/server/serverTests.server.luau b/dev/server/serverTests.server.luau index 67887f5..33bdd42 100644 --- a/dev/server/serverTests.server.luau +++ b/dev/server/serverTests.server.luau @@ -5,7 +5,7 @@ local testPackets = require(ReplicatedStorage.shared.testPackets) Players.PlayerAdded:Connect(function(player) task.wait(1) - testPackets.a:send({ first = "testB", second = math.random(1, 8) }, player) + testPackets.a:send({ second = 5 }, player) end) testPackets.a:listen(function(data) diff --git a/dev/shared/testPackets.luau b/dev/shared/testPackets.luau index 5ef182d..af963e6 100644 --- a/dev/shared/testPackets.luau +++ b/dev/shared/testPackets.luau @@ -3,5 +3,5 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage") local ByteNet = require(ReplicatedStorage.Packages.ByteNet) return { - a = ByteNet.definePacket({ first = ByteNet.dataTypes.string, second = ByteNet.dataTypes.uint8 }), + a = ByteNet.definePacket({ second = ByteNet.dataTypes.uint8() }), } diff --git a/sourcemap.json b/sourcemap.json index 608815d..07dba58 100644 --- a/sourcemap.json +++ b/sourcemap.json @@ -1 +1 @@ -{"name":"bytenet-dev","className":"DataModel","filePaths":["dev.project.json"],"children":[{"name":"ReplicatedStorage","className":"ReplicatedStorage","children":[{"name":"Packages","className":"Folder","children":[{"name":"BoatTEST","className":"ModuleScript","filePaths":["Packages\\BoatTEST.lua"]},{"name":"LuauSignal","className":"ModuleScript","filePaths":["Packages\\LuauSignal.lua"]},{"name":"_Index","className":"Folder","children":[{"name":"boatbomber_boattest@0.1.1","className":"Folder","children":[{"name":"boattest","className":"ModuleScript","filePaths":["Packages\\_Index\\boatbomber_boattest@0.1.1\\boattest\\src\\init.lua","Packages\\_Index\\boatbomber_boattest@0.1.1\\boattest\\default.project.json"],"children":[{"name":"output","className":"ModuleScript","filePaths":["Packages\\_Index\\boatbomber_boattest@0.1.1\\boattest\\src\\output.lua"]},{"name":"run","className":"ModuleScript","filePaths":["Packages\\_Index\\boatbomber_boattest@0.1.1\\boattest\\src\\run.lua"]},{"name":"this","className":"ModuleScript","filePaths":["Packages\\_Index\\boatbomber_boattest@0.1.1\\boattest\\src\\this.lua"]}]}]},{"name":"ffrostflame_luausignal@0.1.3","className":"Folder","children":[{"name":"luausignal","className":"ModuleScript","filePaths":["Packages\\_Index\\ffrostflame_luausignal@0.1.3\\luausignal\\src\\init.luau","Packages\\_Index\\ffrostflame_luausignal@0.1.3\\luausignal\\default.project.json"],"children":[{"name":"typings","className":"ModuleScript","filePaths":["Packages\\_Index\\ffrostflame_luausignal@0.1.3\\luausignal\\src\\typings.luau"]}]}]},{"name":"ffrostflame_wally-instance-manager@0.1.0","className":"Folder","children":[{"name":"wally-instance-manager","className":"ModuleScript","filePaths":["Packages\\_Index\\ffrostflame_wally-instance-manager@0.1.0\\wally-instance-manager\\src\\init.luau","Packages\\_Index\\ffrostflame_wally-instance-manager@0.1.0\\wally-instance-manager\\default.project.json"]}]}]},{"name":"wallyInstanceManager","className":"ModuleScript","filePaths":["Packages\\wallyInstanceManager.lua"]},{"name":"ByteNet","className":"ModuleScript","filePaths":["src\\init.luau"],"children":[{"name":"packets","className":"Folder","children":[{"name":"getUnique","className":"ModuleScript","filePaths":["src\\packets\\getUnique.luau"]},{"name":"identifiers","className":"Folder","children":[{"name":"clientPacketIDs","className":"ModuleScript","filePaths":["src\\packets\\identifiers\\clientPacketIDs.luau"]},{"name":"serverPacketIDs","className":"ModuleScript","filePaths":["src\\packets\\identifiers\\serverPacketIDs.luau"]}]},{"name":"packet","className":"ModuleScript","filePaths":["src\\packets\\packet.luau"]}]},{"name":"process","className":"Folder","children":[{"name":"byteNetInstance","className":"ModuleScript","filePaths":["src\\process\\byteNetInstance.luau"]},{"name":"client","className":"Folder","children":[{"name":"clientProcess","className":"ModuleScript","filePaths":["src\\process\\client\\clientProcess.luau"]}]},{"name":"retrievePacketFromID","className":"ModuleScript","filePaths":["src\\process\\retrievePacketFromID.luau"]},{"name":"server","className":"Folder","children":[{"name":"serverProcess","className":"ModuleScript","filePaths":["src\\process\\server\\serverProcess.luau"]}]}]},{"name":"storage","className":"Folder","children":[{"name":"currentRunContext","className":"ModuleScript","filePaths":["src\\storage\\currentRunContext.luau"]},{"name":"reliabilityTypeList","className":"ModuleScript","filePaths":["src\\storage\\reliabilityTypeList.luau"]}]},{"name":"types","className":"ModuleScript","filePaths":["src\\types.luau"]}]}]},{"name":"shared","className":"Folder","children":[{"name":"testPackets","className":"ModuleScript","filePaths":["dev/shared\\testPackets.luau"]}]}]},{"name":"ServerScriptService","className":"ServerScriptService","children":[{"name":"server","className":"Folder","children":[{"name":"serverTests","className":"Script","filePaths":["dev/server\\serverTests.server.luau"]}]}]},{"name":"StarterPlayer","className":"StarterPlayer","children":[{"name":"StarterPlayerScripts","className":"StarterPlayerScripts","children":[{"name":"clientTests","className":"LocalScript","filePaths":["dev/client\\clientTests.client.luau"]}]}]}]} \ No newline at end of file +{"name":"bytenet-dev","className":"DataModel","filePaths":["dev.project.json"],"children":[{"name":"ReplicatedStorage","className":"ReplicatedStorage","children":[{"name":"Packages","className":"Folder","children":[{"name":"_Index","className":"Folder","children":[{"name":"ffrostflame_wally-instance-manager@0.1.0","className":"Folder","children":[{"name":"wally-instance-manager","className":"ModuleScript","filePaths":["Packages\\_Index\\ffrostflame_wally-instance-manager@0.1.0\\wally-instance-manager\\src\\init.luau","Packages\\_Index\\ffrostflame_wally-instance-manager@0.1.0\\wally-instance-manager\\default.project.json"]}]}]},{"name":"wallyInstanceManager","className":"ModuleScript","filePaths":["Packages\\wallyInstanceManager.lua"]},{"name":"ByteNet","className":"ModuleScript","filePaths":["src\\init.luau"],"children":[{"name":"packets","className":"Folder","children":[{"name":"getUnique","className":"ModuleScript","filePaths":["src\\packets\\getUnique.luau"]},{"name":"identifiers","className":"Folder","children":[{"name":"clientPacketIDs","className":"ModuleScript","filePaths":["src\\packets\\identifiers\\clientPacketIDs.luau"]},{"name":"serverPacketIDs","className":"ModuleScript","filePaths":["src\\packets\\identifiers\\serverPacketIDs.luau"]}]},{"name":"packet","className":"ModuleScript","filePaths":["src\\packets\\packet.luau"]},{"name":"retrievePacketFromID","className":"ModuleScript","filePaths":["src\\packets\\retrievePacketFromID.luau"]}]},{"name":"process","className":"Folder","children":[{"name":"client","className":"ModuleScript","filePaths":["src\\process\\client.luau"]},{"name":"dataTypes","className":"ModuleScript","filePaths":["src\\process\\dataTypes.luau"]},{"name":"read","className":"ModuleScript","filePaths":["src\\process\\read.luau"]},{"name":"sendChannel","className":"ModuleScript","filePaths":["src\\process\\sendChannel.luau"]},{"name":"server","className":"ModuleScript","filePaths":["src\\process\\server.luau"]}]},{"name":"types","className":"ModuleScript","filePaths":["src\\types.luau"]}]}]},{"name":"shared","className":"Folder","children":[{"name":"testPackets","className":"ModuleScript","filePaths":["dev/shared\\testPackets.luau"]}]}]},{"name":"ServerScriptService","className":"ServerScriptService","children":[{"name":"server","className":"Folder","children":[{"name":"serverTests","className":"Script","filePaths":["dev/server\\serverTests.server.luau"]}]}]},{"name":"StarterPlayer","className":"StarterPlayer","children":[{"name":"StarterPlayerScripts","className":"StarterPlayerScripts","children":[{"name":"clientTests","className":"LocalScript","filePaths":["dev/client\\clientTests.client.luau"]}]}]}]} \ No newline at end of file diff --git a/src/init.luau b/src/init.luau index 187525f..2488f84 100644 --- a/src/init.luau +++ b/src/init.luau @@ -2,8 +2,9 @@ local RunService = game:GetService("RunService") local clientPacketIDs = require(script.packets.identifiers.clientPacketIDs) local serverPacketIDs = require(script.packets.identifiers.serverPacketIDs) -local clientProcess = require(script.process.client.clientProcess) -local serverProcess = require(script.process.server.serverProcess) +local clientProcess = require(script.process.client) +local dataTypes = require(script.process.dataTypes) +local serverProcess = require(script.process.server) local types = require(script.types) if RunService:IsServer() then @@ -87,25 +88,22 @@ end When the packet is received, the callback is called with the data, and if on the server, the player who sent the packet. ]=] +local dataTypeTable = setmetatable({}, { + __index = function(_, index) + return function() + if not dataTypes.writers[index] then + error("Invalid data type: " .. index) + end + + return index + end + end, +}) + return ( table.freeze({ definePacket = require(script.packets.packet), - dataTypes = { - string = "string", - uint8 = "uint8", - uint16 = "uint16", - uint32 = "uint32", - - int8 = "int8", - int16 = "int16", - int32 = "int32", - - float32 = "float32", - float64 = "float64", - vec3 = "vec3", - bool = "bool", - buff = "buff", - }, + dataTypes = dataTypeTable, }) :: any ) :: types.ByteNet diff --git a/src/packets/packet.luau b/src/packets/packet.luau index 965a567..ff283f9 100644 --- a/src/packets/packet.luau +++ b/src/packets/packet.luau @@ -1,30 +1,66 @@ -local currentRunContext = require(script.Parent.Parent.storage.currentRunContext) +local RunService = game:GetService("RunService") + +local dataTypes = require(script.Parent.Parent.process.dataTypes) +local types = require(script.Parent.Parent.types) local clientPacketIDs = require(script.Parent.Parent.packets.identifiers.clientPacketIDs) local serverPacketIDs = require(script.Parent.Parent.packets.identifiers.serverPacketIDs) -local reliabilityTypeList = require(script.Parent.Parent.storage.reliabilityTypeList) +local client = require(script.Parent.Parent.process.client) +local server = require(script.Parent.Parent.process.server) local getUnique = require(script.Parent.getUnique) local packetPrototype = {} local packetMetatable = { __index = packetPrototype } export type packetType = typeof(setmetatable( {} :: { - _id: number, - _reliabilityType: reliabilityTypeList.reliabilityType, + id: number, + reliabilityType: string, + + listeners: { [number]: (data: {}, player: Player) -> () }, - _listeners: { [number]: (data: {}, player: Player) -> () }, + packetFormat: types.packetFormat, }, packetMetatable )) -return function(packetStructure: { [string]: any }, reliabilityType: reliabilityTypeList.reliabilityType): packetType +function packetPrototype.send(self: packetType, data: {}) + if RunService:IsServer() then + server.sendAllReliable(self.id, self.packetFormat, data) + else + client.sendReliable(self.id, self.packetFormat, data) + end +end + +function packetPrototype.listen(self: packetType, callback) + table.insert(self.listeners, callback) +end + +return function(packetStructure: { [string]: any }, reliabilityType: "reliable" | "unreliable"): packetType local self = setmetatable({}, packetMetatable) - self._reliabilityType = reliabilityType or "reliable" + -- Basic properties + self.reliabilityType = reliabilityType or "reliable" self._unique = getUnique(packetStructure) - self._id = if currentRunContext == "server" + self.id = if RunService:IsServer() then serverPacketIDs.assignPacket(self._unique, self) else clientPacketIDs.assignPacket(self._unique, self) - self._listeners = {} + self.listeners = {} + + -- Format + self.packetFormat = {} + for key, dataType in packetStructure do + local writer, reader, length = + dataTypes.writers[dataType], dataTypes.readers[dataType], dataTypes.lengths[dataType] + + table.insert(self.packetFormat, { + reader = reader, + writer = writer, + length = length, + key = key, + }) + end + table.sort(self.packetFormat, function(a, b) + return a.key < b.key + end) return self end diff --git a/src/process/retrievePacketFromID.luau b/src/packets/retrievePacketFromID.luau similarity index 100% rename from src/process/retrievePacketFromID.luau rename to src/packets/retrievePacketFromID.luau diff --git a/src/process/byteNetInstance.luau b/src/process/byteNetInstance.luau deleted file mode 100644 index f4e6c0c..0000000 --- a/src/process/byteNetInstance.luau +++ /dev/null @@ -1 +0,0 @@ -return script.Parent.Parent diff --git a/src/process/client.luau b/src/process/client.luau new file mode 100644 index 0000000..0272e2b --- /dev/null +++ b/src/process/client.luau @@ -0,0 +1,57 @@ +local RunService = game:GetService("RunService") + +local wallyInstanceManager = require(script.Parent.Parent.Parent.wallyInstanceManager) +local types = require(script.Parent.Parent.types) +local read = require(script.Parent.read) +local sendChannel = require(script.Parent.sendChannel) + +local function onClientEvent(receivedBuffer) + read(receivedBuffer) +end +local reliableChannel = sendChannel() +local unreliableChannel = sendChannel() + +local clientProcess = {} + +function clientProcess.sendReliable(id: number, format: types.packetFormat, data: { [string]: any }) + reliableChannel:add(id, format, data) +end + +function clientProcess.sendUnreliable(id: number, format: types.packetFormat, data: { [string]: any }) + unreliableChannel:add(id, format, data) +end + +function clientProcess.start() + local byteNetInstance = script.Parent.Parent.Parent + local remoteInstances: { + reliable: RemoteEvent?, + unreliable: UnreliableRemoteEvent?, + } = { + reliable = wallyInstanceManager.waitForInstance(byteNetInstance, "reliable", 3) :: RemoteEvent, + unreliable = wallyInstanceManager.waitForInstance(byteNetInstance, "unreliable", 3) :: UnreliableRemoteEvent, + } + + if not remoteInstances.reliable or not remoteInstances.unreliable then + return + end + + local reliableRemote = remoteInstances.reliable + local unreliableRemote = remoteInstances.unreliable + + reliableRemote.OnClientEvent:Connect(onClientEvent) + unreliableRemote.OnClientEvent:Connect(onClientEvent) + + RunService.Heartbeat:Connect(function() + local reliableBuffer = reliableChannel:empty() + if reliableBuffer then + reliableRemote:FireServer(reliableBuffer) + end + + local unreliableBuffer = unreliableChannel:empty() + if unreliableBuffer then + unreliableRemote:FireServer(unreliableBuffer) + end + end) +end + +return clientProcess diff --git a/src/process/client/clientProcess.luau b/src/process/client/clientProcess.luau deleted file mode 100644 index 9cc63a8..0000000 --- a/src/process/client/clientProcess.luau +++ /dev/null @@ -1,32 +0,0 @@ -local RunService = game:GetService("RunService") - -local byteNetInstance = require(script.Parent.Parent.Parent.process.byteNetInstance) -local reliabilityTypeList = require(script.Parent.Parent.Parent.storage.reliabilityTypeList) -local wallyInstanceManager = require(script.Parent.Parent.Parent.Parent.wallyInstanceManager) - -local function onClientEvent(receivedBuffer) end - -local clientProcess = {} - -function clientProcess.start() - local remoteInstances: { - [string]: RemoteEvent | UnreliableRemoteEvent, - } = { - [reliabilityTypeList.reliable] = wallyInstanceManager.waitForInstance(byteNetInstance, "reliable", 3) :: RemoteEvent, - [reliabilityTypeList.unreliable] = wallyInstanceManager.waitForInstance(byteNetInstance, "unreliable", 3) :: UnreliableRemoteEvent, - } - - if not remoteInstances[reliabilityTypeList.reliable] or not remoteInstances[reliabilityTypeList.unreliable] then - return - end - - local reliable: RemoteEvent = remoteInstances[reliabilityTypeList.reliable] :: RemoteEvent - local unreliable: UnreliableRemoteEvent = remoteInstances[reliabilityTypeList.unreliable] :: UnreliableRemoteEvent - - reliable.OnClientEvent:Connect(onClientEvent) - unreliable.OnClientEvent:Connect(onClientEvent) - - RunService.Heartbeat:Connect(function() end) -end - -return clientProcess diff --git a/src/process/dataTypes.luau b/src/process/dataTypes.luau new file mode 100644 index 0000000..842e27b --- /dev/null +++ b/src/process/dataTypes.luau @@ -0,0 +1,372 @@ +local readers = { + uint8 = buffer.readu8, + uint16 = buffer.readu16, + uint32 = buffer.readu32, + + int8 = buffer.readi8, + int16 = buffer.readi16, + int32 = buffer.readi32, + + float32 = buffer.readf32, + float64 = buffer.readf64, + + opt_uint8 = function(b: buffer, cursor: number): number? + local exists = buffer.readu8(b, cursor) + if exists then + return buffer.readu8(b, cursor + 1) + else + return nil + end + end, + opt_uint16 = function(b: buffer, cursor: number): number? + local exists = buffer.readu8(b, cursor) + if exists then + return buffer.readu16(b, cursor + 1) + else + return nil + end + end, + opt_uint32 = function(b: buffer, cursor: number): number? + local exists = buffer.readu8(b, cursor) + if exists then + return buffer.readu32(b, cursor + 1) + else + return nil + end + end, + + opt_int8 = function(b: buffer, cursor: number): number? + local exists = buffer.readu8(b, cursor) + if exists then + return buffer.readi8(b, cursor + 1) + else + return nil + end + end, + opt_int16 = function(b: buffer, cursor: number): number? + local exists = buffer.readu8(b, cursor) + if exists then + return buffer.readi16(b, cursor + 1) + else + return nil + end + end, + opt_int32 = function(b: buffer, cursor: number): number? + local exists = buffer.readu8(b, cursor) + if exists then + return buffer.readi32(b, cursor + 1) + else + return nil + end + end, + + opt_float32 = function(b: buffer, cursor: number): number? + local exists = buffer.readu8(b, cursor) + if exists then + return buffer.readf32(b, cursor + 1) + else + return nil + end + end, + opt_float64 = function(b: buffer, cursor: number): number? + local exists = buffer.readu8(b, cursor) + if exists then + return buffer.readf64(b, cursor + 1) + else + return nil + end + end, + + bool = function(b: buffer, cursor: number): boolean + return buffer.readu8(b, cursor) == 1 + end, + opt_bool = function(b: buffer, cursor: number): boolean? + local exists = buffer.readu8(b, cursor) + if exists then + return buffer.readu8(b, cursor + 1) == 1 + else + return nil + end + end, + + buff = function(b: buffer, cursor: number): buffer + local len = buffer.readu32(b, cursor) + local newBuff = buffer.create(len) + + buffer.copy(newBuff, 0, b, cursor + 4, len) + + return newBuff + end, + opt_buff = function(b: buffer, cursor: number): buffer? + local exists = buffer.readu8(b, cursor) + if exists then + local len = buffer.readu32(b, cursor + 1) + local newBuff = buffer.create(len) + + buffer.copy(newBuff, 0, b, cursor + 5, len) + + return newBuff + else + return nil + end + end, + + str = function(b: buffer, cursor: number): string + local len = buffer.readu16(b, cursor) + return buffer.readstring(b, cursor + 2, len) + end, + opt_str = function(b: buffer, cursor: number): string? + local exists = buffer.readu8(b, cursor) + if exists then + local len = buffer.readu16(b, cursor + 1) + return buffer.readstring(b, cursor + 3, len) + else + return nil + end + end, + + vec3 = function(b: buffer, cursor: number): Vector3 + local x = buffer.readf32(b, cursor) + local y = buffer.readf32(b, cursor + 4) + local z = buffer.readf32(b, cursor + 8) + + return Vector3.new(x, y, z) + end, + opt_vec3 = function(b: buffer, cursor: number): Vector3? + local exists = buffer.readu8(b, cursor) + if exists then + local x = buffer.readf32(b, cursor + 1) + local y = buffer.readf32(b, cursor + 5) + local z = buffer.readf32(b, cursor + 9) + + return Vector3.new(x, y, z) + else + return nil + end + end, +} +local writers = { + uint8 = buffer.writeu8, + uint16 = buffer.writeu16, + uint32 = buffer.writeu32, + + int8 = buffer.writei8, + int16 = buffer.writei16, + int32 = buffer.writei32, + + float32 = buffer.writef32, + float64 = buffer.writef64, + + opt_uint8 = function(b: buffer, cursor: number, value: number?) + if value then + buffer.writeu8(b, cursor, 1) + buffer.writeu8(b, cursor + 1, value) + else + buffer.writeu8(b, cursor, 0) + end + end, + opt_uint16 = function(b: buffer, cursor: number, value: number?) + if value then + buffer.writeu8(b, cursor, 1) + buffer.writeu16(b, cursor + 1, value) + else + buffer.writeu8(b, cursor, 0) + end + end, + opt_uint32 = function(b: buffer, cursor: number, value: number?) + if value then + buffer.writeu8(b, cursor, 1) + buffer.writeu32(b, cursor + 1, value) + else + buffer.writeu8(b, cursor, 0) + end + end, + + opt_int8 = function(b: buffer, cursor: number, value: number?) + if value then + buffer.writeu8(b, cursor, 1) + buffer.writei8(b, cursor + 1, value) + else + buffer.writeu8(b, cursor, 0) + end + end, + opt_int16 = function(b: buffer, cursor: number, value: number?) + if value then + buffer.writeu8(b, cursor, 1) + buffer.writei16(b, cursor + 1, value) + else + buffer.writeu8(b, cursor, 0) + end + end, + opt_int32 = function(b: buffer, cursor: number, value: number?) + if value then + buffer.writeu8(b, cursor, 1) + buffer.writei32(b, cursor + 1, value) + else + buffer.writeu8(b, cursor, 0) + end + end, + + opt_float32 = function(b: buffer, cursor: number, value: number?) + if value then + buffer.writeu8(b, cursor, 1) + buffer.writef32(b, cursor + 1, value) + else + buffer.writeu8(b, cursor, 0) + end + end, + opt_float64 = function(b: buffer, cursor: number, value: number?) + if value then + buffer.writeu8(b, cursor, 1) + buffer.writef64(b, cursor + 1, value) + else + buffer.writeu8(b, cursor, 0) + end + end, + + bool = function(b: buffer, cursor: number, value: boolean) + buffer.writeu8(b, cursor, if value then 1 else 0) + end, + opt_bool = function(b: buffer, cursor: number, value: boolean?) + if value then + buffer.writeu8(b, cursor, 1) + buffer.writeu8(b, cursor + 1, if value then 1 else 0) + else + buffer.writeu8(b, cursor, 0) + end + end, + + buff = function(b: buffer, cursor: number, value: buffer) + buffer.writeu32(b, cursor, buffer.len(value)) + buffer.copy(b, cursor + 4, value) + end, + opt_buff = function(b: buffer, cursor: number, value: buffer?) + if value then + buffer.writeu8(b, cursor, 1) + buffer.writeu32(b, cursor + 1, buffer.len(value)) + buffer.copy(b, cursor + 5, value) + else + buffer.writeu8(b, cursor, 0) + end + end, + + str = function(b: buffer, cursor: number, value: string) + buffer.writeu16(b, cursor, #value) + buffer.writestring(b, cursor + 2, value) + end, + opt_str = function(b: buffer, cursor: number, value: string?) + if value then + buffer.writeu8(b, cursor, 1) + buffer.writeu16(b, cursor + 1, #value) + buffer.writestring(b, cursor + 3, value) + else + buffer.writeu8(b, cursor, 0) + end + end, + + vec3 = function(b: buffer, cursor: number, value: Vector3) + buffer.writef32(b, cursor, value.X) + buffer.writef32(b, cursor + 4, value.Y) + buffer.writef32(b, cursor + 8, value.Z) + end, + opt_vec3 = function(b: buffer, cursor: number, value: Vector3?) + if value then + buffer.writeu8(b, cursor, 1) + buffer.writef32(b, cursor + 1, value.X) + buffer.writef32(b, cursor + 5, value.Y) + buffer.writef32(b, cursor + 9, value.Z) + else + buffer.writeu8(b, cursor, 0) + end + end, +} + +local lengths = { + uint8 = function() + return 1 + end, + uint16 = function() + return 2 + end, + uint32 = function() + return 4 + end, + + int8 = function() + return 1 + end, + int16 = function() + return 2 + end, + int32 = function() + return 4 + end, + + float32 = function() + return 4 + end, + float64 = function() + return 8 + end, + + opt_uint8 = function(num: number?): number + return if num then 2 else 1 + end, + opt_uint16 = function(num: number?): number + return if num then 3 else 1 + end, + opt_uint32 = function(num: number?): number + return if num then 5 else 1 + end, + + opt_int8 = function(num: number?): number + return if num then 2 else 1 + end, + opt_int16 = function(num: number?): number + return if num then 3 else 1 + end, + opt_int32 = function(num: number?): number + return if num then 5 else 1 + end, + + opt_float32 = function(num: number?): number + return if num then 5 else 1 + end, + opt_float64 = function(num: number?): number + return if num then 9 else 1 + end, + + bool = function() + return 1 + end, + opt_bool = function(bool: boolean?): number + return if bool then 2 else 1 + end, + + buff = function(buff: buffer): number + return buffer.len(buff) + 4 + end, + opt_buff = function(buff: buffer?): number + return if buff then buffer.len(buff) + 5 else 1 + end, + + str = function(str: string): number + return #str + 2 + end, + opt_str = function(str: string?): number + return if str then #str + 3 else 1 + end, + + vec3 = function() + return 12 + end, + opt_vec3 = function(vec: Vector3?): number + return if vec then 13 else 1 + end, +} + +return { + readers = readers, + writers = writers, + lengths = lengths, +} diff --git a/src/process/read.luau b/src/process/read.luau new file mode 100644 index 0000000..0060a2e --- /dev/null +++ b/src/process/read.luau @@ -0,0 +1,28 @@ +local types = require(script.Parent.Parent.types) +local retrievePacketFromID = require(script.Parent.Parent.packets.retrievePacketFromID) + +return function(incomingBuffer: buffer) + local length = buffer.len(incomingBuffer) + local readCursor = 0 + + while readCursor < length do + local id = buffer.readu8(incomingBuffer, readCursor) + readCursor += 1 + + local packet = retrievePacketFromID(id) + + local format = packet.packetFormat :: types.packetFormat + + local deserialized = {} + for _, item in format do + local value = item.reader(incomingBuffer, readCursor) + readCursor += item.length(value) + + deserialized[item.key] = value + end + + for _, listener in packet.listeners do + task.spawn(listener, deserialized) + end + end +end diff --git a/src/process/sendChannel.luau b/src/process/sendChannel.luau new file mode 100644 index 0000000..1bdf354 --- /dev/null +++ b/src/process/sendChannel.luau @@ -0,0 +1,59 @@ +local types = require(script.Parent.Parent.types) + +local sendChannelPrototype = {} +local sendChannelMetatable = { __index = sendChannelPrototype } +export type sendChannelType = typeof(setmetatable( + {} :: { + outgoingBuffer: buffer, + cursor: number, + writeJobs: { () -> () }, + }, + sendChannelMetatable +)) + +function sendChannelPrototype.add( + self: sendChannelType, + id: number, + format: types.packetFormat, + data: { [string]: any } +) + local idPosition = self.cursor + table.insert(self.writeJobs, function() + buffer.writeu8(self.outgoingBuffer, idPosition, id) + end) + self.cursor += 1 + + for _, value in format do + local currentCursor = self.cursor + table.insert(self.writeJobs, function() + print(value) + value.writer(self.outgoingBuffer, currentCursor, data[value.key]) + end) + self.cursor += value.length() + end +end + +function sendChannelPrototype.empty(self: sendChannelType): buffer? + if self.cursor == 0 then + return nil + end + + self.outgoingBuffer = buffer.create(self.cursor) + for _, writer in self.writeJobs do + writer() + end + table.clear(self.writeJobs) + self.cursor = 0 + + return self.outgoingBuffer +end + +return function(): sendChannelType + local self = setmetatable({}, sendChannelMetatable) + + self.outgoingBuffer = buffer.create(0) + self.writeJobs = {} + self.cursor = 0 + + return self +end diff --git a/src/process/server.luau b/src/process/server.luau new file mode 100644 index 0000000..6050fdd --- /dev/null +++ b/src/process/server.luau @@ -0,0 +1,112 @@ +local Players = game:GetService("Players") +local RunService = game:GetService("RunService") + +local wallyInstanceManager = require(script.Parent.Parent.Parent.wallyInstanceManager) +local types = require(script.Parent.Parent.types) +local read = require(script.Parent.read) +local sendChannel = require(script.Parent.sendChannel) +local channels = { + players = {} :: { [Player]: { reliable: sendChannel.sendChannelType, unreliable: sendChannel.sendChannelType } }, + + reliable = sendChannel(), + unreliable = sendChannel(), +} + +local function onServerEvent(player: Player, data) + if not (typeof(data) == "buffer") then + return + end + + if buffer.len(data) >= 100000 then + warn("over 100K byte limit from player: " .. player.UserId) + return + end + + read(data) +end + +local function onPlayerAdded(player: Player) + channels.players[player] = { + reliable = sendChannel(), + unreliable = sendChannel(), + } +end + +local serverProcess = {} + +function serverProcess.sendAllReliable(id: number, format: types.packetFormat, data: { [string]: any }) + channels.reliable:add(id, format, data) +end + +function serverProcess.sendAllUnreliable(id: number, format: types.packetFormat, data: { [string]: any }) + channels.unreliable:add(id, format, data) +end + +function serverProcess.sendPlayerReliable( + player: Player, + id: number, + format: types.packetFormat, + data: { [string]: any } +) + channels.players[player].reliable:add(id, format, data) +end + +function serverProcess.sendPlayerUnreliable( + player: Player, + id: number, + format: types.packetFormat, + data: { [string]: any } +) + channels.players[player].unreliable:add(id, format, data) +end + +function serverProcess.start() + local remoteInstances = { + reliable = Instance.new("RemoteEvent"), + unreliable = Instance.new("UnreliableRemoteEvent"), + } + + remoteInstances.reliable.Name = "reliable" + remoteInstances.unreliable.Name = "unreliable" + + local byteNetInstance = script.Parent.Parent.Parent + wallyInstanceManager.add(byteNetInstance, remoteInstances.reliable) + wallyInstanceManager.add(byteNetInstance, remoteInstances.unreliable) + + for _, player in Players:GetPlayers() do + onPlayerAdded(player) + end + + Players.PlayerAdded:Connect(onPlayerAdded) + Players.PlayerRemoving:Connect(function(player: Player) end) + + remoteInstances.reliable.OnServerEvent:Connect(onServerEvent) + remoteInstances.unreliable.OnServerEvent:Connect(onServerEvent) + + RunService.Heartbeat:Connect(function() + local reliableBuffer = channels.reliable:empty() + if reliableBuffer ~= nil then + remoteInstances.reliable:FireAllClients(reliableBuffer) + end + + local unreliableBuffer = channels.unreliable:empty() + if unreliableBuffer ~= nil then + remoteInstances.unreliable:FireAllClients(unreliableBuffer) + end + + for player, playerChannels in channels.players do + local reliablePlayerBuffer = playerChannels.reliable:empty() + local unreliablePlayerBuffer = playerChannels.unreliable:empty() + + if reliablePlayerBuffer ~= nil then + remoteInstances.reliable:FireClient(player, reliablePlayerBuffer) + end + + if unreliablePlayerBuffer ~= nil then + remoteInstances.unreliable:FireClient(player, unreliablePlayerBuffer) + end + end + end) +end + +return serverProcess diff --git a/src/process/server/serverProcess.luau b/src/process/server/serverProcess.luau deleted file mode 100644 index bd0c782..0000000 --- a/src/process/server/serverProcess.luau +++ /dev/null @@ -1,44 +0,0 @@ -local Players = game:GetService("Players") -local RunService = game:GetService("RunService") - -local byteNetInstance = require(script.Parent.Parent.byteNetInstance) -local wallyInstanceManager = require(script.Parent.Parent.Parent.Parent.wallyInstanceManager) - -local remoteInstances = { - reliable = Instance.new("RemoteEvent"), - unreliable = Instance.new("UnreliableRemoteEvent"), -} - -local function onServerEvent(player: Player, data) - if buffer.len(data) >= 100000 then - warn("over 100K byte limit from player: " .. player.UserId) - return - end -end - -local function onPlayerAdded(player: Player) end - -local serverProcess = {} - -function serverProcess.start() - remoteInstances.reliable.Name = "reliable" - remoteInstances.unreliable.Name = "unreliable" - - wallyInstanceManager.add(byteNetInstance, remoteInstances.reliable) - wallyInstanceManager.add(byteNetInstance, remoteInstances.unreliable) - - for _, player in Players:GetPlayers() do - onPlayerAdded(player) - end - - Players.PlayerAdded:Connect(onPlayerAdded) - - Players.PlayerRemoving:Connect(function(player: Player) end) - - remoteInstances.reliable.OnServerEvent:Connect(onServerEvent) - remoteInstances.unreliable.OnServerEvent:Connect(onServerEvent) - - RunService.Heartbeat:Connect(function() end) -end - -return serverProcess diff --git a/src/storage/currentRunContext.luau b/src/storage/currentRunContext.luau deleted file mode 100644 index e9bb27a..0000000 --- a/src/storage/currentRunContext.luau +++ /dev/null @@ -1,3 +0,0 @@ -local RunService = game:GetService("RunService") - -return if RunService:IsServer() then "server" else "client" diff --git a/src/storage/reliabilityTypeList.luau b/src/storage/reliabilityTypeList.luau deleted file mode 100644 index 53ee722..0000000 --- a/src/storage/reliabilityTypeList.luau +++ /dev/null @@ -1,9 +0,0 @@ -export type reliabilityType = string - -return { - reliable = "reliable", - unreliable = "unreliable", -} :: { - reliable: string, - unreliable: string, -} diff --git a/src/types.luau b/src/types.luau index 3fff8fb..2280ca4 100644 --- a/src/types.luau +++ b/src/types.luau @@ -1,3 +1,12 @@ +export type packetFormat = { + { + writer: (b: buffer, offset: number, value: any) -> (), + reader: (b: buffer, offset: number) -> any, + key: string, + length: (value: any) -> number, + } +} + type Packet = { sendToAll: (self: Packet, data: T) -> (), send: (self: Packet, data: T, target: Player?) -> (), @@ -8,22 +17,30 @@ export type ByteNet = { definePacket: (structure: T, reliabilityType: ("reliable" | "unreliable")?) -> Packet, dataTypes: { - uint8: number, - int8: number, + opt_uint8: () -> number?, + opt_int8: () -> number?, + uint8: () -> number, + int8: () -> number, - uint16: number, - int16: number, + opt_uint16: () -> number?, + opt_int16: () -> number?, + uint16: () -> number, + int16: () -> number, - uint32: number, - int32: number, + opt_uint32: () -> number?, + opt_int32: () -> number?, + uint32: () -> number, + int32: () -> number, - float32: number, - float64: number, + opt_float32: () -> number?, + opt_float64: () -> number?, + float32: () -> number, + float64: () -> number, - vec3: Vector3, - string: string, - bool: boolean, - buff: buffer, + vec3: () -> Vector3, + string: () -> string, + bool: () -> boolean, + buff: () -> buffer, }, } diff --git a/wally.lock b/wally.lock index 981bb6d..76063b0 100644 --- a/wally.lock +++ b/wally.lock @@ -2,20 +2,10 @@ # It is not intended for manual editing. registry = "test" -[[package]] -name = "boatbomber/boattest" -version = "0.1.1" -dependencies = [] - [[package]] name = "ffrostflame/bytenet" -version = "0.1.0" -dependencies = [["BoatTEST", "boatbomber/boattest@0.1.1"], ["LuauSignal", "ffrostflame/luausignal@0.1.3"], ["wallyInstanceManager", "ffrostflame/wally-instance-manager@0.1.0"]] - -[[package]] -name = "ffrostflame/luausignal" -version = "0.1.3" -dependencies = [] +version = "0.2.6" +dependencies = [["wallyInstanceManager", "ffrostflame/wally-instance-manager@0.1.0"]] [[package]] name = "ffrostflame/wally-instance-manager" diff --git a/wally.toml b/wally.toml index 4248f41..fb7c61f 100644 --- a/wally.toml +++ b/wally.toml @@ -1,6 +1,6 @@ [package] name = "ffrostflame/bytenet" -version = "0.2.6" +version = "0.3.0" registry = "https://github.com/UpliftGames/wally-index" realm = "shared" From 9c0341612788c296c95fd9404919d4dbf1254130 Mon Sep 17 00:00:00 2001 From: ffrostfall <80861876+ffrostflame@users.noreply.github.com> Date: Tue, 13 Feb 2024 16:38:40 -0500 Subject: [PATCH 4/9] big rewrite commit --- .gitignore | 3 +- dev/client/clientTests.client.luau | 14 +- dev/concept.luau | 113 ------ dev/server/serverTests.server.luau | 51 ++- dev/shared/testPackets.luau | 16 +- docs/Installation.md | 14 - docs/Tutorials/Getting Started.md | 44 --- docs/Tutorials/_category_.json | 4 - docs/css/smoothscroll.css | 3 + docs/index.md | 164 ++++++++ docs/intro.md | 10 - mkdocs.yml | 36 ++ moonwave.toml | 27 -- sourcemap.json | 2 +- src/dataTypes/array.luau | 40 ++ src/dataTypes/bool.luau | 24 ++ src/dataTypes/buff.luau | 30 ++ src/dataTypes/cframe.luau | 44 +++ src/dataTypes/float32.luau | 16 + src/dataTypes/float64.luau | 16 + src/dataTypes/int16.luau | 16 + src/dataTypes/int32.luau | 16 + src/dataTypes/int8.luau | 16 + src/dataTypes/map.luau | 58 +++ src/dataTypes/optional.luau | 38 ++ src/dataTypes/string.luau | 27 ++ src/dataTypes/uint16.luau | 16 + src/dataTypes/uint32.luau | 16 + src/dataTypes/uint8.luau | 16 + src/dataTypes/vec2.luau | 25 ++ src/dataTypes/vec3.luau | 25 ++ src/init.luau | 132 ++----- .../{identifiers => }/clientPacketIDs.luau | 6 +- src/packets/getUnique.luau | 9 - src/packets/packet.luau | 102 +++-- src/packets/retrievePacketFromID.luau | 12 - .../{identifiers => }/serverPacketIDs.luau | 9 +- src/process/bufferWriter.luau | 110 ++++++ src/process/client.luau | 89 ++++- src/process/dataTypes.luau | 372 ------------------ src/process/read.luau | 25 +- src/process/sendChannel.luau | 59 --- src/process/server.luau | 140 +++++-- src/types.luau | 47 ++- wally.lock | 9 +- wally.toml | 3 +- 46 files changed, 1184 insertions(+), 880 deletions(-) delete mode 100644 dev/concept.luau delete mode 100644 docs/Installation.md delete mode 100644 docs/Tutorials/Getting Started.md delete mode 100644 docs/Tutorials/_category_.json create mode 100644 docs/css/smoothscroll.css create mode 100644 docs/index.md delete mode 100644 docs/intro.md create mode 100644 mkdocs.yml delete mode 100644 moonwave.toml create mode 100644 src/dataTypes/array.luau create mode 100644 src/dataTypes/bool.luau create mode 100644 src/dataTypes/buff.luau create mode 100644 src/dataTypes/cframe.luau create mode 100644 src/dataTypes/float32.luau create mode 100644 src/dataTypes/float64.luau create mode 100644 src/dataTypes/int16.luau create mode 100644 src/dataTypes/int32.luau create mode 100644 src/dataTypes/int8.luau create mode 100644 src/dataTypes/map.luau create mode 100644 src/dataTypes/optional.luau create mode 100644 src/dataTypes/string.luau create mode 100644 src/dataTypes/uint16.luau create mode 100644 src/dataTypes/uint32.luau create mode 100644 src/dataTypes/uint8.luau create mode 100644 src/dataTypes/vec2.luau create mode 100644 src/dataTypes/vec3.luau rename src/packets/{identifiers => }/clientPacketIDs.luau (57%) delete mode 100644 src/packets/getUnique.luau delete mode 100644 src/packets/retrievePacketFromID.luau rename src/packets/{identifiers => }/serverPacketIDs.luau (63%) create mode 100644 src/process/bufferWriter.luau delete mode 100644 src/process/dataTypes.luau delete mode 100644 src/process/sendChannel.luau diff --git a/.gitignore b/.gitignore index 6262eb8..5119609 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ Packages build node_modules -sourcemap.json \ No newline at end of file +sourcemap.json +site \ No newline at end of file diff --git a/dev/client/clientTests.client.luau b/dev/client/clientTests.client.luau index 74dab24..d6c86a9 100644 --- a/dev/client/clientTests.client.luau +++ b/dev/client/clientTests.client.luau @@ -2,13 +2,21 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage") local testPackets = require(ReplicatedStorage.shared.testPackets) +--[[ReplicatedStorage.RemoteEvent.OnClientEvent:Connect(function(data) + --print(data) +end)]] + testPackets.a:listen(function(data) - print("Confirming server -> client") - print(data) + --print("Confirming server -> client") + --print(data) end) task.wait(2) testPackets.a:send({ - second = 5, + a = true, + b = { true, false, true, false, false, true }, + c = { { false }, { true } }, + d = true, + chained = "test", }) diff --git a/dev/concept.luau b/dev/concept.luau deleted file mode 100644 index d48de17..0000000 --- a/dev/concept.luau +++ /dev/null @@ -1,113 +0,0 @@ -local writeCursor = 0 -local readCursor = 0 -local packetBuffer = buffer.create(0) -local writers = {} - -local dataTypeWriters = { - u8 = buffer.writeu8, -} -local dataTypeReaders = { - u8 = buffer.readu8, -} -local dataTypeLengths = { - u8 = function() - return 1 - end, -} - -local dataTypes = { - u8 = "u8", -} - -local function createPacketStructure(structure: { [string]: string }) - local structured = {} - for key, value in structure do - local accessIndex = dataTypes[value] - local reader, writer, length = - dataTypeReaders[accessIndex], dataTypeWriters[accessIndex], dataTypeLengths[accessIndex] - - table.insert(structured, { - reader = reader, - writer = writer, - length = length, - key = key, - }) - end - return structured -end - -local allPackets = { - [1] = createPacketStructure({ - a = dataTypes.u8, - b = dataTypes.u8, - }), -} - -local function writePacket( - id: number, - structure: { - { - writer: (b: buffer, offset: number, value: any) -> (), - reader: (b: buffer, offset: number) -> (), - key: string, - length: () -> number, - } - }, - dict: { [string]: number } -) - local idPosition = writeCursor - table.insert(writers, function() - buffer.writeu8(packetBuffer, idPosition, id) - end) - writeCursor += 1 - - for _, value in structure do - local currentCursor = writeCursor - table.insert(writers, function() - value.writer(packetBuffer, currentCursor, dict[value.key]) - end) - writeCursor += value.length() - end -end - --- The user sends data -local start = os.clock() -for _ = 1, 1000 do - writePacket(1, allPackets[1], { - a = 1, - b = 2, - }) -end -print(os.clock() - start) - -print(allPackets[1]) --- This would be run every frame -local start2 = os.clock() -packetBuffer = buffer.create(writeCursor) -for _, writer in writers do - writer() -end -table.clear(writers) -print(os.clock() - start2) - --- Reading - -local function read(incomingBuffer: buffer) - local length = buffer.len(incomingBuffer) - - while readCursor < length do - local id = buffer.readu8(incomingBuffer, readCursor) - readCursor += 1 - - local structure = allPackets[id] - local deserialized = {} - for _, item in structure do - local value = item.reader(incomingBuffer, readCursor) - readCursor += item.length() - - deserialized[item.key] = value - end - end -end - -read(packetBuffer) diff --git a/dev/server/serverTests.server.luau b/dev/server/serverTests.server.luau index 33bdd42..5809d1a 100644 --- a/dev/server/serverTests.server.luau +++ b/dev/server/serverTests.server.luau @@ -1,14 +1,57 @@ +--!native +--!optimize 2 local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") +local RunService = game:GetService("RunService") local testPackets = require(ReplicatedStorage.shared.testPackets) -Players.PlayerAdded:Connect(function(player) +local data = { + a = false, + b = { true, false, false }, + c = { { false }, { true } }, + d = nil, + e = { [8] = 4 }, + f = { + [Vector3.new(15, 15, 15)] = 2554, + }, +} + +RunService.Heartbeat:Connect(function() + --[[for i = 1, 100 do + ReplicatedStorage.RemoteEvent:FireAllClients({ + a = false, + b = { true, false, false }, + c = { { false }, { true } }, + d = nil, + e = { [8] = 4 }, + f = { + [Vector3.new(15, 15, 15)] = 2554, + }, + }) + end]] + debug.profilebegin("send") + for _ = 1, 100 do + testPackets.a:sendToAll(data) + end + debug.profileend() +end) + +Players.PlayerAdded:Connect(function() task.wait(1) - testPackets.a:send({ second = 5 }, player) + testPackets.a:sendToAll({ + a = false, + b = { true, false, false }, + c = { { false }, { true } }, + d = nil, + e = { [8] = 4 }, + f = { + [Vector3.new(15, 15, 15)] = 2554, + }, + }) end) -testPackets.a:listen(function(data) +testPackets.a:listen(function(a) print("Confirming client -> server") - print(data) + print(a) end) diff --git a/dev/shared/testPackets.luau b/dev/shared/testPackets.luau index af963e6..0f14f59 100644 --- a/dev/shared/testPackets.luau +++ b/dev/shared/testPackets.luau @@ -3,5 +3,19 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage") local ByteNet = require(ReplicatedStorage.Packages.ByteNet) return { - a = ByteNet.definePacket({ second = ByteNet.dataTypes.uint8() }), + a = ByteNet.definePacket({ + structure = { + a = ByteNet.dataTypes.bool(), + b = ByteNet.dataTypes.array(ByteNet.dataTypes.bool()), + c = ByteNet.dataTypes.array(ByteNet.dataTypes.array(ByteNet.dataTypes.bool())), + d = ByteNet.dataTypes.optional(ByteNet.dataTypes.bool()), + e = ByteNet.dataTypes.optional( + ByteNet.dataTypes.map(ByteNet.dataTypes.uint16(), ByteNet.dataTypes.uint8()) + ), + f = ByteNet.dataTypes.optional(ByteNet.dataTypes.map(ByteNet.dataTypes.vec3(), ByteNet.dataTypes.uint16())), + chained = ByteNet.dataTypes.optional( + ByteNet.dataTypes.optional(ByteNet.dataTypes.optional(ByteNet.dataTypes.string())) + ), + }, + }), } diff --git a/docs/Installation.md b/docs/Installation.md deleted file mode 100644 index 106abed..0000000 --- a/docs/Installation.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -sidebar_position: 2 ---- - -# Installation - -## Through Wally [Recommended] - -If you're using Wally, you can simply drop this snippet in, except replace `latest` with the latest ByteNet version. - -```toml title="wally.toml" -[dependencies] -ByteNet = "ffrostflame/bytenet@latest" -``` \ No newline at end of file diff --git a/docs/Tutorials/Getting Started.md b/docs/Tutorials/Getting Started.md deleted file mode 100644 index 42b9990..0000000 --- a/docs/Tutorials/Getting Started.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -sidebar_position: 1 ---- - -# Getting Started - -## Your first Packet -To actually make use of ByteNet, you'll first need to create a packet. Packets require a structure like so: -```lua -local ByteNet = require(path.to.ByteNet) - -local myPacket = ByteNet.definePacket({ - textField = ByteNet.dataTypes.string, -}) -``` -You might've noticed this, but **you don't provide ByteNet a name.** What gives? ByteNet is built around your networking structures being shared. Instead of relying on a name, ByteNet relies on your packet structure. So you'll want to create a ModuleScript under ReplicatedStorage to store your packets, instead of relying on individual scripts. For example: -```lua name="packets.luau" --- under ReplicatedStorage -local ByteNet = require(path.to.ByteNet) - -return { - myPacket = ByteNet.definePacket("reliable", { - textField = ByteNet.dataTypes.string, - }) -} -``` -Great! Now we have a packet. Luckily, utilizing packets in ByteNet is extremely simple: -```lua --- On the server... -local ByteNet = require(path.to.ByteNet) -local packets = require(path.to.packets) - -packets.myPacket:listen(function(data, player) - print(`{ player.Name } said {data.textField}`) -end) - --- On the client.. -local ByteNet = require(path.to.ByteNet) -local packets = require(path.to.packets) - -packets.myPacket:send({ - textField = "Hello, world!" -}) -``` \ No newline at end of file diff --git a/docs/Tutorials/_category_.json b/docs/Tutorials/_category_.json deleted file mode 100644 index 9f3dfb7..0000000 --- a/docs/Tutorials/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Tutorials", - "position": 3 - } \ No newline at end of file diff --git a/docs/css/smoothscroll.css b/docs/css/smoothscroll.css new file mode 100644 index 0000000..21db7fe --- /dev/null +++ b/docs/css/smoothscroll.css @@ -0,0 +1,3 @@ +html { + scroll-behavior: smooth; +} \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..791c4f6 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,164 @@ +# a +a +a +a +a +a +a +aa +a + +a +aa +a + +a + + + +a + + + + + + +a + + + + +a +# a +a +a +a +a +a +a +aa +a + +a +aa +a + +a + + + +a + + + + + + +a + + + + +a +# a +a +a +a +a +a +a +aa +a + +a +aa +a + +a + + + +a + + + + + + +a + + + + +a +# a +a +a +a +a +a +a +aa +a + +a +aa +a + +a + + + +a + + + + + + +a + + + + +a +# a +a +a +a +a +a +a +aa +a + +a +aa +a + +!!! note "This yields." + Amazing + +``` lua +-- test +print("hello world!") + +``` + +a + + + +a + + + + + + +a + + + + +a \ No newline at end of file diff --git a/docs/intro.md b/docs/intro.md deleted file mode 100644 index 7bdb2c1..0000000 --- a/docs/intro.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -sidebar_position: 1 ---- - -# Intro - -ByteNet is an advanced networking library targeted at devs who want way more granular control over their networking. It sends solely buffers through RemoteEvents, w/ automatic queueing functionality, and has built-in serialization and deserialization. This means you basically just don't have to worry about optimization when using ByteNet! - -## Packets -ByteNet is built off an object called a `packet`. These packets have a clear and unchanging structure. You create packets by using `definePacket`, then use the `send` and `listen` methods to send and receive networked information. You send, and receive these packets as dictionaries; key-value pairs. But don't worry! The keys are serialized away, they don't affect performance or packet size **at all.** \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..cfc6f2e --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,36 @@ +site_name: ByteNet + +repo_url: https://github.com/ffrostflame/ByteNet + +### Build settings ### + +theme: readthedocs + +nav: + - Home: index.md +theme: + name: material + palette: + scheme: slate + font: + text: Roboto + code: Fira Code + features: + - content.code.copy + extra_css: + - css/smoothscroll.css + + icon: + repo: octicons/mark-github-16 + +markdown_extensions: + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences + - admonition + - pymdownx.details + - pymdownx.superfences \ No newline at end of file diff --git a/moonwave.toml b/moonwave.toml deleted file mode 100644 index e2dd997..0000000 --- a/moonwave.toml +++ /dev/null @@ -1,27 +0,0 @@ -title = "ByteNet" -gitRepoUrl = "https://github.com/ffrostflame/bytenet/" - -changelog = true - -[docusaurus] -projectName = "ByteNet" -tagline = "description" - -[footer] -style = "dark" - -[home] -enabled = false -includeReadme = false - -[[home.features]] -title = "Cuts out Roblox overhead" -description = "You decide what you're sending. Supports uint8, int16, etc." - -[[home.features]] -title = "Strictly typed" -description = "Written purely in Luau, for Luau" - -[[home.features]] -title = "Simple" -description = "Easy to learn quickly" \ No newline at end of file diff --git a/sourcemap.json b/sourcemap.json index 07dba58..d832e58 100644 --- a/sourcemap.json +++ b/sourcemap.json @@ -1 +1 @@ -{"name":"bytenet-dev","className":"DataModel","filePaths":["dev.project.json"],"children":[{"name":"ReplicatedStorage","className":"ReplicatedStorage","children":[{"name":"Packages","className":"Folder","children":[{"name":"_Index","className":"Folder","children":[{"name":"ffrostflame_wally-instance-manager@0.1.0","className":"Folder","children":[{"name":"wally-instance-manager","className":"ModuleScript","filePaths":["Packages\\_Index\\ffrostflame_wally-instance-manager@0.1.0\\wally-instance-manager\\src\\init.luau","Packages\\_Index\\ffrostflame_wally-instance-manager@0.1.0\\wally-instance-manager\\default.project.json"]}]}]},{"name":"wallyInstanceManager","className":"ModuleScript","filePaths":["Packages\\wallyInstanceManager.lua"]},{"name":"ByteNet","className":"ModuleScript","filePaths":["src\\init.luau"],"children":[{"name":"packets","className":"Folder","children":[{"name":"getUnique","className":"ModuleScript","filePaths":["src\\packets\\getUnique.luau"]},{"name":"identifiers","className":"Folder","children":[{"name":"clientPacketIDs","className":"ModuleScript","filePaths":["src\\packets\\identifiers\\clientPacketIDs.luau"]},{"name":"serverPacketIDs","className":"ModuleScript","filePaths":["src\\packets\\identifiers\\serverPacketIDs.luau"]}]},{"name":"packet","className":"ModuleScript","filePaths":["src\\packets\\packet.luau"]},{"name":"retrievePacketFromID","className":"ModuleScript","filePaths":["src\\packets\\retrievePacketFromID.luau"]}]},{"name":"process","className":"Folder","children":[{"name":"client","className":"ModuleScript","filePaths":["src\\process\\client.luau"]},{"name":"dataTypes","className":"ModuleScript","filePaths":["src\\process\\dataTypes.luau"]},{"name":"read","className":"ModuleScript","filePaths":["src\\process\\read.luau"]},{"name":"sendChannel","className":"ModuleScript","filePaths":["src\\process\\sendChannel.luau"]},{"name":"server","className":"ModuleScript","filePaths":["src\\process\\server.luau"]}]},{"name":"types","className":"ModuleScript","filePaths":["src\\types.luau"]}]}]},{"name":"shared","className":"Folder","children":[{"name":"testPackets","className":"ModuleScript","filePaths":["dev/shared\\testPackets.luau"]}]}]},{"name":"ServerScriptService","className":"ServerScriptService","children":[{"name":"server","className":"Folder","children":[{"name":"serverTests","className":"Script","filePaths":["dev/server\\serverTests.server.luau"]}]}]},{"name":"StarterPlayer","className":"StarterPlayer","children":[{"name":"StarterPlayerScripts","className":"StarterPlayerScripts","children":[{"name":"clientTests","className":"LocalScript","filePaths":["dev/client\\clientTests.client.luau"]}]}]}]} \ No newline at end of file +{"name":"bytenet-dev","className":"DataModel","filePaths":["dev.project.json"],"children":[{"name":"ReplicatedStorage","className":"ReplicatedStorage","children":[{"name":"Packages","className":"Folder","children":[{"name":"TableKit","className":"ModuleScript","filePaths":["Packages\\TableKit.lua"]},{"name":"_Index","className":"Folder","children":[{"name":"ffrostflame_tablekit@0.2.4","className":"Folder","children":[{"name":"tablekit","className":"ModuleScript","filePaths":["Packages\\_Index\\ffrostflame_tablekit@0.2.4\\tablekit\\src\\init.luau","Packages\\_Index\\ffrostflame_tablekit@0.2.4\\tablekit\\default.project.json"]}]},{"name":"ffrostflame_wally-instance-manager@0.1.0","className":"Folder","children":[{"name":"wally-instance-manager","className":"ModuleScript","filePaths":["Packages\\_Index\\ffrostflame_wally-instance-manager@0.1.0\\wally-instance-manager\\src\\init.luau","Packages\\_Index\\ffrostflame_wally-instance-manager@0.1.0\\wally-instance-manager\\default.project.json"]}]}]},{"name":"wallyInstanceManager","className":"ModuleScript","filePaths":["Packages\\wallyInstanceManager.lua"]},{"name":"ByteNet","className":"ModuleScript","filePaths":["src\\init.luau"],"children":[{"name":"dataTypes","className":"Folder","children":[{"name":"array","className":"ModuleScript","filePaths":["src\\dataTypes\\array.luau"]},{"name":"bool","className":"ModuleScript","filePaths":["src\\dataTypes\\bool.luau"]},{"name":"buff","className":"ModuleScript","filePaths":["src\\dataTypes\\buff.luau"]},{"name":"cframe","className":"ModuleScript","filePaths":["src\\dataTypes\\cframe.luau"]},{"name":"float32","className":"ModuleScript","filePaths":["src\\dataTypes\\float32.luau"]},{"name":"float64","className":"ModuleScript","filePaths":["src\\dataTypes\\float64.luau"]},{"name":"int16","className":"ModuleScript","filePaths":["src\\dataTypes\\int16.luau"]},{"name":"int32","className":"ModuleScript","filePaths":["src\\dataTypes\\int32.luau"]},{"name":"int8","className":"ModuleScript","filePaths":["src\\dataTypes\\int8.luau"]},{"name":"map","className":"ModuleScript","filePaths":["src\\dataTypes\\map.luau"]},{"name":"optional","className":"ModuleScript","filePaths":["src\\dataTypes\\optional.luau"]},{"name":"string","className":"ModuleScript","filePaths":["src\\dataTypes\\string.luau"]},{"name":"uint16","className":"ModuleScript","filePaths":["src\\dataTypes\\uint16.luau"]},{"name":"uint32","className":"ModuleScript","filePaths":["src\\dataTypes\\uint32.luau"]},{"name":"uint8","className":"ModuleScript","filePaths":["src\\dataTypes\\uint8.luau"]},{"name":"vec2","className":"ModuleScript","filePaths":["src\\dataTypes\\vec2.luau"]},{"name":"vec3","className":"ModuleScript","filePaths":["src\\dataTypes\\vec3.luau"]}]},{"name":"packets","className":"Folder","children":[{"name":"clientPacketIDs","className":"ModuleScript","filePaths":["src\\packets\\clientPacketIDs.luau"]},{"name":"packet","className":"ModuleScript","filePaths":["src\\packets\\packet.luau"]},{"name":"serverPacketIDs","className":"ModuleScript","filePaths":["src\\packets\\serverPacketIDs.luau"]}]},{"name":"process","className":"Folder","children":[{"name":"bufferWriter","className":"ModuleScript","filePaths":["src\\process\\bufferWriter.luau"]},{"name":"client","className":"ModuleScript","filePaths":["src\\process\\client.luau"]},{"name":"read","className":"ModuleScript","filePaths":["src\\process\\read.luau"]},{"name":"server","className":"ModuleScript","filePaths":["src\\process\\server.luau"]}]},{"name":"types","className":"ModuleScript","filePaths":["src\\types.luau"]}]}]},{"name":"shared","className":"Folder","children":[{"name":"testPackets","className":"ModuleScript","filePaths":["dev/shared\\testPackets.luau"]}]}]},{"name":"ServerScriptService","className":"ServerScriptService","children":[{"name":"server","className":"Folder","children":[{"name":"serverTests","className":"Script","filePaths":["dev/server\\serverTests.server.luau"]}]}]},{"name":"StarterPlayer","className":"StarterPlayer","children":[{"name":"StarterPlayerScripts","className":"StarterPlayerScripts","children":[{"name":"clientTests","className":"LocalScript","filePaths":["dev/client\\clientTests.client.luau"]}]}]}]} \ No newline at end of file diff --git a/src/dataTypes/array.luau b/src/dataTypes/array.luau new file mode 100644 index 0000000..3b5b9fc --- /dev/null +++ b/src/dataTypes/array.luau @@ -0,0 +1,40 @@ +local bufferWriter = require(script.Parent.Parent.process.bufferWriter) +local types = require(script.Parent.Parent.types) + +local u16 = bufferWriter.u16 + +--[[ + Create a new array with the given dataTypeInterface +]] +return function(valueType: types.dataTypeInterface) + local valueWrite = valueType.write + + return { + read = function(b: buffer, cursor: number) + local arrayLength = buffer.readu16(b, cursor) + local arrayCursor = cursor + 2 + local array = {} + + for _ = 1, arrayLength do + local item, length = valueType.read(b, arrayCursor) + table.insert(array, item) + + arrayCursor += length + end + + return array, (arrayCursor - cursor) + end, + write = function(cursor: number, value: any) + u16(cursor, #value) -- write length, 2 bytes + local arrayCursor = cursor + 2 + + for _, item in value do + -- add the length of the item to the cursor + arrayCursor += valueWrite(arrayCursor, item) + end + + -- return size of the array: arrayCursor - cursor = size + return arrayCursor - cursor + end, + } +end :: (valueType: types.dataTypeInterface) -> types.dataTypeInterface<{ [number]: T }> diff --git a/src/dataTypes/bool.luau b/src/dataTypes/bool.luau new file mode 100644 index 0000000..85a8c22 --- /dev/null +++ b/src/dataTypes/bool.luau @@ -0,0 +1,24 @@ +local bufferWriter = require(script.Parent.Parent.process.bufferWriter) +local types = require(script.Parent.Parent.types) + +local btrue = bufferWriter.btrue +local bfalse = bufferWriter.bfalse + +local bool = { + --[[ + 1 = true + 0 = false + + Write and read based off a uint8 + ]] + read = function(b: buffer, cursor: number) + return buffer.readu8(b, cursor) == 1, 1 + end, + write = function(cursor: number, value: boolean) + return if value then btrue(cursor) else bfalse(cursor) + end, +} + +return function(): types.dataTypeInterface + return bool +end diff --git a/src/dataTypes/buff.luau b/src/dataTypes/buff.luau new file mode 100644 index 0000000..b6ff5cb --- /dev/null +++ b/src/dataTypes/buff.luau @@ -0,0 +1,30 @@ +local bufferWriter = require(script.Parent.Parent.process.bufferWriter) +local types = require(script.Parent.Parent.types) + +local u16 = bufferWriter.u16 +local copy = bufferWriter.copy + +local buff = { + read = function(b: buffer, cursor: number) + local length = buffer.readu16(b, cursor) + local freshBuffer = buffer.create(length) + + -- copy the data from the main buffer to the new buffer with an offset of 2 because of length + buffer.copy(freshBuffer, 0, b, cursor + 2, length) + + return freshBuffer, length + 2 + end, + write = function(cursor: number, data: buffer) + local length = buffer.len(data) + + -- write the length of the buffer, then the buffer itself + u16(cursor, length) + copy(cursor + 2, data) + + return length + 2 + end, +} + +return function(): types.dataTypeInterface + return buff +end diff --git a/src/dataTypes/cframe.luau b/src/dataTypes/cframe.luau new file mode 100644 index 0000000..6d382ad --- /dev/null +++ b/src/dataTypes/cframe.luau @@ -0,0 +1,44 @@ +local bufferWriter = require(script.Parent.Parent.process.bufferWriter) +local types = require(script.Parent.Parent.types) + +local f32 = bufferWriter.f32 + +-- thanks jack :p +local cframe = { + read = function(b: buffer, cursor: number) + local x = buffer.readf32(b, cursor) + local y = buffer.readf32(b, cursor + 4) + local z = buffer.readf32(b, cursor + 8) + local rx = buffer.readf32(b, cursor + 12) + local ry = buffer.readf32(b, cursor + 16) + local rz = buffer.readf32(b, cursor + 20) + + -- Re-construct the CFrame from the axis-angle representation + local axis = Vector3.new(rx, ry, rz) + local angle = axis.Magnitude + + return CFrame.fromAxisAngle(axis, angle) + Vector3.new(x, y, z), 24 + end, + write = function(cursor: number, value: CFrame) + -- Convert the CFrame to an axis-angle representation + local x, y, z = value.X, value.Y, value.Z + + local axis, angle = value:ToAxisAngle() + local rx, ry, rz = axis.X, axis.Y, axis.Z + axis = axis * angle + + -- Math done, write it now + f32(cursor, x) + f32(cursor + 4, y) + f32(cursor + 8, z) + f32(cursor + 12, rx) + f32(cursor + 16, ry) + f32(cursor + 20, rz) + + return 24 + end, +} + +return function(): types.dataTypeInterface + return cframe +end diff --git a/src/dataTypes/float32.luau b/src/dataTypes/float32.luau new file mode 100644 index 0000000..080c75c --- /dev/null +++ b/src/dataTypes/float32.luau @@ -0,0 +1,16 @@ +local bufferWriter = require(script.Parent.Parent.process.bufferWriter) +local types = require(script.Parent.Parent.types) + +local f32 = bufferWriter.f32 + +local float32 = { + write = f32, + + read = function(b: buffer, cursor: number) + return buffer.readf32(b, cursor), 4 + end, +} + +return function(): types.dataTypeInterface + return float32 +end diff --git a/src/dataTypes/float64.luau b/src/dataTypes/float64.luau new file mode 100644 index 0000000..39490af --- /dev/null +++ b/src/dataTypes/float64.luau @@ -0,0 +1,16 @@ +local bufferWriter = require(script.Parent.Parent.process.bufferWriter) +local types = require(script.Parent.Parent.types) + +local f64 = bufferWriter.f64 + +local float64 = { + write = f64, + + read = function(b: buffer, cursor: number) + return buffer.readf64(b, cursor), 8 + end, +} + +return function(): types.dataTypeInterface + return float64 +end diff --git a/src/dataTypes/int16.luau b/src/dataTypes/int16.luau new file mode 100644 index 0000000..75dce17 --- /dev/null +++ b/src/dataTypes/int16.luau @@ -0,0 +1,16 @@ +local bufferWriter = require(script.Parent.Parent.process.bufferWriter) +local types = require(script.Parent.Parent.types) + +local i16 = bufferWriter.i16 + +local int16 = { + write = i16, + + read = function(b: buffer, cursor: number) + return buffer.readi16(b, cursor), 2 + end, +} + +return function(): types.dataTypeInterface + return int16 +end diff --git a/src/dataTypes/int32.luau b/src/dataTypes/int32.luau new file mode 100644 index 0000000..734d8ac --- /dev/null +++ b/src/dataTypes/int32.luau @@ -0,0 +1,16 @@ +local bufferWriter = require(script.Parent.Parent.process.bufferWriter) +local types = require(script.Parent.Parent.types) + +local i32 = bufferWriter.i32 + +local int32 = { + write = i32, + + read = function(b: buffer, cursor: number) + return buffer.readi32(b, cursor), 4 + end, +} + +return function(): types.dataTypeInterface + return int32 +end diff --git a/src/dataTypes/int8.luau b/src/dataTypes/int8.luau new file mode 100644 index 0000000..213adb7 --- /dev/null +++ b/src/dataTypes/int8.luau @@ -0,0 +1,16 @@ +local bufferWriter = require(script.Parent.Parent.process.bufferWriter) +local types = require(script.Parent.Parent.types) + +local i8 = bufferWriter.i8 + +local int8 = { + write = i8, + + read = function(b: buffer, cursor: number) + return buffer.readi8(b, cursor), 1 + end, +} + +return function(): types.dataTypeInterface + return int8 +end diff --git a/src/dataTypes/map.luau b/src/dataTypes/map.luau new file mode 100644 index 0000000..f06547e --- /dev/null +++ b/src/dataTypes/map.luau @@ -0,0 +1,58 @@ +local bufferWriter = require(script.Parent.Parent.process.bufferWriter) +local types = require(script.Parent.Parent.types) + +local u16 = bufferWriter.u16 + +-- thanks jack :p +return function( + keyType: types.dataTypeInterface, + valueType: types.dataTypeInterface +): types.dataTypeInterface<{ [any]: any }> + -- Cache these functions to avoid the overhead of the index + local keyWrite = keyType.write + local valueWrite = valueType.write + + return { + read = function(b: buffer, cursor: number) + local map = {} + local mapCursor = cursor + + -- Read map length + local mapLength = buffer.readu16(b, mapCursor) + mapCursor += 2 + + for _ = 1, mapLength do + -- read key/value pairs and add them to the map + local key, keyLength = keyType.read(b, mapCursor) + mapCursor += keyLength + + local value, valueLength = valueType.read(b, mapCursor) + mapCursor += valueLength + + map[key] = value + end + + -- Return the map, alongside length, because mapCursor - cursor = size + return map, mapCursor - cursor + end, + write = function(cursor: number, map: any) + -- Don't do the count just yet because we have to loop either way + local count = 0 + local mapCursor = cursor + 2 + + for k, v in map do + count += 1 + + -- Write key/value pairs and add length to mapCursor + mapCursor += keyWrite(mapCursor, k) + mapCursor += valueWrite(mapCursor, v) + end + + -- Write length + u16(cursor, count) + + -- again, mapCursor - cursor = size + return mapCursor - cursor + end, + } +end diff --git a/src/dataTypes/optional.luau b/src/dataTypes/optional.luau new file mode 100644 index 0000000..e1ac3f7 --- /dev/null +++ b/src/dataTypes/optional.luau @@ -0,0 +1,38 @@ +local bufferWriter = require(script.Parent.Parent.process.bufferWriter) +local types = require(script.Parent.Parent.types) + +local btrue = bufferWriter.btrue +local bfalse = bufferWriter.bfalse + +return function(valueType: types.dataTypeInterface) + return { + --[[ + first byte is a boolean, if it's true, the next bytes are the value of valueType + if it's false, its length of 1 cuz only 1 boolean + ]] + + read = function(b: buffer, cursor: number) + local exists = buffer.readu8(b, cursor) + + if exists == 0 then + -- doesn't exist + return nil, 1 + else + -- exists, read the value + local item, length = valueType.read(b, cursor + 1) + return item, length + 1 + end + end, + write = function(cursor: number, value: any) + if value == nil then + -- preemptively write false + bfalse(cursor) + return 1 + end + + -- write true, then the value + btrue(cursor) + return valueType.write(cursor + 1, value) + 1 + end, + } +end :: (valueType: types.dataTypeInterface) -> types.dataTypeInterface diff --git a/src/dataTypes/string.luau b/src/dataTypes/string.luau new file mode 100644 index 0000000..e1b73c5 --- /dev/null +++ b/src/dataTypes/string.luau @@ -0,0 +1,27 @@ +local bufferWriter = require(script.Parent.Parent.process.bufferWriter) +local types = require(script.Parent.Parent.types) + +local u16 = bufferWriter.u16 +local writestring = bufferWriter.writestring + +local str = { + -- 2 bytes for the length, then the string + + read = function(b: buffer, cursor: number) + local length = buffer.readu16(b, cursor) + + return buffer.readstring(b, cursor + 2, length), length + 2 + end, + write = function(cursor: number, data: string) + local length = string.len(data) + + u16(cursor, length) + writestring(cursor + 2, data) + + return length + 2 + end, +} + +return function(): types.dataTypeInterface + return str +end diff --git a/src/dataTypes/uint16.luau b/src/dataTypes/uint16.luau new file mode 100644 index 0000000..e51519e --- /dev/null +++ b/src/dataTypes/uint16.luau @@ -0,0 +1,16 @@ +local bufferWriter = require(script.Parent.Parent.process.bufferWriter) +local types = require(script.Parent.Parent.types) + +local u16 = bufferWriter.u16 + +local uint16 = { + write = u16, + + read = function(b: buffer, cursor: number) + return buffer.readu16(b, cursor), 2 + end, +} + +return function(): types.dataTypeInterface + return uint16 +end diff --git a/src/dataTypes/uint32.luau b/src/dataTypes/uint32.luau new file mode 100644 index 0000000..d63627f --- /dev/null +++ b/src/dataTypes/uint32.luau @@ -0,0 +1,16 @@ +local bufferWriter = require(script.Parent.Parent.process.bufferWriter) +local types = require(script.Parent.Parent.types) + +local u32 = bufferWriter.u32 + +local uint32 = { + write = u32, + + read = function(b: buffer, cursor: number) + return buffer.readu32(b, cursor), 4 + end, +} + +return function(): types.dataTypeInterface + return uint32 +end diff --git a/src/dataTypes/uint8.luau b/src/dataTypes/uint8.luau new file mode 100644 index 0000000..e4f152e --- /dev/null +++ b/src/dataTypes/uint8.luau @@ -0,0 +1,16 @@ +local types = require(script.Parent.Parent.types) +local bufferWriter = require(script.Parent.Parent.process.bufferWriter) + +local u8 = bufferWriter.u8 + +local uint8 = { + write = u8, + + read = function(b: buffer, cursor: number) + return buffer.readu8(b, cursor), 1 + end, +} + +return function(): types.dataTypeInterface + return uint8 +end diff --git a/src/dataTypes/vec2.luau b/src/dataTypes/vec2.luau new file mode 100644 index 0000000..7b016e8 --- /dev/null +++ b/src/dataTypes/vec2.luau @@ -0,0 +1,25 @@ +local bufferWriter = require(script.Parent.Parent.process.bufferWriter) +local types = require(script.Parent.Parent.types) + +local f32 = bufferWriter.f32 + +local vec2 = { + --[[ + 2 float32s, one for X, one for Y + ]] + + read = function(b: buffer, cursor: number) + return Vector2.new(buffer.readf32(b, cursor), buffer.readf32(b, cursor + 4)), 8 + end, + + write = function(cursor: number, value: Vector2) + f32(cursor, value.X) + f32(cursor + 4, value.Y) + + return 8 + end, +} + +return function(): types.dataTypeInterface + return vec2 +end diff --git a/src/dataTypes/vec3.luau b/src/dataTypes/vec3.luau new file mode 100644 index 0000000..235f2a7 --- /dev/null +++ b/src/dataTypes/vec3.luau @@ -0,0 +1,25 @@ +local bufferWriter = require(script.Parent.Parent.process.bufferWriter) +local types = require(script.Parent.Parent.types) + +local f32 = bufferWriter.f32 + +local vec3 = { + --[[ + 3 floats, 12 bytes + ]] + read = function(b: buffer, cursor: number) + return Vector3.new(buffer.readf32(b, cursor), buffer.readf32(b, cursor + 4), buffer.readf32(b, cursor + 8)), 12 + end, + + write = function(cursor: number, value: Vector3) + f32(cursor, value.X) + f32(cursor + 4, value.Y) + f32(cursor + 8, value.Z) + + return 12 + end, +} + +return function(): types.dataTypeInterface + return vec3 +end diff --git a/src/init.luau b/src/init.luau index 2488f84..4315e1a 100644 --- a/src/init.luau +++ b/src/init.luau @@ -1,9 +1,26 @@ local RunService = game:GetService("RunService") -local clientPacketIDs = require(script.packets.identifiers.clientPacketIDs) -local serverPacketIDs = require(script.packets.identifiers.serverPacketIDs) +local clientPacketIDs = require(script.packets.clientPacketIDs) +local serverPacketIDs = require(script.packets.serverPacketIDs) +local packet = require(script.packets.packet) local clientProcess = require(script.process.client) -local dataTypes = require(script.process.dataTypes) +local array = require(script.dataTypes.array) +local bool = require(script.dataTypes.bool) +local buff = require(script.dataTypes.buff) +local cframe = require(script.dataTypes.cframe) +local float32 = require(script.dataTypes.float32) +local float64 = require(script.dataTypes.float64) +local int16 = require(script.dataTypes.int16) +local int32 = require(script.dataTypes.int32) +local int8 = require(script.dataTypes.int8) +local map = require(script.dataTypes.map) +local optional = require(script.dataTypes.optional) +local string = require(script.dataTypes.string) +local uint16 = require(script.dataTypes.uint16) +local uint32 = require(script.dataTypes.uint32) +local uint8 = require(script.dataTypes.uint8) +local vec2 = require(script.dataTypes.vec2) +local vec3 = require(script.dataTypes.vec3) local serverProcess = require(script.process.server) local types = require(script.types) @@ -15,95 +32,28 @@ else clientPacketIDs.start() end ---[=[ - @class ByteNet - - The root interface for ByteNet. -]=] - ---[=[ - @function definePacket - @within ByteNet - - @param reliabilityType "reliable" | "unreliable" - @param structure { [string]: DataType } - - @return Packet -]=] - ---[=[ - @type DataType [arbitrary] - @within ByteNet - Declares the type of data of a field. -]=] - ---[=[ - @interface dataTypes - @within ByteNet - .string DataType - .uint8 DataType - .int8 DataType - .uint16 DataType - .int16 DataType - .uint32 DataType - .int32 DataType - .f32 DataType - .f64 DataType - .v3 DataType - .bool DataType -]=] - ---[=[ - @class Packet - - The class returned by `ByteNet.definePacket`. Packets are utilized to send data between the server and client. -]=] - ---[=[ - @method sendToAll - @within Packet - @server - - @param data { [string]: any } - - Sends data to all players. -]=] - ---[=[ - @method send - @within Packet - - @param data { [string]: any } - @param target Player? - - If on the client, sends data to the server. If on the server, sends data to a specific player. -]=] - ---[=[ - @method listen - @within Packet - - @param callback (data: {[string]: any}, player: Player?) -> () - - When the packet is received, the callback is called with the data, and if on the server, the player who sent the packet. -]=] - -local dataTypeTable = setmetatable({}, { - __index = function(_, index) - return function() - if not dataTypes.writers[index] then - error("Invalid data type: " .. index) - end - - return index - end - end, -}) - return ( table.freeze({ - definePacket = require(script.packets.packet), - - dataTypes = dataTypeTable, + definePacket = packet, + + dataTypes = { + array = array, + bool = bool, + optional = optional, + uint8 = uint8, + uint16 = uint16, + uint32 = uint32, + int8 = int8, + int16 = int16, + int32 = int32, + float32 = float32, + float64 = float64, + cframe = cframe, + string = string, + vec2 = vec2, + vec3 = vec3, + buff = buff, + map = map, + }, }) :: any ) :: types.ByteNet diff --git a/src/packets/identifiers/clientPacketIDs.luau b/src/packets/clientPacketIDs.luau similarity index 57% rename from src/packets/identifiers/clientPacketIDs.luau rename to src/packets/clientPacketIDs.luau index 7962c3c..eafa4b5 100644 --- a/src/packets/identifiers/clientPacketIDs.luau +++ b/src/packets/clientPacketIDs.luau @@ -1,4 +1,4 @@ -local wallyInstanceManager = require(script.Parent.Parent.Parent.Parent.wallyInstanceManager) +local ReplicatedStorage = game:GetService("ReplicatedStorage") local packetStorage = nil local packetMap = {} @@ -6,10 +6,10 @@ local packetMap = {} local clientPacketIDs = {} function clientPacketIDs.start() - packetStorage = wallyInstanceManager.get(script.Parent.Parent.Parent, "packetStorage") :: Folder + packetStorage = ReplicatedStorage:FindFirstChild("ByteNetPacketStorage") if not packetStorage then - packetStorage = wallyInstanceManager.waitForInstance(script.Parent.Parent.Parent, "packetStorage", 3) :: Folder + packetStorage = ReplicatedStorage:WaitForChild("ByteNetPacketStorage") end end diff --git a/src/packets/getUnique.luau b/src/packets/getUnique.luau deleted file mode 100644 index 71528fb..0000000 --- a/src/packets/getUnique.luau +++ /dev/null @@ -1,9 +0,0 @@ -return function(packetStructure: { [string]: any }) - local unique = "" - - for key in packetStructure do - unique ..= key - end - - return unique -end diff --git a/src/packets/packet.luau b/src/packets/packet.luau index ff283f9..513884f 100644 --- a/src/packets/packet.luau +++ b/src/packets/packet.luau @@ -1,12 +1,23 @@ +--!native +--!optimize 2 +local Players = game:GetService("Players") local RunService = game:GetService("RunService") -local dataTypes = require(script.Parent.Parent.process.dataTypes) local types = require(script.Parent.Parent.types) -local clientPacketIDs = require(script.Parent.Parent.packets.identifiers.clientPacketIDs) -local serverPacketIDs = require(script.Parent.Parent.packets.identifiers.serverPacketIDs) +local clientPacketIDs = require(script.Parent.Parent.packets.clientPacketIDs) +local serverPacketIDs = require(script.Parent.Parent.packets.serverPacketIDs) local client = require(script.Parent.Parent.process.client) local server = require(script.Parent.Parent.process.server) -local getUnique = require(script.Parent.getUnique) + +local function getUnique(structure: { [string]: any }) + local unique = "" + + for key in structure do + unique ..= key + end + + return unique +end local packetPrototype = {} local packetMetatable = { __index = packetPrototype } @@ -14,17 +25,51 @@ export type packetType = typeof(setmetatable( {} :: { id: number, reliabilityType: string, - listeners: { [number]: (data: {}, player: Player) -> () }, - packetFormat: types.packetFormat, }, packetMetatable )) -function packetPrototype.send(self: packetType, data: {}) +function packetPrototype.sendTo(self: packetType, data: {}, player: Player) + if RunService:IsServer() then + if self.reliabilityType == "reliable" then + server.sendPlayerReliable(player, self.id, self.packetFormat, data) + else + server.sendPlayerUnreliable(player, self.id, self.packetFormat, data) + end + end +end + +function packetPrototype.sendToAllExcept(self: packetType, data: {}, except: Player) if RunService:IsServer() then + for _, player in Players:GetPlayers() do + if player ~= except then + if self.reliabilityType == "reliable" then + server.sendPlayerReliable(player, self.id, self.packetFormat, data) + else + server.sendPlayerUnreliable(player, self.id, self.packetFormat, data) + end + end + end + end +end + +function packetPrototype.sendToAll(self: packetType, data: {}) + if self.reliabilityType == "reliable" then server.sendAllReliable(self.id, self.packetFormat, data) + else + server.sendAllUnreliable(self.id, self.packetFormat, data) + end +end + +function packetPrototype.send(self: packetType, data: {}) + if RunService:IsServer() then + if self.reliabilityType == "reliable" then + server.sendAllReliable(self.id, self.packetFormat, data) + else + server.sendAllUnreliable(self.id, self.packetFormat, data) + end else client.sendReliable(self.id, self.packetFormat, data) end @@ -34,32 +79,43 @@ function packetPrototype.listen(self: packetType, callback) table.insert(self.listeners, callback) end -return function(packetStructure: { [string]: any }, reliabilityType: "reliable" | "unreliable"): packetType +type packetProps = { + structure: { [string]: types.dataTypeInterface }, + reliabilityType: "reliable" | "unreliable", + callbackBehavior: { + spawnThread: boolean, + allowMultiple: boolean, + }, +} +return function(props: packetProps): packetType local self = setmetatable({}, packetMetatable) - -- Basic properties - self.reliabilityType = reliabilityType or "reliable" - self._unique = getUnique(packetStructure) + -- Basic properties: reliability type, "unique" which is used to get the packet ID, and set up listeners + self.reliabilityType = props.reliabilityType or "reliable" + self._unique = getUnique(props.structure) self.id = if RunService:IsServer() then serverPacketIDs.assignPacket(self._unique, self) else clientPacketIDs.assignPacket(self._unique, self) self.listeners = {} - -- Format self.packetFormat = {} - for key, dataType in packetStructure do - local writer, reader, length = - dataTypes.writers[dataType], dataTypes.readers[dataType], dataTypes.lengths[dataType] - - table.insert(self.packetFormat, { - reader = reader, - writer = writer, - length = length, - key = key, - }) + for key, dataType in props.structure do + -- Fetch all relevant data from the structure (read, write, key), and then insert it into the packet format + local value: { [number]: types.packetFormatElement } = { + dataType.read, + dataType.write, + key, + } + + table.insert(self.packetFormat, value) end + + -- Sort the format so that way it's in a static order shared across both client and server table.sort(self.packetFormat, function(a, b) - return a.key < b.key + local keyA = a[3] + local keyB = b[3] + + return keyA > keyB end) return self diff --git a/src/packets/retrievePacketFromID.luau b/src/packets/retrievePacketFromID.luau deleted file mode 100644 index 45a91f3..0000000 --- a/src/packets/retrievePacketFromID.luau +++ /dev/null @@ -1,12 +0,0 @@ -local RunService = game:GetService("RunService") - -local clientPacketIDs = require(script.Parent.Parent.packets.identifiers.clientPacketIDs) -local serverPacketIDs = require(script.Parent.Parent.packets.identifiers.serverPacketIDs) - -local isServer = RunService:IsServer() - -if isServer then - return serverPacketIDs.getPacketFromID -else - return clientPacketIDs.getPacketFromID -end diff --git a/src/packets/identifiers/serverPacketIDs.luau b/src/packets/serverPacketIDs.luau similarity index 63% rename from src/packets/identifiers/serverPacketIDs.luau rename to src/packets/serverPacketIDs.luau index 14f300e..66feccd 100644 --- a/src/packets/identifiers/serverPacketIDs.luau +++ b/src/packets/serverPacketIDs.luau @@ -1,14 +1,15 @@ -local wallyInstanceManager = require(script.Parent.Parent.Parent.Parent.wallyInstanceManager) +local ReplicatedStorage = game:GetService("ReplicatedStorage") local packetMap = {} -local packetStorage = Instance.new("Folder") +local packetStorage = nil local idCounter = 0 local serverPacketIDs = {} function serverPacketIDs.start() - packetStorage.Name = "packetStorage" - wallyInstanceManager.add(script.Parent.Parent.Parent, packetStorage) + packetStorage = Instance.new("Folder") + packetStorage.Name = "ByteNetPacketStorage" + packetStorage.Parent = ReplicatedStorage end function serverPacketIDs.assignPacket(packetContents: string, packet) diff --git a/src/process/bufferWriter.luau b/src/process/bufferWriter.luau new file mode 100644 index 0000000..53eaa8a --- /dev/null +++ b/src/process/bufferWriter.luau @@ -0,0 +1,110 @@ +--!native +--!optimize 2 + +--[[ + Collects all write operations into a queue. + When the buffer is written, it will be written in the order of the queue. + + All operations take in 2 parameters: the cursor and the value. + Knowing this we can easily attach everything in a nice table, where + { + [1] = writer, + [2] = cursor, + [3] = value + } + + A lot of these functions just exist as shorthand optimizations. +]] +local types = require(script.Parent.Parent.types) + +local current: types.bufferQueue + +local bufferWriter = {} + +function bufferWriter.u8(cursor: number, value: number) + table.insert(current, buffer.writeu8) + table.insert(current, cursor) + table.insert(current, value) + return 1 +end + +function bufferWriter.i8(cursor: number, value: number) + table.insert(current, buffer.writei8) + table.insert(current, cursor) + table.insert(current, value) + return 1 +end + +function bufferWriter.u16(cursor: number, value: number) + table.insert(current, buffer.writeu16) + table.insert(current, cursor) + table.insert(current, value) + return 2 +end + +function bufferWriter.i16(cursor: number, value: number) + table.insert(current, buffer.writei8) + table.insert(current, cursor) + table.insert(current, value) + return 2 +end + +function bufferWriter.u32(cursor: number, value: number) + table.insert(current, buffer.writeu32) + table.insert(current, cursor) + table.insert(current, value) + return 4 +end + +function bufferWriter.writestring(cursor: number, value: string) + table.insert(current, buffer.writestring) + table.insert(current, cursor) + table.insert(current, value) +end + +function bufferWriter.i32(cursor: number, value: number) + table.insert(current, buffer.writei8) + table.insert(current, cursor) + table.insert(current, value) + return 4 +end + +function bufferWriter.f32(cursor: number, value: number) + table.insert(current, buffer.writef32) + table.insert(current, cursor) + table.insert(current, value) + return 4 +end + +function bufferWriter.f64(cursor: number, value: number) + table.insert(current, buffer.writef64) + table.insert(current, cursor) + table.insert(current, value) + return 8 +end + +function bufferWriter.copy(targetOffset, source) + table.insert(current, buffer.copy) + table.insert(current, targetOffset) + table.insert(current, source) +end + +function bufferWriter.btrue(cursor: number) + table.insert(current, buffer.writeu8) + table.insert(current, cursor) + table.insert(current, 1) + return 1 +end + +function bufferWriter.bfalse(cursor: number) + table.insert(current, buffer.writeu8) + table.insert(current, cursor) + table.insert(current, 0) + return 1 +end + +function bufferWriter.set(collector) + current = collector +end + +return bufferWriter diff --git a/src/process/client.luau b/src/process/client.luau index 0272e2b..0604a6f 100644 --- a/src/process/client.luau +++ b/src/process/client.luau @@ -1,34 +1,89 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") local RunService = game:GetService("RunService") -local wallyInstanceManager = require(script.Parent.Parent.Parent.wallyInstanceManager) local types = require(script.Parent.Parent.types) local read = require(script.Parent.read) -local sendChannel = require(script.Parent.sendChannel) +local bufferWriter = require(script.Parent.bufferWriter) local function onClientEvent(receivedBuffer) read(receivedBuffer) end -local reliableChannel = sendChannel() -local unreliableChannel = sendChannel() + +type channelData = { + cursor: number, + jobs: types.bufferQueue, +} + +local cursor = 0 +local jobs = {} + +local function load(freshChannel: channelData?) + if not freshChannel then + cursor = 0 + jobs = {} + bufferWriter.set(jobs) + return + end + + cursor = freshChannel.cursor + jobs = freshChannel.jobs + bufferWriter.set(jobs) +end + +local function save(): channelData + return { + cursor = cursor, + jobs = jobs, + } +end + +local function addPacketToLoadedChannel(id: number, format: types.packetFormat, data: { [string]: any }) + bufferWriter.u8(cursor, id) + cursor += 1 + + for _, value in format do + cursor += (value[2] :: (cursor: number, value: any) -> number)(cursor, data[value[3] :: string]) + end +end + +local function dumpLoadedChannel(): buffer + local dumpBuffer = buffer.create(cursor) + + for index = 1, #jobs, 3 do + (jobs[index] :: (b: buffer, cursor: number, value: any) -> ())( + dumpBuffer, + jobs[index + 1] :: number, + jobs[index + 2] + ) + end + + return dumpBuffer +end + +local reliable: channelData? = nil +local unreliable: channelData? = nil local clientProcess = {} function clientProcess.sendReliable(id: number, format: types.packetFormat, data: { [string]: any }) - reliableChannel:add(id, format, data) + load(reliable) + addPacketToLoadedChannel(id, format, data) + reliable = save() end function clientProcess.sendUnreliable(id: number, format: types.packetFormat, data: { [string]: any }) - unreliableChannel:add(id, format, data) + load(unreliable) + addPacketToLoadedChannel(id, format, data) + unreliable = save() end function clientProcess.start() - local byteNetInstance = script.Parent.Parent.Parent local remoteInstances: { reliable: RemoteEvent?, unreliable: UnreliableRemoteEvent?, } = { - reliable = wallyInstanceManager.waitForInstance(byteNetInstance, "reliable", 3) :: RemoteEvent, - unreliable = wallyInstanceManager.waitForInstance(byteNetInstance, "unreliable", 3) :: UnreliableRemoteEvent, + reliable = ReplicatedStorage:WaitForChild("ByteNetReliable"), + unreliable = ReplicatedStorage:WaitForChild("ByteNetUnreliable"), } if not remoteInstances.reliable or not remoteInstances.unreliable then @@ -42,14 +97,18 @@ function clientProcess.start() unreliableRemote.OnClientEvent:Connect(onClientEvent) RunService.Heartbeat:Connect(function() - local reliableBuffer = reliableChannel:empty() - if reliableBuffer then - reliableRemote:FireServer(reliableBuffer) + if reliable ~= nil then + load(reliable) + reliableRemote:FireServer(dumpLoadedChannel()) + + reliable = nil end - local unreliableBuffer = unreliableChannel:empty() - if unreliableBuffer then - unreliableRemote:FireServer(unreliableBuffer) + if unreliable ~= nil then + load(unreliable) + unreliableRemote:FireServer(dumpLoadedChannel()) + + unreliable = nil end end) end diff --git a/src/process/dataTypes.luau b/src/process/dataTypes.luau deleted file mode 100644 index 842e27b..0000000 --- a/src/process/dataTypes.luau +++ /dev/null @@ -1,372 +0,0 @@ -local readers = { - uint8 = buffer.readu8, - uint16 = buffer.readu16, - uint32 = buffer.readu32, - - int8 = buffer.readi8, - int16 = buffer.readi16, - int32 = buffer.readi32, - - float32 = buffer.readf32, - float64 = buffer.readf64, - - opt_uint8 = function(b: buffer, cursor: number): number? - local exists = buffer.readu8(b, cursor) - if exists then - return buffer.readu8(b, cursor + 1) - else - return nil - end - end, - opt_uint16 = function(b: buffer, cursor: number): number? - local exists = buffer.readu8(b, cursor) - if exists then - return buffer.readu16(b, cursor + 1) - else - return nil - end - end, - opt_uint32 = function(b: buffer, cursor: number): number? - local exists = buffer.readu8(b, cursor) - if exists then - return buffer.readu32(b, cursor + 1) - else - return nil - end - end, - - opt_int8 = function(b: buffer, cursor: number): number? - local exists = buffer.readu8(b, cursor) - if exists then - return buffer.readi8(b, cursor + 1) - else - return nil - end - end, - opt_int16 = function(b: buffer, cursor: number): number? - local exists = buffer.readu8(b, cursor) - if exists then - return buffer.readi16(b, cursor + 1) - else - return nil - end - end, - opt_int32 = function(b: buffer, cursor: number): number? - local exists = buffer.readu8(b, cursor) - if exists then - return buffer.readi32(b, cursor + 1) - else - return nil - end - end, - - opt_float32 = function(b: buffer, cursor: number): number? - local exists = buffer.readu8(b, cursor) - if exists then - return buffer.readf32(b, cursor + 1) - else - return nil - end - end, - opt_float64 = function(b: buffer, cursor: number): number? - local exists = buffer.readu8(b, cursor) - if exists then - return buffer.readf64(b, cursor + 1) - else - return nil - end - end, - - bool = function(b: buffer, cursor: number): boolean - return buffer.readu8(b, cursor) == 1 - end, - opt_bool = function(b: buffer, cursor: number): boolean? - local exists = buffer.readu8(b, cursor) - if exists then - return buffer.readu8(b, cursor + 1) == 1 - else - return nil - end - end, - - buff = function(b: buffer, cursor: number): buffer - local len = buffer.readu32(b, cursor) - local newBuff = buffer.create(len) - - buffer.copy(newBuff, 0, b, cursor + 4, len) - - return newBuff - end, - opt_buff = function(b: buffer, cursor: number): buffer? - local exists = buffer.readu8(b, cursor) - if exists then - local len = buffer.readu32(b, cursor + 1) - local newBuff = buffer.create(len) - - buffer.copy(newBuff, 0, b, cursor + 5, len) - - return newBuff - else - return nil - end - end, - - str = function(b: buffer, cursor: number): string - local len = buffer.readu16(b, cursor) - return buffer.readstring(b, cursor + 2, len) - end, - opt_str = function(b: buffer, cursor: number): string? - local exists = buffer.readu8(b, cursor) - if exists then - local len = buffer.readu16(b, cursor + 1) - return buffer.readstring(b, cursor + 3, len) - else - return nil - end - end, - - vec3 = function(b: buffer, cursor: number): Vector3 - local x = buffer.readf32(b, cursor) - local y = buffer.readf32(b, cursor + 4) - local z = buffer.readf32(b, cursor + 8) - - return Vector3.new(x, y, z) - end, - opt_vec3 = function(b: buffer, cursor: number): Vector3? - local exists = buffer.readu8(b, cursor) - if exists then - local x = buffer.readf32(b, cursor + 1) - local y = buffer.readf32(b, cursor + 5) - local z = buffer.readf32(b, cursor + 9) - - return Vector3.new(x, y, z) - else - return nil - end - end, -} -local writers = { - uint8 = buffer.writeu8, - uint16 = buffer.writeu16, - uint32 = buffer.writeu32, - - int8 = buffer.writei8, - int16 = buffer.writei16, - int32 = buffer.writei32, - - float32 = buffer.writef32, - float64 = buffer.writef64, - - opt_uint8 = function(b: buffer, cursor: number, value: number?) - if value then - buffer.writeu8(b, cursor, 1) - buffer.writeu8(b, cursor + 1, value) - else - buffer.writeu8(b, cursor, 0) - end - end, - opt_uint16 = function(b: buffer, cursor: number, value: number?) - if value then - buffer.writeu8(b, cursor, 1) - buffer.writeu16(b, cursor + 1, value) - else - buffer.writeu8(b, cursor, 0) - end - end, - opt_uint32 = function(b: buffer, cursor: number, value: number?) - if value then - buffer.writeu8(b, cursor, 1) - buffer.writeu32(b, cursor + 1, value) - else - buffer.writeu8(b, cursor, 0) - end - end, - - opt_int8 = function(b: buffer, cursor: number, value: number?) - if value then - buffer.writeu8(b, cursor, 1) - buffer.writei8(b, cursor + 1, value) - else - buffer.writeu8(b, cursor, 0) - end - end, - opt_int16 = function(b: buffer, cursor: number, value: number?) - if value then - buffer.writeu8(b, cursor, 1) - buffer.writei16(b, cursor + 1, value) - else - buffer.writeu8(b, cursor, 0) - end - end, - opt_int32 = function(b: buffer, cursor: number, value: number?) - if value then - buffer.writeu8(b, cursor, 1) - buffer.writei32(b, cursor + 1, value) - else - buffer.writeu8(b, cursor, 0) - end - end, - - opt_float32 = function(b: buffer, cursor: number, value: number?) - if value then - buffer.writeu8(b, cursor, 1) - buffer.writef32(b, cursor + 1, value) - else - buffer.writeu8(b, cursor, 0) - end - end, - opt_float64 = function(b: buffer, cursor: number, value: number?) - if value then - buffer.writeu8(b, cursor, 1) - buffer.writef64(b, cursor + 1, value) - else - buffer.writeu8(b, cursor, 0) - end - end, - - bool = function(b: buffer, cursor: number, value: boolean) - buffer.writeu8(b, cursor, if value then 1 else 0) - end, - opt_bool = function(b: buffer, cursor: number, value: boolean?) - if value then - buffer.writeu8(b, cursor, 1) - buffer.writeu8(b, cursor + 1, if value then 1 else 0) - else - buffer.writeu8(b, cursor, 0) - end - end, - - buff = function(b: buffer, cursor: number, value: buffer) - buffer.writeu32(b, cursor, buffer.len(value)) - buffer.copy(b, cursor + 4, value) - end, - opt_buff = function(b: buffer, cursor: number, value: buffer?) - if value then - buffer.writeu8(b, cursor, 1) - buffer.writeu32(b, cursor + 1, buffer.len(value)) - buffer.copy(b, cursor + 5, value) - else - buffer.writeu8(b, cursor, 0) - end - end, - - str = function(b: buffer, cursor: number, value: string) - buffer.writeu16(b, cursor, #value) - buffer.writestring(b, cursor + 2, value) - end, - opt_str = function(b: buffer, cursor: number, value: string?) - if value then - buffer.writeu8(b, cursor, 1) - buffer.writeu16(b, cursor + 1, #value) - buffer.writestring(b, cursor + 3, value) - else - buffer.writeu8(b, cursor, 0) - end - end, - - vec3 = function(b: buffer, cursor: number, value: Vector3) - buffer.writef32(b, cursor, value.X) - buffer.writef32(b, cursor + 4, value.Y) - buffer.writef32(b, cursor + 8, value.Z) - end, - opt_vec3 = function(b: buffer, cursor: number, value: Vector3?) - if value then - buffer.writeu8(b, cursor, 1) - buffer.writef32(b, cursor + 1, value.X) - buffer.writef32(b, cursor + 5, value.Y) - buffer.writef32(b, cursor + 9, value.Z) - else - buffer.writeu8(b, cursor, 0) - end - end, -} - -local lengths = { - uint8 = function() - return 1 - end, - uint16 = function() - return 2 - end, - uint32 = function() - return 4 - end, - - int8 = function() - return 1 - end, - int16 = function() - return 2 - end, - int32 = function() - return 4 - end, - - float32 = function() - return 4 - end, - float64 = function() - return 8 - end, - - opt_uint8 = function(num: number?): number - return if num then 2 else 1 - end, - opt_uint16 = function(num: number?): number - return if num then 3 else 1 - end, - opt_uint32 = function(num: number?): number - return if num then 5 else 1 - end, - - opt_int8 = function(num: number?): number - return if num then 2 else 1 - end, - opt_int16 = function(num: number?): number - return if num then 3 else 1 - end, - opt_int32 = function(num: number?): number - return if num then 5 else 1 - end, - - opt_float32 = function(num: number?): number - return if num then 5 else 1 - end, - opt_float64 = function(num: number?): number - return if num then 9 else 1 - end, - - bool = function() - return 1 - end, - opt_bool = function(bool: boolean?): number - return if bool then 2 else 1 - end, - - buff = function(buff: buffer): number - return buffer.len(buff) + 4 - end, - opt_buff = function(buff: buffer?): number - return if buff then buffer.len(buff) + 5 else 1 - end, - - str = function(str: string): number - return #str + 2 - end, - opt_str = function(str: string?): number - return if str then #str + 3 else 1 - end, - - vec3 = function() - return 12 - end, - opt_vec3 = function(vec: Vector3?): number - return if vec then 13 else 1 - end, -} - -return { - readers = readers, - writers = writers, - lengths = lengths, -} diff --git a/src/process/read.luau b/src/process/read.luau index 0060a2e..1c36106 100644 --- a/src/process/read.luau +++ b/src/process/read.luau @@ -1,24 +1,39 @@ +local RunService = game:GetService("RunService") + +local clientPacketIDs = require(script.Parent.Parent.packets.clientPacketIDs) +local serverPacketIDs = require(script.Parent.Parent.packets.serverPacketIDs) local types = require(script.Parent.Parent.types) -local retrievePacketFromID = require(script.Parent.Parent.packets.retrievePacketFromID) + +-- Shorthand to avoid having to check if we're on the server or client every time +local retrievePacketFromID = ( + if RunService:IsServer() then serverPacketIDs.getPacketFromID else clientPacketIDs.getPacketFromID +) :: (packetID: number) -> any return function(incomingBuffer: buffer) local length = buffer.len(incomingBuffer) local readCursor = 0 while readCursor < length do + -- Read packet ID local id = buffer.readu8(incomingBuffer, readCursor) readCursor += 1 + -- Get packet class, then get packet format local packet = retrievePacketFromID(id) - local format = packet.packetFormat :: types.packetFormat local deserialized = {} for _, item in format do - local value = item.reader(incomingBuffer, readCursor) - readCursor += item.length(value) + -- item[1] is the reader function for the value type + -- it'll return the value and the length of the value + local value, itemLength = ((item[1] :: any) :: (b: buffer, cursor: number) -> (any, number))( + incomingBuffer, + readCursor + ) + readCursor += itemLength - deserialized[item.key] = value + -- item[3] is the key, so we're converting it into something usable here + deserialized[item[3]] = value end for _, listener in packet.listeners do diff --git a/src/process/sendChannel.luau b/src/process/sendChannel.luau deleted file mode 100644 index 1bdf354..0000000 --- a/src/process/sendChannel.luau +++ /dev/null @@ -1,59 +0,0 @@ -local types = require(script.Parent.Parent.types) - -local sendChannelPrototype = {} -local sendChannelMetatable = { __index = sendChannelPrototype } -export type sendChannelType = typeof(setmetatable( - {} :: { - outgoingBuffer: buffer, - cursor: number, - writeJobs: { () -> () }, - }, - sendChannelMetatable -)) - -function sendChannelPrototype.add( - self: sendChannelType, - id: number, - format: types.packetFormat, - data: { [string]: any } -) - local idPosition = self.cursor - table.insert(self.writeJobs, function() - buffer.writeu8(self.outgoingBuffer, idPosition, id) - end) - self.cursor += 1 - - for _, value in format do - local currentCursor = self.cursor - table.insert(self.writeJobs, function() - print(value) - value.writer(self.outgoingBuffer, currentCursor, data[value.key]) - end) - self.cursor += value.length() - end -end - -function sendChannelPrototype.empty(self: sendChannelType): buffer? - if self.cursor == 0 then - return nil - end - - self.outgoingBuffer = buffer.create(self.cursor) - for _, writer in self.writeJobs do - writer() - end - table.clear(self.writeJobs) - self.cursor = 0 - - return self.outgoingBuffer -end - -return function(): sendChannelType - local self = setmetatable({}, sendChannelMetatable) - - self.outgoingBuffer = buffer.create(0) - self.writeJobs = {} - self.cursor = 0 - - return self -end diff --git a/src/process/server.luau b/src/process/server.luau index 6050fdd..a13cd94 100644 --- a/src/process/server.luau +++ b/src/process/server.luau @@ -1,17 +1,46 @@ +--!native +--!optimize 2 local Players = game:GetService("Players") +local ReplicatedStorage = game:GetService("ReplicatedStorage") local RunService = game:GetService("RunService") -local wallyInstanceManager = require(script.Parent.Parent.Parent.wallyInstanceManager) local types = require(script.Parent.Parent.types) local read = require(script.Parent.read) -local sendChannel = require(script.Parent.sendChannel) -local channels = { - players = {} :: { [Player]: { reliable: sendChannel.sendChannelType, unreliable: sendChannel.sendChannelType } }, +local bufferWriter = require(script.Parent.bufferWriter) - reliable = sendChannel(), - unreliable = sendChannel(), +local globalReliable: channelData? = nil +local globalUnreliable: channelData? = nil +local perPlayerReliable: { [Player]: channelData? } = {} +local perPlayerUnreliable: { [Player]: channelData? } = {} + +type channelData = { + cursor: number, + jobs: types.bufferQueue, } +local cursor = 0 +local jobs = {} + +local function load(freshChannel: channelData?) + if not freshChannel then + cursor = 0 + jobs = {} + bufferWriter.set(jobs) + return + end + + cursor = freshChannel.cursor + jobs = freshChannel.jobs + bufferWriter.set(jobs) +end + +local function save(): channelData + return { + cursor = cursor, + jobs = jobs, + } +end + local function onServerEvent(player: Player, data) if not (typeof(data) == "buffer") then return @@ -25,21 +54,46 @@ local function onServerEvent(player: Player, data) read(data) end +local function addPacketToLoadedChannel(id: number, format: types.packetFormat, data: { [string]: any }) + bufferWriter.u8(cursor, id) + cursor += 1 + + for _, value in format do + cursor += (value[2] :: (cursor: number, value: any) -> number)(cursor, data[value[3] :: string]) + end +end + +local function dumpLoadedChannel(): buffer + local dumpBuffer = buffer.create(cursor) + + for index = 1, #jobs, 3 do + (jobs[index] :: (b: buffer, cursor: number, value: any) -> ())( + dumpBuffer, + jobs[index + 1] :: number, + jobs[index + 2] + ) + end + + return dumpBuffer +end + local function onPlayerAdded(player: Player) - channels.players[player] = { - reliable = sendChannel(), - unreliable = sendChannel(), - } + perPlayerReliable[player] = nil + perPlayerUnreliable[player] = nil end local serverProcess = {} function serverProcess.sendAllReliable(id: number, format: types.packetFormat, data: { [string]: any }) - channels.reliable:add(id, format, data) + load(globalReliable) + addPacketToLoadedChannel(id, format, data) + globalReliable = save() end function serverProcess.sendAllUnreliable(id: number, format: types.packetFormat, data: { [string]: any }) - channels.unreliable:add(id, format, data) + load(globalUnreliable) + addPacketToLoadedChannel(id, format, data) + globalUnreliable = save() end function serverProcess.sendPlayerReliable( @@ -48,7 +102,9 @@ function serverProcess.sendPlayerReliable( format: types.packetFormat, data: { [string]: any } ) - channels.players[player].reliable:add(id, format, data) + load(perPlayerReliable[player]) + addPacketToLoadedChannel(id, format, data) + perPlayerReliable[player] = save() end function serverProcess.sendPlayerUnreliable( @@ -57,53 +113,57 @@ function serverProcess.sendPlayerUnreliable( format: types.packetFormat, data: { [string]: any } ) - channels.players[player].unreliable:add(id, format, data) + load(perPlayerUnreliable[player]) + addPacketToLoadedChannel(id, format, data) + perPlayerUnreliable[player] = save() end function serverProcess.start() - local remoteInstances = { - reliable = Instance.new("RemoteEvent"), - unreliable = Instance.new("UnreliableRemoteEvent"), - } + local reliableRemote = Instance.new("RemoteEvent") + local unreliableRemote = Instance.new("UnreliableRemoteEvent") - remoteInstances.reliable.Name = "reliable" - remoteInstances.unreliable.Name = "unreliable" + reliableRemote.Name = "ByteNetReliable" + unreliableRemote.Name = "ByteNetUnreliable" - local byteNetInstance = script.Parent.Parent.Parent - wallyInstanceManager.add(byteNetInstance, remoteInstances.reliable) - wallyInstanceManager.add(byteNetInstance, remoteInstances.unreliable) + reliableRemote.Parent = ReplicatedStorage + unreliableRemote.Parent = ReplicatedStorage for _, player in Players:GetPlayers() do onPlayerAdded(player) end Players.PlayerAdded:Connect(onPlayerAdded) - Players.PlayerRemoving:Connect(function(player: Player) end) + Players.PlayerRemoving:Connect(function() end) - remoteInstances.reliable.OnServerEvent:Connect(onServerEvent) - remoteInstances.unreliable.OnServerEvent:Connect(onServerEvent) + reliableRemote.OnServerEvent:Connect(onServerEvent) + unreliableRemote.OnServerEvent:Connect(onServerEvent) RunService.Heartbeat:Connect(function() - local reliableBuffer = channels.reliable:empty() - if reliableBuffer ~= nil then - remoteInstances.reliable:FireAllClients(reliableBuffer) - end + if globalReliable ~= nil then + load(globalReliable) + reliableRemote:FireAllClients(dumpLoadedChannel()) - local unreliableBuffer = channels.unreliable:empty() - if unreliableBuffer ~= nil then - remoteInstances.unreliable:FireAllClients(unreliableBuffer) + globalReliable = nil end - for player, playerChannels in channels.players do - local reliablePlayerBuffer = playerChannels.reliable:empty() - local unreliablePlayerBuffer = playerChannels.unreliable:empty() + if globalUnreliable ~= nil then + load(globalUnreliable) + unreliableRemote:FireAllClients(dumpLoadedChannel()) + + globalUnreliable = nil + end - if reliablePlayerBuffer ~= nil then - remoteInstances.reliable:FireClient(player, reliablePlayerBuffer) + for _, player in Players:GetPlayers() do + if perPlayerReliable[player] ~= nil then + load(perPlayerReliable[player]) + reliableRemote:FireClient(player, dumpLoadedChannel()) + perPlayerReliable[player] = nil end - if unreliablePlayerBuffer ~= nil then - remoteInstances.unreliable:FireClient(player, unreliablePlayerBuffer) + if perPlayerUnreliable[player] ~= nil then + load(perPlayerUnreliable[player]) + unreliableRemote:FireClient(player, dumpLoadedChannel()) + perPlayerUnreliable[player] = nil end end end) diff --git a/src/types.luau b/src/types.luau index 2280ca4..eccfe4c 100644 --- a/src/types.luau +++ b/src/types.luau @@ -1,11 +1,20 @@ export type packetFormat = { { - writer: (b: buffer, offset: number, value: any) -> (), - reader: (b: buffer, offset: number) -> any, - key: string, - length: (value: any) -> number, + -- read | write | key + [number]: ((b: buffer, offset: number) -> (any, number)) | ((offset: number, value: any) -> number) | string, } } +export type packetFormatElement = + ((b: buffer, offset: number) -> (any, number)) + | ((offset: number, value: any) -> number) + | string + +export type bufferQueue = { ((b: buffer, c: number, v: any) -> ()) | number | string | buffer } + +export type dataTypeInterface = { + write: (cursor: number, value: T) -> number, + read: (b: buffer, cursor: number) -> (T, number), +} type Packet = { sendToAll: (self: Packet, data: T) -> (), @@ -14,33 +23,29 @@ type Packet = { } export type ByteNet = { - definePacket: (structure: T, reliabilityType: ("reliable" | "unreliable")?) -> Packet, + definePacket: (props: { + structure: T, + reliabilityType: ("reliable" | "unreliable")?, + }) -> Packet, dataTypes: { - opt_uint8: () -> number?, - opt_int8: () -> number?, + bool: () -> boolean, + array: (value: T) -> { [number]: T }, + optional: (value: T) -> T?, uint8: () -> number, - int8: () -> number, - - opt_uint16: () -> number?, - opt_int16: () -> number?, uint16: () -> number, - int16: () -> number, - - opt_uint32: () -> number?, - opt_int32: () -> number?, uint32: () -> number, + int8: () -> number, + int16: () -> number, int32: () -> number, - - opt_float32: () -> number?, - opt_float64: () -> number?, float32: () -> number, float64: () -> number, - - vec3: () -> Vector3, string: () -> string, - bool: () -> boolean, + vec3: () -> Vector3, + vec2: () -> Vector2, buff: () -> buffer, + cframe: () -> CFrame, + map: (key: K, value: V) -> { [K]: V }, }, } diff --git a/wally.lock b/wally.lock index 76063b0..5e6fc88 100644 --- a/wally.lock +++ b/wally.lock @@ -4,8 +4,13 @@ registry = "test" [[package]] name = "ffrostflame/bytenet" -version = "0.2.6" -dependencies = [["wallyInstanceManager", "ffrostflame/wally-instance-manager@0.1.0"]] +version = "0.3.0-dev" +dependencies = [["TableKit", "ffrostflame/tablekit@0.2.4"], ["wallyInstanceManager", "ffrostflame/wally-instance-manager@0.1.0"]] + +[[package]] +name = "ffrostflame/tablekit" +version = "0.2.4" +dependencies = [] [[package]] name = "ffrostflame/wally-instance-manager" diff --git a/wally.toml b/wally.toml index fb7c61f..20785f5 100644 --- a/wally.toml +++ b/wally.toml @@ -1,8 +1,7 @@ [package] name = "ffrostflame/bytenet" -version = "0.3.0" +version = "0.3.0-dev2" registry = "https://github.com/UpliftGames/wally-index" realm = "shared" [dependencies] -wallyInstanceManager = "ffrostflame/wally-instance-manager@0.1.0" \ No newline at end of file From 933bdc94f4595bae633f1ada5023d589e0d8a215 Mon Sep 17 00:00:00 2001 From: ffrostfall <80861876+ffrostflame@users.noreply.github.com> Date: Tue, 13 Feb 2024 16:51:49 -0500 Subject: [PATCH 5/9] Add comments --- src/process/client.luau | 2 ++ src/process/server.luau | 46 +++++++++++++++++++++++------------------ src/types.luau | 5 +++++ wally.toml | 2 +- 4 files changed, 34 insertions(+), 21 deletions(-) diff --git a/src/process/client.luau b/src/process/client.luau index 0604a6f..3221b49 100644 --- a/src/process/client.luau +++ b/src/process/client.luau @@ -9,6 +9,7 @@ local function onClientEvent(receivedBuffer) read(receivedBuffer) end +-- Shared with: src/process/server.luau (Infeasible to split this into another file) type channelData = { cursor: number, jobs: types.bufferQueue, @@ -59,6 +60,7 @@ local function dumpLoadedChannel(): buffer return dumpBuffer end +-- No longer shared local reliable: channelData? = nil local unreliable: channelData? = nil diff --git a/src/process/server.luau b/src/process/server.luau index a13cd94..984cc43 100644 --- a/src/process/server.luau +++ b/src/process/server.luau @@ -8,23 +8,26 @@ local types = require(script.Parent.Parent.types) local read = require(script.Parent.read) local bufferWriter = require(script.Parent.bufferWriter) +-- All channelData is set to nil upon being sent which is why these are all optionals local globalReliable: channelData? = nil local globalUnreliable: channelData? = nil local perPlayerReliable: { [Player]: channelData? } = {} local perPlayerUnreliable: { [Player]: channelData? } = {} +-- Shared with: src/process/client.luau (Infeasible to split this into another file) type channelData = { cursor: number, jobs: types.bufferQueue, } +-- Cursor is copied, jobs is not. local cursor = 0 local jobs = {} local function load(freshChannel: channelData?) if not freshChannel then cursor = 0 - jobs = {} + jobs = {} -- It's important to note this creates a new channel and creates a new reference bufferWriter.set(jobs) return end @@ -37,15 +40,18 @@ end local function save(): channelData return { cursor = cursor, - jobs = jobs, + jobs = jobs, -- This saves the current reference to the jobs table, letting us switch it out later. Also important. } end +-- TODO handle invalid data better local function onServerEvent(player: Player, data) + -- Only accept buffer data if not (typeof(data) == "buffer") then return end + -- Limit the amount of data if buffer.len(data) >= 100000 then warn("over 100K byte limit from player: " .. player.UserId) return @@ -55,10 +61,12 @@ local function onServerEvent(player: Player, data) end local function addPacketToLoadedChannel(id: number, format: types.packetFormat, data: { [string]: any }) + -- packet ID bufferWriter.u8(cursor, id) cursor += 1 for _, value in format do + -- value[2] is the write function, value[3] is the key cursor += (value[2] :: (cursor: number, value: any) -> number)(cursor, data[value[3] :: string]) end end @@ -66,7 +74,10 @@ end local function dumpLoadedChannel(): buffer local dumpBuffer = buffer.create(cursor) + -- remember, jobs[1] is the function, jobs[2] is the cursor, and jobs[3] is the value + -- iterate over all jobs in increments of 3, and call accordingly for index = 1, #jobs, 3 do + -- We have to do this typecasting because luau doesn't have tuple tables yet (jobs[index] :: (b: buffer, cursor: number, value: any) -> ())( dumpBuffer, jobs[index + 1] :: number, @@ -76,11 +87,7 @@ local function dumpLoadedChannel(): buffer return dumpBuffer end - -local function onPlayerAdded(player: Player) - perPlayerReliable[player] = nil - perPlayerUnreliable[player] = nil -end +-- No longer shared local serverProcess = {} @@ -120,34 +127,29 @@ end function serverProcess.start() local reliableRemote = Instance.new("RemoteEvent") - local unreliableRemote = Instance.new("UnreliableRemoteEvent") - reliableRemote.Name = "ByteNetReliable" - unreliableRemote.Name = "ByteNetUnreliable" - + reliableRemote.OnServerEvent:Connect(onServerEvent) reliableRemote.Parent = ReplicatedStorage - unreliableRemote.Parent = ReplicatedStorage - - for _, player in Players:GetPlayers() do - onPlayerAdded(player) - end - - Players.PlayerAdded:Connect(onPlayerAdded) - Players.PlayerRemoving:Connect(function() end) - reliableRemote.OnServerEvent:Connect(onServerEvent) + local unreliableRemote = Instance.new("UnreliableRemoteEvent") + unreliableRemote.Name = "ByteNetUnreliable" unreliableRemote.OnServerEvent:Connect(onServerEvent) + unreliableRemote.Parent = ReplicatedStorage RunService.Heartbeat:Connect(function() + -- Check if the channel has anything before trying to send it if globalReliable ~= nil then load(globalReliable) + reliableRemote:FireAllClients(dumpLoadedChannel()) + -- Clear channel reference globalReliable = nil end if globalUnreliable ~= nil then load(globalUnreliable) + unreliableRemote:FireAllClients(dumpLoadedChannel()) globalUnreliable = nil @@ -156,13 +158,17 @@ function serverProcess.start() for _, player in Players:GetPlayers() do if perPlayerReliable[player] ~= nil then load(perPlayerReliable[player]) + reliableRemote:FireClient(player, dumpLoadedChannel()) + perPlayerReliable[player] = nil end if perPlayerUnreliable[player] ~= nil then load(perPlayerUnreliable[player]) + unreliableRemote:FireClient(player, dumpLoadedChannel()) + perPlayerUnreliable[player] = nil end end diff --git a/src/types.luau b/src/types.luau index eccfe4c..28ee945 100644 --- a/src/types.luau +++ b/src/types.luau @@ -1,3 +1,4 @@ +-- Used internally to define the format of a packet in an efficient way export type packetFormat = { { -- read | write | key @@ -9,19 +10,23 @@ export type packetFormatElement = | ((offset: number, value: any) -> number) | string +-- Used internally to efficiently handle the "deferred write" with buffers export type bufferQueue = { ((b: buffer, c: number, v: any) -> ()) | number | string | buffer } +-- Used internally for serializing and deserializing all data types export type dataTypeInterface = { write: (cursor: number, value: T) -> number, read: (b: buffer, cursor: number) -> (T, number), } +-- Somewhat public facing: used as return result in definePacket type Packet = { sendToAll: (self: Packet, data: T) -> (), send: (self: Packet, data: T, target: Player?) -> (), listen: (self: Packet, callback: (data: T, player: Player?) -> ()) -> (), } +-- Library type export type ByteNet = { definePacket: (props: { structure: T, diff --git a/wally.toml b/wally.toml index 20785f5..06a9762 100644 --- a/wally.toml +++ b/wally.toml @@ -1,6 +1,6 @@ [package] name = "ffrostflame/bytenet" -version = "0.3.0-dev2" +version = "0.3.0-dev3" registry = "https://github.com/UpliftGames/wally-index" realm = "shared" From 49d66054369d3501b6beaf9632bbc746af0b029b Mon Sep 17 00:00:00 2001 From: ffrostfall <80861876+ffrostflame@users.noreply.github.com> Date: Tue, 13 Feb 2024 16:53:09 -0500 Subject: [PATCH 6/9] Add typescript dependency to package.json --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 124ad83..de35ec5 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "license": "MIT", "module": "commonjs", "devDependencies": { - "@rbxts/types": "^1.0.737" + "@rbxts/types": "^1.0.737", + "typescript": "^5.3.3" } } From dfaded99180bf532e20fe06bd48633fa8f221703 Mon Sep 17 00:00:00 2001 From: ffrostfall <80861876+ffrostflame@users.noreply.github.com> Date: Tue, 13 Feb 2024 16:59:04 -0500 Subject: [PATCH 7/9] remove unnecessary dictionary for remotes --- src/process/client.luau | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/process/client.luau b/src/process/client.luau index 3221b49..2c3d5fb 100644 --- a/src/process/client.luau +++ b/src/process/client.luau @@ -80,34 +80,26 @@ function clientProcess.sendUnreliable(id: number, format: types.packetFormat, da end function clientProcess.start() - local remoteInstances: { - reliable: RemoteEvent?, - unreliable: UnreliableRemoteEvent?, - } = { - reliable = ReplicatedStorage:WaitForChild("ByteNetReliable"), - unreliable = ReplicatedStorage:WaitForChild("ByteNetUnreliable"), - } - - if not remoteInstances.reliable or not remoteInstances.unreliable then - return - end - - local reliableRemote = remoteInstances.reliable - local unreliableRemote = remoteInstances.unreliable - + local reliableRemote = ReplicatedStorage:WaitForChild("ByteNetReliable") reliableRemote.OnClientEvent:Connect(onClientEvent) + + local unreliableRemote = ReplicatedStorage:WaitForChild("ByteNetUnreliable") unreliableRemote.OnClientEvent:Connect(onClientEvent) RunService.Heartbeat:Connect(function() + -- Again, checking if there's anything in the channel before we send it. if reliable ~= nil then load(reliable) + reliableRemote:FireServer(dumpLoadedChannel()) + -- effectively clears the channel reliable = nil end if unreliable ~= nil then load(unreliable) + unreliableRemote:FireServer(dumpLoadedChannel()) unreliable = nil From eafb9c6b086fe88d0e153bd1ab361c708bc1fca3 Mon Sep 17 00:00:00 2001 From: ffrostfall <80861876+ffrostflame@users.noreply.github.com> Date: Wed, 14 Feb 2024 15:08:59 -0500 Subject: [PATCH 8/9] docs --- docs/api/dataTypes/Primitives.md | 0 docs/api/dataTypes/Specials.md | 0 docs/api/functions/definePacket.md | 0 docs/assets/bytenetLogo.png | Bin 0 -> 163199 bytes docs/assets/colors.css | 35 +++++ docs/assets/favicon.png | Bin 0 -> 811 bytes docs/assets/home.css | 70 +++++++++ docs/css/smoothscroll.css | 3 - docs/index.md | 232 +++++++++-------------------- mkdocs.yml | 31 +++- 10 files changed, 196 insertions(+), 175 deletions(-) create mode 100644 docs/api/dataTypes/Primitives.md create mode 100644 docs/api/dataTypes/Specials.md create mode 100644 docs/api/functions/definePacket.md create mode 100644 docs/assets/bytenetLogo.png create mode 100644 docs/assets/colors.css create mode 100644 docs/assets/favicon.png create mode 100644 docs/assets/home.css delete mode 100644 docs/css/smoothscroll.css diff --git a/docs/api/dataTypes/Primitives.md b/docs/api/dataTypes/Primitives.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/api/dataTypes/Specials.md b/docs/api/dataTypes/Specials.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/api/functions/definePacket.md b/docs/api/functions/definePacket.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/assets/bytenetLogo.png b/docs/assets/bytenetLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..cc3e9fa91382a2583ca1f12d8f674383e0464c3e GIT binary patch literal 163199 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�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

t&`9SOpND|VRY-!{OM1G<;jt@{C8|6^pRS9Dw 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

J1a*SuO+Ye` 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 literal 0 HcmV?d00001 diff --git a/docs/assets/colors.css b/docs/assets/colors.css new file mode 100644 index 0000000..bf1157e --- /dev/null +++ b/docs/assets/colors.css @@ -0,0 +1,35 @@ +#nav__link { + --md-primary-fg-color: #000000; +} + +.md-header { + background-color: #001127; +} +.md-tabs { + background-color: #001127; +} +.md-ellipsis { + color: rgb(255, 255, 255); +} + +body { + --md-primary-bg-color: #e6f9ff; +} + +[data-md-color-scheme="slate"] { + --md-code-fg-color: rgb(26, 26, 26); + --md-code-bg-color: rgb(0, 0, 0); + --md-code-hl-color: rgb(255, 255, 255); + --md-code-hl-number-color: hsl(290, 60%, 70%); + --md-code-hl-special-color: rgb(255, 255, 255); + --md-code-hl-function-color: rgb(255, 255, 255); + --md-code-hl-constant-color: rgb(233, 126, 126); + --md-code-hl-keyword-color: hsl(187, 75%, 60%); + --md-code-hl-string-color: hsl(330, 80%, 77%); + --md-code-hl-name-color: rgb(253, 248, 255); + --md-code-hl-operator-color: rgb(255, 182, 222); + --md-code-hl-punctuation-color: hsl(61, 100%, 72%); + --md-code-hl-comment-color: hsl(329, 24%, 51%); + --md-code-hl-generic-color: rgb(165, 79, 79); + --md-code-hl-variable-color: rgb(255, 147, 147); +} \ No newline at end of file diff --git a/docs/assets/favicon.png b/docs/assets/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..8a018d84a18df0d182803ebbf30e7c6c3a8d750f GIT binary patch literal 811 zcmV+`1JwM9P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0>DW`K~y+Tol{Fl zR8biI?wvb#(rHG=aUR;_C==DDhoPHF3Xviqf)*{>MQs{_9vB7{qoJ5&St=Px~s{Z5kTtX~h0zB^5=$LVBVG%0{B7~W>!WaG( zxhjcyPRi#X2r(_>blRXdlu{bx>YA$V&2e<1njvb8)mBghsM!DYhi{;_i(_cozMH*B zr&GvK_T1=#EGy_}Z|7#FrhS1*_=>VW6@1T=nPg#v;h z0b4w^h(=@d8*taw$k_s#w;GvotpWoB@94+~H$COYiQ@w}xc3ys-+o3qnnY4aAit2I zV+`p;0zI8Rj68qA8CZB#ErNtpPKHx~L#;>2@WZF{TrHHlD0fn}66lU;42)`L>YEH$68OHy9m9^(fCI6^?P9~DTS=ot13tVcAEt*do^CYeuqdV z3!Y=Fs`l?Cs*n{0ZC8%K@STF3e|BSRyPup1Re~xQ$L^9lArQ9Bx18M_kNd@X-)Hefy$u z@bM7Bi)mcD-T_&XpwP9dSk +

simple buffer-based networking

+

+ In ByteNet, you don't need to worry about type validation, optimization, packet structure, etc. ByteNet does all the hard parts for you! Strictly typed with an incredibly basic API that explains itself, ByteNet makes networking simple, easy, and quick. +

+ + Getting started + Download + +
+

+

+ practical example of a packet's definition +

+ + ```lua title="packets.luau" + local ByteNet = require(path.to.ByteNet) + + local packets = { + myPacket = ByteNet.definePacket({ + structure = { + -- A map where the key is a Vector3, and the value is a string + map = ByteNet.dataTypes.map(ByteNet.dataTypes.vec3(), ByteNet.dataTypes.string()) + } + }) + } + ``` + +

+
+ +

No more RemoteEvents

+

+ ByteNet handles all instances for you; you don't need to worry about reliability type, parenting, etc +

+ +

As low-level as you can get

+

+ ByteNet lets you directly read/write data types such as uint32, buffers, in a way where the keys are abstracted away. Reap the benefits of a structured, strictly typed library, while retaining the benefits of hardcore optimization. +

+
+ +
+ +

+

+ example of sending a packet to all clients +

+ + + ```lua title="server.luau" + local packets = require(path.to.packets) + + packets.myPacket:sendToAll({ + map = { + [Vector3.new(1, 1, 1)] = "This is one, one, one! Why not just use Vector3.one?" + } + }) + ``` + +
+ \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index cfc6f2e..563c0b0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,28 +1,43 @@ site_name: ByteNet - +repo_name: ffrostfall/ByteNet repo_url: https://github.com/ffrostflame/ByteNet -### Build settings ### - -theme: readthedocs - nav: - Home: index.md + - API: + - DataTypes: + - Primitives: api/dataTypes/Primitives.md + - Specials: api/dataTypes/Specials.md + - Functions: + - definePacket: api/functions/definePacket.md + theme: name: material + favicon: assets/favicon.png + logo: assets/bytenetLogo.png palette: scheme: slate + primary: custom + accent: white font: text: Roboto code: Fira Code features: - content.code.copy - extra_css: - - css/smoothscroll.css - + - navigation.tabs + - navigation.top + - navigation.sections + - navigation.instant + - navigation.indexes + - search.suggest + - search.highlight icon: repo: octicons/mark-github-16 +extra_css: + - assets/colors.css + - assets/home.css + markdown_extensions: - pymdownx.highlight: anchor_linenums: true From 4c2a9fca4895a4a88a9ea353d2d0f14fafd66aa6 Mon Sep 17 00:00:00 2001 From: ffrostflame Date: Thu, 15 Feb 2024 17:15:30 -0500 Subject: [PATCH 9/9] Documentation + polish --- dev/client/clientTests.client.luau | 2 +- dev/shared/testPackets.luau | 18 ++--- docs/api/dataTypes/Primitives.md | 29 ++++++++ docs/api/dataTypes/Specials.md | 108 +++++++++++++++++++++++++++++ docs/api/functions/definePacket.md | 95 +++++++++++++++++++++++++ docs/assets/colors.css | 6 +- docs/assets/home.css | 55 ++++++++++++++- docs/index.md | 68 +++++++++--------- mkdocs.yml | 15 ++-- src/init.luau | 36 +++++----- src/process/read.luau | 4 +- src/process/server.luau | 8 +-- src/types.luau | 36 +++++----- 13 files changed, 381 insertions(+), 99 deletions(-) diff --git a/dev/client/clientTests.client.luau b/dev/client/clientTests.client.luau index d6c86a9..8f97f03 100644 --- a/dev/client/clientTests.client.luau +++ b/dev/client/clientTests.client.luau @@ -6,7 +6,7 @@ local testPackets = require(ReplicatedStorage.shared.testPackets) --print(data) end)]] -testPackets.a:listen(function(data) +testPackets.a:listen(function() --print("Confirming server -> client") --print(data) end) diff --git a/dev/shared/testPackets.luau b/dev/shared/testPackets.luau index 0f14f59..8b676d3 100644 --- a/dev/shared/testPackets.luau +++ b/dev/shared/testPackets.luau @@ -5,17 +5,13 @@ local ByteNet = require(ReplicatedStorage.Packages.ByteNet) return { a = ByteNet.definePacket({ structure = { - a = ByteNet.dataTypes.bool(), - b = ByteNet.dataTypes.array(ByteNet.dataTypes.bool()), - c = ByteNet.dataTypes.array(ByteNet.dataTypes.array(ByteNet.dataTypes.bool())), - d = ByteNet.dataTypes.optional(ByteNet.dataTypes.bool()), - e = ByteNet.dataTypes.optional( - ByteNet.dataTypes.map(ByteNet.dataTypes.uint16(), ByteNet.dataTypes.uint8()) - ), - f = ByteNet.dataTypes.optional(ByteNet.dataTypes.map(ByteNet.dataTypes.vec3(), ByteNet.dataTypes.uint16())), - chained = ByteNet.dataTypes.optional( - ByteNet.dataTypes.optional(ByteNet.dataTypes.optional(ByteNet.dataTypes.string())) - ), + a = ByteNet.bool, + b = ByteNet.array(ByteNet.bool), + c = ByteNet.array(ByteNet.array(ByteNet.bool)), + d = ByteNet.optional(ByteNet.bool), + e = ByteNet.optional(ByteNet.map(ByteNet.uint16, ByteNet.uint8)), + f = ByteNet.optional(ByteNet.map(ByteNet.vec3, ByteNet.uint16)), + chained = ByteNet.optional(ByteNet.optional(ByteNet.optional(ByteNet.string))), }, }), } diff --git a/docs/api/dataTypes/Primitives.md b/docs/api/dataTypes/Primitives.md index e69de29..3628650 100644 --- a/docs/api/dataTypes/Primitives.md +++ b/docs/api/dataTypes/Primitives.md @@ -0,0 +1,29 @@ +
+ +

Available primitive types

+ +
+ +ByteNet provides a large amount of "primitive" types for you to build more complex types that suit your game. Since primitive types don't need any parameters, you can just access them like the following: `ByteNet.`. For building more complex data structures, go look at the Specials page. + +--- +## Supported generic types +- `string`: String +- `buff`: Buffer +- `bool`: Boolean +--- +## Supported number types +- `uint8`: Unsigned 8-bit integer +- `uint16`: Unsigned 16-bit integer +- `uint32`: Unsigned 32-bit integer +- `int8`: Signed 8-bit integer +- `int16`: Signed 16-bit integer +- `int32`: Signed 32-bit integer +- `float32`: Standard 32-bit float +- `float64`: Standard 64-bit float +--- +## Supported Roblox types +- `cframe`: CoordinateFrame +- `vec2`: Vector2 +- `vec3`: Vector3 +--- \ No newline at end of file diff --git a/docs/api/dataTypes/Specials.md b/docs/api/dataTypes/Specials.md index e69de29..3f17da5 100644 --- a/docs/api/dataTypes/Specials.md +++ b/docs/api/dataTypes/Specials.md @@ -0,0 +1,108 @@ +
+ +

Available special types

+ +
+ +Special types are how complex packet types are made. They can take in nearly any type, including themselves, and most importantly they are dynamic. This means you can have an array of any length, a map of any key type and any value type, and an optional value of any type. + +Special types always take *parameters*. You have to call them: `ByteNet.()`. + +!!!danger + ### There are drawbacks to using these! + + - Using these types incurs 1-2 bytes of overhead due to the dynamic nature. + - They take drastically more time to parse + - They are heavier on memory usage, as a new closure is created each time. You will never have to worry about this unless you have dozens of packets, though. + +--- + +## Optionals +Optional types are a cool name for the concept of "This doesn't have to exist". It's good for optional parameters: for example if some invoked function were to fail, you might want to send back a blank copy to indicate that something is wrong. + +```lua title="packets.luau" +return { + myCoolPacket = ByteNet.definePacket({ + structure = { + -- An "optional" type takes in a parameter. + -- This can be anything! You can even have optional arrays. + helloWorldString = ByteNet.optional(ByteNet.string) + + -- This works! + eachCharacter = ByteNet.optional(ByteNet.array(ByteNet.string)) + }, + }) +} +``` +You really don't have to think about using optional types. You just send it! +```lua title="server.luau" +local packets = require(path.to.packets) + +local randomlyStringOrNil = + if math.random(1, 2) == 1 then "Hello, world!" else nil + +packets.myCoolPacket:sendToAll({ + helloWorldString = randomlyAppearingString, + + -- Note that even if we don't put the "eachCharacter" field here, + -- it won't error. This is because it's optional! +}) +``` + +--- + +## Arrays +Arrays are fairly self explanatory. They're just plain old arrays. However, it's important to note that mixed tables have **undefined** behavior when passed as an array. This means things might be missing when they come out on the other side! + +There is a 2 byte overhead to sending an array in ByteNet. This is because these 2 bytes are an unsigned 16-bit integer, which stores the array length. As a side effect, arrays sent through ByteNet have a max length of **2^16**, which is equal to **65,536**. It's likely that in the future, you will be able to reduce the overhead to 1 byte through configuration, in turn reducing the max length of the array. + +```lua title="packets.luau" +return { + myCoolPacket = ByteNet.definePacket({ + structure = { + myArray = ByteNet.array(ByteNet.bool) + }, + }) +} +``` + +```lua title="server.luau" +local packets = require(path.to.packets) + +packets.myCoolPacket:sendToAll({ + -- Important to note that mixed arrays/arrays with holes + -- shouldn't be sent through. + myArray = { true, false, true } +}) +``` + +--- + +## Maps +Maps are by far the most powerful "special" type in ByteNet. They let you send, what's most commonly referred to as a dictionary, through ByteNet. However it's important to keep in mind two things: the type of the key (or index), and the type of the value, cannot change. + +Like arrays, maps have a 2-byte overhead to store the length of the map. This is done by iterating over the map using generic iteration and increasing a variable by 1 for every key-value pair. This, once again, means that there is a **2^16** (which equals to **65,536**) cap to the number of elements in the map. + +```lua title="packets.luau" +return { + myCoolPacket = ByteNet.definePacket({ + structure = { + -- [name] = age + people = ByteNet.map(ByteNet.string, ByteNet.uint16) + }, + }) +} +``` + +```lua title="server.luau" +local packets = require(path.to.packets) + +packets.myCoolPacket:sendToAll({ + people = { + john = 21, + jane = 24, + dave = 26, + you = 162, + } +}) +``` \ No newline at end of file diff --git a/docs/api/functions/definePacket.md b/docs/api/functions/definePacket.md index e69de29..c0c1260 100644 --- a/docs/api/functions/definePacket.md +++ b/docs/api/functions/definePacket.md @@ -0,0 +1,95 @@ +
+ +

Defining a packet

+ +
+ +## what even is a packet in ByteNet anyway? +Packets are the structured dictionaries you use to define the "format" your data is going to be sent in. These are named packets as you 'send' packets through network, and packets have their contents usually formatted in a specific way because, well they have to be. + +ByteNet's purpose as a library is to provide a way to structure your data, and send that structure in a hyper-optimized way. + +--- + +## Okay, where do I start? +I highly recommended storing all of your packets in a single, shared module, and then using a dictionary to access the individual packets. Not only does this make using ByteNet a breeze, it also gives you a better form of typechecking. + +The keys of your packet determine the ID it gets. The server and the client sharing this key is essential: that's the core of how ByteNet works. Unfortunately, this means you cannot have duplicate contents of packets. **This may change in the future.** + +Enough of how it works, let's start off with making a basic packet: +```lua title="packets.luau" +local ByteNet = require(path.to.bytenet) + +return { + printSomething = ByteNet.definePacket({ + -- This structure field is very important! + structure = { + message = ByteNet.string, + } + }) +} +``` + +--- + +## Sending + +On the server, there are 3 methods of sending packets to players. It's important to note that when a player should be specified, it's the *second* parameter given, not the first. This is an intentional design choice. + +- `sendToAll` +- `sendToAllExcept` +- `sendTo` + +On the client, there is only one, because you can only send data to the server: + +- `send` + +These functions *must* obey your structure created in `definePacket`, and if you have strict typing on, an error will be raised if the types do not match. + +*code examples* +```lua title="client.luau" +-- Sending to server +packets.myPacket:send({ + message = "Hello, world!" +}) +``` +```lua title="server.luau" +-- Sending to all players +packets.myPacket:sendToAll({ + message = "Hello, players!" +}) + +-- Sending to an individual player +local someArbitraryPlayer = Players.You + +packets.myPacket:sendTo({ + message = "Hello, random player!" +}, someArbitraryPlayer) + +-- Sending to all except a certain player +local someArbitraryPlayer = Players.You + +packets.myPacket:sendToAllExcept({ + message = "Hello, everyone except one person!" +}) +``` + +## Receiving + +You can use the `listen` method to listen for when a packet is received. + +*code examples* +```lua title="server.luau" +packets.myPacket:listen(function(data, player) + print(`{player.UserId} says { data.message }`) +end) +``` +```lua title="client.luau" +packets.myPacket:listen(function(data) + print(`server says { data.message }`) +end) +``` +--- + +## Configuration (Reliability types) +Currently, the only config accessible right now is reliability types. This will change as time goes on, however right now, you can switch reliability types by defining `reliabilityType` to be `"reliable"` or `"unreliable"`. \ No newline at end of file diff --git a/docs/assets/colors.css b/docs/assets/colors.css index bf1157e..99246fe 100644 --- a/docs/assets/colors.css +++ b/docs/assets/colors.css @@ -12,13 +12,17 @@ color: rgb(255, 255, 255); } +.md-typeset p { + color: white +} + body { --md-primary-bg-color: #e6f9ff; } [data-md-color-scheme="slate"] { --md-code-fg-color: rgb(26, 26, 26); - --md-code-bg-color: rgb(0, 0, 0); + --md-code-bg-color: rgb(51, 51, 51); --md-code-hl-color: rgb(255, 255, 255); --md-code-hl-number-color: hsl(290, 60%, 70%); --md-code-hl-special-color: rgb(255, 255, 255); diff --git a/docs/assets/home.css b/docs/assets/home.css index 6205b76..28aead0 100644 --- a/docs/assets/home.css +++ b/docs/assets/home.css @@ -19,8 +19,59 @@ #bytenet-header h1 { font-size: 2.5em; font-weight: bolder; - margin-bottom: 0; + margin-bottom: 10px; + color: white; +} + +.docs h1 { + font-size: 2em; + font-weight: bold; + margin-bottom: 10px; + color: white; +} + +.md-typeset h1 { + margin-bottom: 1cm; + color: white; + font-weight: bold; +} + +.md-typeset h2 { color: white; + margin-top: 10px; + margin-bottom: 0px; +} + +.md-typeset h3#there-are-drawbacks-to-using-these { + font-weight: bolder; + color: #ff1744; + margin-top: 10px; + margin-bottom: 0px; + font-size: 1.5em; +} + +.md-typeset .admonition.danger li { + font-size: 1.2em; + color: #ff4a6f +} + +.md-typeset .admonition.danger { + background-color: #4b071563; +} + +.md-grid { + max-width: 75%; +} + +.md-typeset ul li { + margin-bottom: 0px; + margin-top: 0px; + color: rgb(255, 223, 163); +} + +.md-typeset code { + color: #e6f9ff; + background-color: rgb(26, 26, 26); } #bytenet-header h2 { @@ -41,7 +92,7 @@ font-weight: lighter; margin-top: 5px; margin-bottom: 15px; - color: rgb(219, 254, 255); + color: rgb(255, 255, 255); } #bytenet-header #small { diff --git a/docs/index.md b/docs/index.md index d9951fe..3fc44a0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,7 +4,7 @@ - navigation --- -
+

simple buffer-based networking

In ByteNet, you don't need to worry about type validation, optimization, packet structure, etc. ByteNet does all the hard parts for you! Strictly typed with an incredibly basic API that explains itself, ByteNet makes networking simple, easy, and quick. @@ -31,38 +31,36 @@ }) } ``` - -

-
- -

No more RemoteEvents

-

- ByteNet handles all instances for you; you don't need to worry about reliability type, parenting, etc -

- -

As low-level as you can get

-

- ByteNet lets you directly read/write data types such as uint32, buffers, in a way where the keys are abstracted away. Reap the benefits of a structured, strictly typed library, while retaining the benefits of hardcore optimization. -

-
- -
- -

-

- example of sending a packet to all clients -

+

- - ```lua title="server.luau" - local packets = require(path.to.packets) - - packets.myPacket:sendToAll({ - map = { - [Vector3.new(1, 1, 1)] = "This is one, one, one! Why not just use Vector3.one?" - } - }) - ``` - -
-
\ No newline at end of file +

+ example of sending a packet to all clients +

+ + +```lua title="server.luau" +local packets = require(path.to.packets) + +packets.myPacket:sendToAll({ + map = { + [Vector3.new(1, 1, 1)] = "10x less bandwidth than a remote!", + [Vector3.new(1, 2, 1)] = "oh wait, its way more than 10x less.", + + [Vector3.new(2, 1, 1)] = "depending on how you use your data,", + [Vector3.new(1, 1, 2)] = "it could be 100x less!", + } +}) +``` + +
+ + +--- + +# Not enough? here's more + +## - No more RemoteEvents +ByteNet handles all instances for you; you don't need to worry about reliability type, parenting, etc + +## - As low-level as you can get +ByteNet lets you directly read/write data types such as uint32, buffers, in a way where the keys are abstracted away. Reap the benefits of a structured, strictly typed library, while retaining the benefits of hardcore optimization. \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 563c0b0..6dd3c2c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -4,12 +4,13 @@ repo_url: https://github.com/ffrostflame/ByteNet nav: - Home: index.md - - API: + - Documentation: + - Packets: + - definePacket: api/functions/definePacket.md - DataTypes: - Primitives: api/dataTypes/Primitives.md - Specials: api/dataTypes/Specials.md - - Functions: - - definePacket: api/functions/definePacket.md + theme: name: material @@ -45,7 +46,11 @@ markdown_extensions: pygments_lang_class: true - pymdownx.inlinehilite - pymdownx.snippets - - pymdownx.superfences - admonition - pymdownx.details - - pymdownx.superfences \ No newline at end of file + - pymdownx.extra + - pymdownx.superfences + - attr_list + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg \ No newline at end of file diff --git a/src/init.luau b/src/init.luau index 4315e1a..acca3fc 100644 --- a/src/init.luau +++ b/src/init.luau @@ -36,24 +36,22 @@ return ( table.freeze({ definePacket = packet, - dataTypes = { - array = array, - bool = bool, - optional = optional, - uint8 = uint8, - uint16 = uint16, - uint32 = uint32, - int8 = int8, - int16 = int16, - int32 = int32, - float32 = float32, - float64 = float64, - cframe = cframe, - string = string, - vec2 = vec2, - vec3 = vec3, - buff = buff, - map = map, - }, + array = array, + bool = bool(), + optional = optional, + uint8 = uint8(), + uint16 = uint16(), + uint32 = uint32(), + int8 = int8(), + int16 = int16(), + int32 = int32(), + float32 = float32(), + float64 = float64(), + cframe = cframe(), + string = string(), + vec2 = vec2(), + vec3 = vec3(), + buff = buff(), + map = map, }) :: any ) :: types.ByteNet diff --git a/src/process/read.luau b/src/process/read.luau index 1c36106..8e43d42 100644 --- a/src/process/read.luau +++ b/src/process/read.luau @@ -9,7 +9,7 @@ local retrievePacketFromID = ( if RunService:IsServer() then serverPacketIDs.getPacketFromID else clientPacketIDs.getPacketFromID ) :: (packetID: number) -> any -return function(incomingBuffer: buffer) +return function(incomingBuffer: buffer, player: Player?) local length = buffer.len(incomingBuffer) local readCursor = 0 @@ -37,7 +37,7 @@ return function(incomingBuffer: buffer) end for _, listener in packet.listeners do - task.spawn(listener, deserialized) + task.spawn(listener, deserialized, player) end end end diff --git a/src/process/server.luau b/src/process/server.luau index 984cc43..a1012bd 100644 --- a/src/process/server.luau +++ b/src/process/server.luau @@ -21,7 +21,7 @@ type channelData = { } -- Cursor is copied, jobs is not. -local cursor = 0 +local cursor: number = 0 local jobs = {} local function load(freshChannel: channelData?) @@ -57,7 +57,7 @@ local function onServerEvent(player: Player, data) return end - read(data) + read(data, player) end local function addPacketToLoadedChannel(id: number, format: types.packetFormat, data: { [string]: any }) @@ -126,12 +126,12 @@ function serverProcess.sendPlayerUnreliable( end function serverProcess.start() - local reliableRemote = Instance.new("RemoteEvent") + local reliableRemote: RemoteEvent = Instance.new("RemoteEvent") reliableRemote.Name = "ByteNetReliable" reliableRemote.OnServerEvent:Connect(onServerEvent) reliableRemote.Parent = ReplicatedStorage - local unreliableRemote = Instance.new("UnreliableRemoteEvent") + local unreliableRemote: UnreliableRemoteEvent = Instance.new("UnreliableRemoteEvent") unreliableRemote.Name = "ByteNetUnreliable" unreliableRemote.OnServerEvent:Connect(onServerEvent) unreliableRemote.Parent = ReplicatedStorage diff --git a/src/types.luau b/src/types.luau index 28ee945..c8bde3a 100644 --- a/src/types.luau +++ b/src/types.luau @@ -33,25 +33,23 @@ export type ByteNet = { reliabilityType: ("reliable" | "unreliable")?, }) -> Packet, - dataTypes: { - bool: () -> boolean, - array: (value: T) -> { [number]: T }, - optional: (value: T) -> T?, - uint8: () -> number, - uint16: () -> number, - uint32: () -> number, - int8: () -> number, - int16: () -> number, - int32: () -> number, - float32: () -> number, - float64: () -> number, - string: () -> string, - vec3: () -> Vector3, - vec2: () -> Vector2, - buff: () -> buffer, - cframe: () -> CFrame, - map: (key: K, value: V) -> { [K]: V }, - }, + bool: boolean, + array: (value: T) -> { [number]: T }, + optional: (value: T) -> T?, + uint8: number, + uint16: number, + uint32: number, + int8: number, + int16: number, + int32: number, + float32: number, + float64: number, + string: string, + vec3: Vector3, + vec2: Vector2, + buff: buffer, + cframe: CFrame, + map: (key: K, value: V) -> { [K]: V }, } return nil