diff --git a/src/ReplicatedFirst/Packages/Chickynoid/Shared/Vendor/RemotePacketSizeCounter.lua b/src/ReplicatedFirst/Packages/Chickynoid/Shared/Vendor/RemotePacketSizeCounter.lua new file mode 100644 index 0000000..198487d --- /dev/null +++ b/src/ReplicatedFirst/Packages/Chickynoid/Shared/Vendor/RemotePacketSizeCounter.lua @@ -0,0 +1,200 @@ +--!strict +-- https://github.com/Pyseph/RemotePacketSizeCounter +local BASE_REMOTE_OVERHEAD = 9 +local REMOTEFUNCTION_OVERHEAD = 2 +local CLIENT_TO_SERVER_OVERHEAD = 5 + +local TYPE_OVERHEAD = 1 + +-- Byte sizes of different types of values +local Float64 = 8 +local Float32 = 4 +local Float16 = 2 +local Int32 = 4 +local Int16 = 2 +local Int8 = 1 + +-- Vector3's are stored as 3 Float32s, which equates to 12 bytes. They have a 1-byte overhead +-- for what's presumably type differentiation, so the informal calculation for datatypes is: +-- num_types*num_bytes_in_type + TYPE_OVERHEAD +-- Example: +-- Vector3: 3 float32s, 1 byte overhead: 3*4 + 1 = 13 bytes +-- The structs of datatypes can be found below: +-- https://dom.rojo.space/binary.html +-- !! It should still be benchmarked to see if the bytes are correctly calculated !! + +local COLOR3_BYTES = 3*Float32 +local VECTOR3_BYTES = 3*Float32 + +local TypeByteSizes: {[string]: number} = { + ["nil"] = 0, + EnumItem = Int32, + boolean = 1, + number = Float64, + UDim = Float32 + Int32, + UDim2 = 2*(Float32 + Int32), + Ray = 6*Float32, + Faces = 6, + Axes = 6, + BrickColor = Int32, + Color3 = COLOR3_BYTES, + Vector2 = 2*Float32, + Vector3 = VECTOR3_BYTES, + -- It's unclear how instances are sent, but in binary-storage format they're stored with + -- 'Referents', which can be found in the binary-storage documentation above. + -- Benchmarks also show that they take up 4 bytes, excluding byte overhead. + Instance = Int32, + Vector2int16 = 2*Int16, + Vector3int16 = 3*Int16, + NumberSequenceKeypoint = 3*Float32, + ColorSequenceKeypoint = 4*Float32, + NumberRange = 2*Float32, + Rect = 2*(2*Float32), + PhysicalProperties = 5*Float32, + Color3uint8 = 3*Int8, +} + +-- https://dom.rojo.space/binary.html#cframe +local CFrameSpecialCases = { + [CFrame.Angles(0, 0, 0)] = true, [CFrame.Angles(0, math.rad(180), math.rad(0))] = true, + [CFrame.Angles(math.rad(90), 0, 0)] = true, [CFrame.Angles(math.rad(-90), math.rad(-180), math.rad(0))] = true, + [CFrame.Angles(0, math.rad(180), math.rad(180))] = true, [CFrame.Angles(0, math.rad(0), math.rad(180))] = true, + [CFrame.Angles(math.rad(-90), 0, 0)] = true, [CFrame.Angles(math.rad(90), math.rad(180), math.rad(0))] = true, + [CFrame.Angles(0, math.rad(180), math.rad(90))] = true, [CFrame.Angles(0, math.rad(0), math.rad(-90))] = true, + [CFrame.Angles(0, math.rad(90), math.rad(90))] = true, [CFrame.Angles(0, math.rad(-90), math.rad(-90))] = true, + [CFrame.Angles(0, 0, math.rad(90))] = true, [CFrame.Angles(0, math.rad(-180), math.rad(-90))] = true, + [CFrame.Angles(0, math.rad(-90), math.rad(90))] = true, [CFrame.Angles(0, math.rad(90), math.rad(-90))] = true, + [CFrame.Angles(math.rad(-90), math.rad(-90), 0)] = true, [CFrame.Angles(math.rad(90), math.rad(90), 0)] = true, + [CFrame.Angles(0, math.rad(-90), 0)] = true, [CFrame.Angles(0, math.rad(90), 0)] = true, + [CFrame.Angles(math.rad(90), math.rad(-90), 0)] = true, [CFrame.Angles(math.rad(-90), math.rad(90), 0)] = true, + [CFrame.Angles(0, math.rad(90), math.rad(180))] = true, [CFrame.Angles(0, math.rad(-90), math.rad(180))] = true, +} + +-- https://en.wikipedia.org/wiki/Variable-length_quantity +local function GetVLQSize(InitialSize: number, Length: number) + return math.max(math.ceil(math.log(Length + InitialSize, 128)), InitialSize) +end + +local function GetDataByteSize(Data: any, AlreadyTraversed: {[{[any]: any}]: boolean}) + local DataType = typeof(Data) + if TypeByteSizes[DataType] then + return TypeByteSizes[DataType] + elseif DataType == "string" or DataType == "buffer" then + -- https://data-oriented-house.github.io/Squash/docs/binary/#strings + local Length = DataType == "string" and #Data or buffer.len(Data) + return GetVLQSize(1, Length) + Length + elseif DataType == "table" then + if AlreadyTraversed[Data] then + return 0 + end + AlreadyTraversed[Data] = true + + local KeyTotal = 0 + local ValueTotal = 0 + + local NumKeys = 1 + local IsArray = Data[1] ~= nil + for Key, Value in next, Data do + NumKeys += 1 + + if not IsArray then + KeyTotal += GetDataByteSize(Key, AlreadyTraversed) + TYPE_OVERHEAD + end + ValueTotal += GetDataByteSize(Value, AlreadyTraversed) + TYPE_OVERHEAD + end + + if IsArray then + return GetVLQSize(1, NumKeys) + ValueTotal + else + return GetVLQSize(1, NumKeys) + KeyTotal + ValueTotal + end + elseif DataType == "CFrame" then + local IsSpecialCase = false + for SpecialCase in next, CFrameSpecialCases do + if SpecialCase == Data.Rotation then + IsSpecialCase = true + break + end + end + + if IsSpecialCase then + -- Axis-aligned CFrames skip sending rotation and are encoded as only 13 bytes + return 1 + VECTOR3_BYTES + else + -- 1 byte for the ID, 12 bytes for the position vector, and 6 bytes for the quaternion representation + -- I'm assuming they send x,y,z quaternions and reconstruct w from `x*x + y*y + z*z + w*w = 1`. + return 1 + VECTOR3_BYTES + 3*Float16 + end + elseif DataType == "NumberSequence" or DataType == "ColorSequence" then + local Total = 4 + for _, Keypoint in next, Data.Keypoints do + Total += GetDataByteSize(Keypoint, AlreadyTraversed) + end + + return Total + else + warn("[PacketSizeCounter]: Unsupported data type: " .. DataType) + return 0 + end +end + +--- @class PacketSizeCounter +--- The main class for calculating the size of packets. +local PacketSizeCounter = {} + +--- @prop BaseRemoteOverhead number +--- @within PacketSizeCounter +--- @readonly +--- The byte overhead of a remote event, in bytes. +PacketSizeCounter.BaseRemoteOverhead = BASE_REMOTE_OVERHEAD + + +--- @prop RemoteFunctionOverhead number +--- @within PacketSizeCounter +--- @readonly +--- The additional byte overhead of a remote function, in bytes. +PacketSizeCounter.RemoteFunctionOverhead = REMOTEFUNCTION_OVERHEAD + + +--- @prop ClientToServerOverhead number +--- @within PacketSizeCounter +--- @readonly +--- The additional byte overhead of a client-to-server remote, in bytes. +PacketSizeCounter.ClientToServerOverhead = CLIENT_TO_SERVER_OVERHEAD + + +--- @prop TypeOverhead number +--- @within PacketSizeCounter +--- @readonly +--- The byte overhead of a type, in bytes. +PacketSizeCounter.TypeOverhead = TYPE_OVERHEAD + +--- Returns the byte size of a packet from the given data. Remote overhead is automatically added, and is different based on the remote type and run context. +function PacketSizeCounter.GetPacketSize(CounterData: { + RunContext: "Server" | "Client", + RemoteType: "RemoteEvent" | "RemoteFunction", + PacketData: {any} +}): number + local Total = BASE_REMOTE_OVERHEAD + if CounterData.RemoteType == "RemoteFunction" then + Total += REMOTEFUNCTION_OVERHEAD + end + if CounterData.RunContext == "Client" then + Total += CLIENT_TO_SERVER_OVERHEAD + end + + local AlreadyTraversed = {} + + for _, Data in ipairs(CounterData.PacketData) do + Total += GetDataByteSize(Data, AlreadyTraversed) + TYPE_OVERHEAD + end + + return Total +end +--- Returns the byte size of a single data object type. Supports most types. +function PacketSizeCounter.GetDataByteSize(Data: any): number + return GetDataByteSize(Data, {}) + TYPE_OVERHEAD +end + +table.freeze(PacketSizeCounter) +return PacketSizeCounter \ No newline at end of file diff --git a/src/ServerScriptService/Packages/Chickynoid/Server/ServerModule.lua b/src/ServerScriptService/Packages/Chickynoid/Server/ServerModule.lua index 613b4f2..c070b27 100644 --- a/src/ServerScriptService/Packages/Chickynoid/Server/ServerModule.lua +++ b/src/ServerScriptService/Packages/Chickynoid/Server/ServerModule.lua @@ -23,6 +23,8 @@ local WeaponsModule = require(script.Parent.WeaponsServer) local CollisionModule = require(path.Shared.Simulation.CollisionModule) local Antilag = require(script.Parent.Antilag) local FastSignal = require(path.Shared.Vendor.FastSignal) +local RemotePacketSizeCounter = require(path.Shared.Vendor.RemotePacketSizeCounter) + local ServerMods = require(script.Parent.ServerMods) local Animations = require(path.Shared.Simulation.Animations) @@ -90,6 +92,7 @@ ServerModule.flags.DEBUG_BOT_BANDWIDTH = false ]=] function ServerModule:Setup() self.worldRoot = self:GetDoNotReplicate() + -- self.worldRoot = workspace Players.PlayerAdded:Connect(function(player) self:PlayerConnected(player) @@ -182,7 +185,7 @@ function ServerModule:AssignSlot(playerRecord) return false end -function ServerModule:AddConnection(userId, player) +function ServerModule:AddConnection(userId, player, characterMod) if self.playerRecords[userId] ~= nil or self.loadingPlayerRecords[userId] ~= nil then warn("Player was already connected.", userId) self:PlayerDisconnected(userId) @@ -210,7 +213,7 @@ function ServerModule:AddConnection(userId, player) playerRecord.OnBeforePlayerSpawn = FastSignal.new() playerRecord.visHistoryList = {} - playerRecord.characterMod = "HumanoidChickynoid" + playerRecord.characterMod = characterMod or "HumanoidChickynoid" playerRecord.lastConfirmedSnapshotServerFrame = nil --Stays nil til a player confirms they've seen a whole snapshot, for delta compression purposes @@ -324,6 +327,8 @@ function ServerModule:AddConnection(userId, player) self.chickynoid = chickynoid chickynoid.playerRecord = self + -- This can be really inefficient when you have a ton of parts! Consider + -- changing this spawn behavior, maybe CollectionService or something. local list = {} for _, obj: SpawnLocation in pairs(workspace:GetDescendants()) do if obj:IsA("SpawnLocation") and obj.Enabled == true then @@ -666,7 +671,13 @@ function ServerModule:UpdatePlayerStatesToPlayers() event.serverFrame = self.serverTotalFrames event.playerStateDelta, event.playerStateDeltaFrame = playerRecord.chickynoid:ConstructPlayerStateDelta(self.serverTotalFrames) - playerRecord:SendUnreliableEventToClient(event) + -- // CHECK PACKET SIZE, PROBABLY HUGE FOR A BIG STATE + local s = RemotePacketSizeCounter.GetDataByteSize(event.playerStateDelta) + if s > 700 then + playerRecord:SendEventToClient(event) + else + playerRecord:SendUnreliableEventToClient(event) + end --Clear the error state flag playerRecord.chickynoid.errorState = Enums.NetworkProblemState.None @@ -718,7 +729,7 @@ function ServerModule:UpdatePlayerThinks(deltaTime) if playerRecord.chickynoid then playerRecord.chickynoid:Think(self, self.serverSimulationTime, deltaTime) - if playerRecord.chickynoid.simulation.state.pos.y < -2000 then + if playerRecord.chickynoid.simulation.state.position.y < -2000 then playerRecord:Despawn() end end diff --git a/src/ServerScriptService/Packages/Chickynoid/Server/ServerSnapshotGen.lua b/src/ServerScriptService/Packages/Chickynoid/Server/ServerSnapshotGen.lua index 2f944ec..33bd980 100644 --- a/src/ServerScriptService/Packages/Chickynoid/Server/ServerSnapshotGen.lua +++ b/src/ServerScriptService/Packages/Chickynoid/Server/ServerSnapshotGen.lua @@ -8,6 +8,7 @@ local Profiler = require(path.Shared.Vendor.Profiler) local CharacterData = require(path.Shared.Simulation.CharacterData) local DeltaTable = require(path.Shared.Vendor.DeltaTable) +local RemotePacketSizeCounter = require(path.Shared.Vendor.RemotePacketSizeCounter) local Enums = require(path.Shared.Enums) local EventType = Enums.EventType local absoluteMaxSizeOfBuffer = 4096 @@ -219,7 +220,12 @@ function module:DoWork(playerRecords, serverTotalFrames, serverSimulationTime, d for _,snapshot in queue do snapshot.m = #queue - playerRecord:SendUnreliableEventToClient(snapshot) + local s = RemotePacketSizeCounter.GetDataByteSize(snapshot.playerStateDelta) + if s > 700 then + playerRecord:SendEventToClient(snapshot) + else + playerRecord:SendUnreliableEventToClient(snapshot) + end end end end