@skating/swiftpacket 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +123 -0
- package/package.json +31 -0
- package/src/client.luau +472 -0
- package/src/configuration.luau +74 -0
- package/src/cursor.luau +18 -0
- package/src/index.d.ts +122 -0
- package/src/init.luau +199 -0
- package/src/packet.luau +186 -0
- package/src/rate_limit.luau +38 -0
- package/src/registry.luau +40 -0
- package/src/schema.luau +1606 -0
- package/src/server.luau +691 -0
- package/src/signal.luau +145 -0
- package/src/thread_pool.luau +37 -0
- package/src/wire.luau +218 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 skaterstudios (skaterstudios.com)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# SwiftPacket
|
|
2
|
+
|
|
3
|
+
[](https://github.com/skatingii/SwiftPacket/actions/workflows/ci.yml)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
A fast, fully typed, buffer-batched networking library for Roblox.
|
|
7
|
+
|
|
8
|
+
Describe each packet once, with the types of the values it carries, then fire it
|
|
9
|
+
like a function. SwiftPacket writes every message into one buffer per player,
|
|
10
|
+
sends it once per frame, and checks everything a client sends before your code
|
|
11
|
+
sees it.
|
|
12
|
+
|
|
13
|
+
- **Typed**: `Fire()`, handlers and responses are checked by the Luau type
|
|
14
|
+
checker, without a single cast.
|
|
15
|
+
- **Small**: every value is written in the fewest bytes its type allows, with no
|
|
16
|
+
type tags on the wire.
|
|
17
|
+
- **Batched**: one remote call per player per frame, for reliable and unreliable
|
|
18
|
+
packets.
|
|
19
|
+
- **Ordered**: messages to a player arrive in the order they were fired, events
|
|
20
|
+
and requests alike.
|
|
21
|
+
- **Hardened**: bounds checks, rejection of NaN and infinity, a per-player byte
|
|
22
|
+
budget, rate limits and validators.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
With [Wally](https://wally.run), add SwiftPacket to your `wally.toml`:
|
|
27
|
+
|
|
28
|
+
```toml
|
|
29
|
+
[dependencies]
|
|
30
|
+
SwiftPacket = "skatingii/swiftpacket@0.1.4"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
With [roblox-ts](https://roblox-ts.com), install it from npm:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
npm install @skating/swiftpacket
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Add the `@skating` scope to your project so roblox-ts can find it. In
|
|
40
|
+
`tsconfig.json`:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
"typeRoots": ["node_modules/@rbxts", "node_modules/@skating"]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
And in `default.project.json`, next to `@rbxts` under `node_modules`:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
"@skating": { "$path": "node_modules/@skating" }
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Then import it:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import SwiftPacket from "@skating/swiftpacket";
|
|
56
|
+
|
|
57
|
+
const Chat = SwiftPacket("Chat", SwiftPacket.String);
|
|
58
|
+
const GetCoins = SwiftPacket("GetCoins").Response(SwiftPacket.U32);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Or download `SwiftPacket.rbxm` from the
|
|
62
|
+
[latest release](https://github.com/skatingii/SwiftPacket/releases/latest) and put
|
|
63
|
+
it in `ReplicatedStorage`.
|
|
64
|
+
|
|
65
|
+
## Example
|
|
66
|
+
|
|
67
|
+
Define your packets in one module that both the server and the client require:
|
|
68
|
+
|
|
69
|
+
```luau
|
|
70
|
+
const SwiftPacket = require("@game/ReplicatedStorage/Packages/SwiftPacket")
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
Chat = SwiftPacket("Chat", SwiftPacket.String):RateLimit(5, 1),
|
|
74
|
+
Move = SwiftPacket("Move", SwiftPacket.Vector3F24):Unreliable(),
|
|
75
|
+
GetCoins = SwiftPacket("GetCoins"):Response(SwiftPacket.U32),
|
|
76
|
+
Inventory = SwiftPacket("Inventory", {
|
|
77
|
+
Coins = SwiftPacket.U32,
|
|
78
|
+
Items = { SwiftPacket.String },
|
|
79
|
+
}),
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
On the server:
|
|
84
|
+
|
|
85
|
+
```luau
|
|
86
|
+
const Packets = require("@game/ReplicatedStorage/Packets")
|
|
87
|
+
|
|
88
|
+
Packets.Chat.OnServerEvent:Connect(function(Player, Message)
|
|
89
|
+
Packets.Chat:FireExcept(Player, `{Player.Name}: {Message}`)
|
|
90
|
+
end)
|
|
91
|
+
|
|
92
|
+
Packets.GetCoins:SetServerInvoke(function(Player)
|
|
93
|
+
return 100
|
|
94
|
+
end)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
On the client:
|
|
98
|
+
|
|
99
|
+
```luau
|
|
100
|
+
const Packets = require("@game/ReplicatedStorage/Packets")
|
|
101
|
+
|
|
102
|
+
Packets.Chat:Fire("hello")
|
|
103
|
+
|
|
104
|
+
const Coins = Packets.GetCoins:Fire()
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Documentation
|
|
108
|
+
|
|
109
|
+
- [Crash course](docs/tut/crash-course/1-introduction.md): packets, events,
|
|
110
|
+
requests, types and security, one page each.
|
|
111
|
+
- [API reference](docs/api/swiftpacket.md): every function and type.
|
|
112
|
+
- [Migrating from Packet](docs/tut/crash-course/8-migrating-from-packet.md):
|
|
113
|
+
what changes if you are coming from Packet.
|
|
114
|
+
|
|
115
|
+
## Security
|
|
116
|
+
|
|
117
|
+
Please do not report a vulnerability in a public issue. See
|
|
118
|
+
[SECURITY.md](SECURITY.md) for how to report one privately.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
SwiftPacket is released under the [MIT License](LICENSE) by
|
|
123
|
+
[skaterstudios](https://skaterstudios.com).
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@skating/swiftpacket",
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"description": "A fast, fully typed, buffer-batched networking library for Roblox",
|
|
5
|
+
"main": "src/init.luau",
|
|
6
|
+
"types": "src/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"src"
|
|
9
|
+
],
|
|
10
|
+
"keywords": [
|
|
11
|
+
"roblox",
|
|
12
|
+
"roblox-ts",
|
|
13
|
+
"luau",
|
|
14
|
+
"networking",
|
|
15
|
+
"remote",
|
|
16
|
+
"buffer"
|
|
17
|
+
],
|
|
18
|
+
"author": "skaterstudios",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"homepage": "https://github.com/skatingii/SwiftPacket#readme",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/skatingii/SwiftPacket.git"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/skatingii/SwiftPacket/issues"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/client.luau
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--!optimize 2
|
|
3
|
+
|
|
4
|
+
--[[
|
|
5
|
+
Author: @skaterstudios
|
|
6
|
+
Website: skaterstudios.com
|
|
7
|
+
GitHub: github.com/skatingii/SwiftPacket
|
|
8
|
+
Discord Server: discord.gg/skaterstudios
|
|
9
|
+
|
|
10
|
+
Licensed under the MIT License. You may use, modify, redistribute and sell
|
|
11
|
+
this system freely, as long as this notice and the LICENSE file stay with it.
|
|
12
|
+
]]
|
|
13
|
+
|
|
14
|
+
const RunService = game:GetService("RunService")
|
|
15
|
+
|
|
16
|
+
const Configuration = require("./configuration")
|
|
17
|
+
const Cursor = require("./cursor")
|
|
18
|
+
const Registry = require("./registry")
|
|
19
|
+
const Schema = require("./schema")
|
|
20
|
+
const ThreadPool = require("./thread_pool")
|
|
21
|
+
const Wire = require("./wire")
|
|
22
|
+
|
|
23
|
+
type Definition = Registry.Definition
|
|
24
|
+
type Arguments = Schema.Arguments
|
|
25
|
+
|
|
26
|
+
type PendingCall = {
|
|
27
|
+
Thread: thread,
|
|
28
|
+
Deadline: number,
|
|
29
|
+
Definition: Definition,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type Held = {
|
|
33
|
+
Source: buffer,
|
|
34
|
+
Instances: { any },
|
|
35
|
+
Offset: number,
|
|
36
|
+
InstancesOffset: number,
|
|
37
|
+
MissingId: number,
|
|
38
|
+
ReceivedAt: number,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const Client = {}
|
|
42
|
+
|
|
43
|
+
const Reliable = Cursor.New()
|
|
44
|
+
const Unreliable: { Cursor.Cursor } = { Cursor.New(Configuration.UnreliableByteLimit) }
|
|
45
|
+
const PendingCalls: { [number]: PendingCall } = {}
|
|
46
|
+
const Definitions: { [string]: Definition } = {}
|
|
47
|
+
const Queued: { [Definition]: { Cursor.Cursor } } = {}
|
|
48
|
+
const Waiting: { [Definition]: { thread } } = {}
|
|
49
|
+
const HeldBatches: { Held } = {}
|
|
50
|
+
const Scratch = Cursor.New()
|
|
51
|
+
const Incoming = Wire.NewBatch()
|
|
52
|
+
const NoInstances: { any } = {}
|
|
53
|
+
const ResponseFlag = Wire.ResponseFlag
|
|
54
|
+
const EventIndex = Wire.EventIndex
|
|
55
|
+
|
|
56
|
+
local ReliableRemote: RemoteEvent? = nil
|
|
57
|
+
local UnreliableRemote: UnreliableRemoteEvent? = nil
|
|
58
|
+
local UnreliableCount = 1
|
|
59
|
+
local NextCallIndex = 0
|
|
60
|
+
local Elapsed = 0
|
|
61
|
+
local RetryScheduled = false
|
|
62
|
+
|
|
63
|
+
const function Deliver(Definition: Definition, Id: number, Body: Cursor.Cursor)
|
|
64
|
+
if Definition.Reliable then
|
|
65
|
+
Wire.Deliver(Reliable, Id, nil, nil, Body)
|
|
66
|
+
|
|
67
|
+
return
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
const Size = Wire.Size(Id, nil, Body)
|
|
71
|
+
|
|
72
|
+
if Size > Configuration.UnreliableByteLimit then
|
|
73
|
+
error(
|
|
74
|
+
`[swiftpacket] {Definition.Name} is {Size} bytes, over the unreliable limit of {Configuration.UnreliableByteLimit}`,
|
|
75
|
+
0
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
local Segment = Unreliable[UnreliableCount]
|
|
80
|
+
|
|
81
|
+
if Segment.Offset > 0 and Segment.Offset + Size > Configuration.UnreliableByteLimit then
|
|
82
|
+
UnreliableCount += 1
|
|
83
|
+
|
|
84
|
+
const Next = Unreliable[UnreliableCount]
|
|
85
|
+
|
|
86
|
+
if Next then
|
|
87
|
+
Segment = Next
|
|
88
|
+
else
|
|
89
|
+
Segment = Cursor.New(Configuration.UnreliableByteLimit)
|
|
90
|
+
Unreliable[UnreliableCount] = Segment
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
Wire.Deliver(Segment, Id, nil, nil, Body)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
function Client.Send(Definition: Definition, ...: any)
|
|
98
|
+
const Id = Definition.Id
|
|
99
|
+
|
|
100
|
+
if Id == nil then
|
|
101
|
+
const Body = Cursor.New()
|
|
102
|
+
Schema.WriteBody(Body, Definition.Name, Definition.Arguments, ...)
|
|
103
|
+
|
|
104
|
+
const Bodies = Queued[Definition] or {}
|
|
105
|
+
table.insert(Bodies, Body)
|
|
106
|
+
Queued[Definition] = Bodies
|
|
107
|
+
|
|
108
|
+
return
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
if Definition.Reliable then
|
|
112
|
+
Schema.WriteEvent(Reliable, Id, Definition.Name, Definition.Arguments, ...)
|
|
113
|
+
|
|
114
|
+
return
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
Schema.WriteBody(Scratch, Definition.Name, Definition.Arguments, ...)
|
|
118
|
+
Deliver(Definition, Id, Scratch)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
const function FindCallIndex(): number
|
|
122
|
+
local CallIndex = NextCallIndex
|
|
123
|
+
|
|
124
|
+
for Attempt = 1, Configuration.CallCapacity do
|
|
125
|
+
if PendingCalls[CallIndex] == nil then
|
|
126
|
+
return CallIndex
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
CallIndex = (CallIndex + 1) % Configuration.CallCapacity
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
error("[swiftpacket] too many calls are waiting on a response", 0)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
function Client.Invoke(Definition: Definition, ...: any): ...any
|
|
136
|
+
local CallIndex = 0
|
|
137
|
+
|
|
138
|
+
if Definition.Id == nil then
|
|
139
|
+
const Body = Cursor.New()
|
|
140
|
+
Schema.WriteBody(Body, Definition.Name, Definition.Arguments, ...)
|
|
141
|
+
|
|
142
|
+
const Threads = Waiting[Definition] or {}
|
|
143
|
+
table.insert(Threads, coroutine.running())
|
|
144
|
+
Waiting[Definition] = Threads
|
|
145
|
+
|
|
146
|
+
coroutine.yield()
|
|
147
|
+
|
|
148
|
+
const Id = Definition.Id
|
|
149
|
+
|
|
150
|
+
if Id == nil then
|
|
151
|
+
error(`[swiftpacket] {Definition.Name} has no id`, 0)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
CallIndex = FindCallIndex()
|
|
155
|
+
Wire.Deliver(Reliable, Id, CallIndex, nil, Body)
|
|
156
|
+
else
|
|
157
|
+
CallIndex = FindCallIndex()
|
|
158
|
+
Schema.WriteRequest(Reliable, Definition.Id, CallIndex, Definition.Name, Definition.Arguments, ...)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
NextCallIndex = (CallIndex + 1) % Configuration.CallCapacity
|
|
162
|
+
PendingCalls[CallIndex] = {
|
|
163
|
+
Thread = coroutine.running(),
|
|
164
|
+
Deadline = os.clock() + Definition.TimeoutSeconds,
|
|
165
|
+
Definition = Definition,
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return coroutine.yield()
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
const function Respond(Definition: Definition, CallIndex: number, ...: any)
|
|
172
|
+
const Handler = Definition.OnClientInvoke
|
|
173
|
+
local Results: Arguments = { n = 0 }
|
|
174
|
+
|
|
175
|
+
if Handler then
|
|
176
|
+
Results = table.pack(pcall(Handler, ...))
|
|
177
|
+
else
|
|
178
|
+
Results = { n = 2, false, "no OnClientInvoke was set" }
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
const Id = Definition.Id
|
|
182
|
+
|
|
183
|
+
if Id == nil then
|
|
184
|
+
return
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
const Ok, Problem = Wire.EncodeResults(Scratch, Definition, Results)
|
|
188
|
+
|
|
189
|
+
if not Ok then
|
|
190
|
+
warn(`[swiftpacket] call from the server failed: {Problem}`)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
Wire.DeliverResponse(Reliable, Id, CallIndex, if Ok then Scratch else nil)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
const function Resolve(Definition: Definition, CallIndex: number, Success: boolean, ...: any)
|
|
197
|
+
const Call = PendingCalls[CallIndex]
|
|
198
|
+
|
|
199
|
+
if Call == nil or Call.Definition ~= Definition then
|
|
200
|
+
Configuration.Warn(`[swiftpacket] ignored an unexpected response to {Definition.Name}`)
|
|
201
|
+
|
|
202
|
+
return
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
PendingCalls[CallIndex] = nil
|
|
206
|
+
|
|
207
|
+
if Success then
|
|
208
|
+
task.defer(Call.Thread, ...)
|
|
209
|
+
else
|
|
210
|
+
task.defer(Call.Thread, Wire.Unpack(Definition.TimeoutValues))
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
const function Dispatch()
|
|
215
|
+
const Values = Incoming.Values
|
|
216
|
+
|
|
217
|
+
for Index = 1, Incoming.Count do
|
|
218
|
+
const Definition = Incoming.Definitions[Index]
|
|
219
|
+
const CallIndex = Incoming.CallIndices[Index]
|
|
220
|
+
const First = Incoming.Starts[Index]
|
|
221
|
+
const Last = First + Incoming.Counts[Index] - 1
|
|
222
|
+
|
|
223
|
+
if CallIndex >= ResponseFlag then
|
|
224
|
+
Resolve(Definition, CallIndex - ResponseFlag, Incoming.Successes[Index], table.unpack(Values, First, Last))
|
|
225
|
+
elseif CallIndex ~= EventIndex then
|
|
226
|
+
ThreadPool.Defer(Respond, Definition, CallIndex, table.unpack(Values, First, Last))
|
|
227
|
+
elseif First == Last then
|
|
228
|
+
Definition.OnClientEvent:Fire(Values[First])
|
|
229
|
+
else
|
|
230
|
+
Definition.OnClientEvent:Fire(table.unpack(Values, First, Last))
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
const function Process(Source: buffer, Attached: { any }, Offset: number, StartInstances: number): number?
|
|
236
|
+
const Ok, Result = pcall(Wire.Decode, Incoming, Source, Attached, false, Offset, StartInstances)
|
|
237
|
+
|
|
238
|
+
if not Ok then
|
|
239
|
+
Wire.Release(Incoming)
|
|
240
|
+
warn(`[swiftpacket] could not read a batch from the server: {tostring(Result)}`)
|
|
241
|
+
|
|
242
|
+
return nil
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
Dispatch()
|
|
246
|
+
Wire.Release(Incoming)
|
|
247
|
+
|
|
248
|
+
return Result
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
const function Retry()
|
|
252
|
+
RetryScheduled = false
|
|
253
|
+
|
|
254
|
+
local Index = 1
|
|
255
|
+
|
|
256
|
+
while Index <= #HeldBatches do
|
|
257
|
+
const Entry = HeldBatches[Index]
|
|
258
|
+
|
|
259
|
+
if Registry.ById[Entry.MissingId] == nil then
|
|
260
|
+
Index += 1
|
|
261
|
+
|
|
262
|
+
continue
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
const MissingId = Process(Entry.Source, Entry.Instances, Entry.Offset, Entry.InstancesOffset)
|
|
266
|
+
|
|
267
|
+
if MissingId then
|
|
268
|
+
Entry.Offset = Incoming.StopOffset
|
|
269
|
+
Entry.InstancesOffset = Incoming.StopInstances
|
|
270
|
+
Entry.MissingId = MissingId
|
|
271
|
+
Index += 1
|
|
272
|
+
else
|
|
273
|
+
table.remove(HeldBatches, Index)
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
const function ScheduleRetry()
|
|
279
|
+
if RetryScheduled or HeldBatches[1] == nil then
|
|
280
|
+
return
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
RetryScheduled = true
|
|
284
|
+
task.defer(Retry)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
const function Assign(Definition: Definition, Id: number)
|
|
288
|
+
const Previous = Definition.Id
|
|
289
|
+
|
|
290
|
+
if Previous and Registry.ById[Previous] == Definition then
|
|
291
|
+
Registry.ById[Previous] = nil
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
Definition.Id = Id
|
|
295
|
+
Registry.ById[Id] = Definition
|
|
296
|
+
|
|
297
|
+
const Bodies = Queued[Definition]
|
|
298
|
+
Queued[Definition] = nil
|
|
299
|
+
|
|
300
|
+
if Bodies then
|
|
301
|
+
for Index, Body in Bodies do
|
|
302
|
+
Deliver(Definition, Id, Body)
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
const Threads = Waiting[Definition]
|
|
307
|
+
Waiting[Definition] = nil
|
|
308
|
+
|
|
309
|
+
if Threads then
|
|
310
|
+
for Index, Thread in Threads do
|
|
311
|
+
task.spawn(Thread)
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
ScheduleRetry()
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
function Client.Register(Definition: Definition)
|
|
319
|
+
const Remote = ReliableRemote
|
|
320
|
+
|
|
321
|
+
if Remote == nil then
|
|
322
|
+
error("[swiftpacket] the client transport has not started", 0)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
Definitions[Definition.Name] = Definition
|
|
326
|
+
|
|
327
|
+
const Id = Remote:GetAttribute(Definition.Name)
|
|
328
|
+
|
|
329
|
+
if type(Id) == "number" then
|
|
330
|
+
Assign(Definition, Id)
|
|
331
|
+
|
|
332
|
+
return
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
task.delay(5, function()
|
|
336
|
+
if Definition.Id == nil then
|
|
337
|
+
warn(`[swiftpacket] {Definition.Name} has no id after 5 seconds, define it on the server too`)
|
|
338
|
+
end
|
|
339
|
+
end)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
const function Receive(Source: any, Instances: any)
|
|
343
|
+
if typeof(Source) ~= "buffer" then
|
|
344
|
+
return
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
const Attached = if type(Instances) == "table" then Instances else NoInstances
|
|
348
|
+
const MissingId = Process(Source, Attached, 0, 0)
|
|
349
|
+
|
|
350
|
+
if MissingId then
|
|
351
|
+
table.insert(HeldBatches, {
|
|
352
|
+
Source = Source,
|
|
353
|
+
Instances = Attached,
|
|
354
|
+
Offset = Incoming.StopOffset,
|
|
355
|
+
InstancesOffset = Incoming.StopInstances,
|
|
356
|
+
MissingId = MissingId,
|
|
357
|
+
ReceivedAt = os.clock(),
|
|
358
|
+
})
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
const function FireSegment(Remote: RemoteEvent | UnreliableRemoteEvent, Segment: Cursor.Cursor)
|
|
363
|
+
if Segment.Offset == 0 then
|
|
364
|
+
return
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
const Data, Attached = Cursor.Take(Segment)
|
|
368
|
+
Cursor.Reset(Segment)
|
|
369
|
+
|
|
370
|
+
if Remote:IsA("RemoteEvent") then
|
|
371
|
+
if Attached then
|
|
372
|
+
Remote:FireServer(Data, Attached)
|
|
373
|
+
else
|
|
374
|
+
Remote:FireServer(Data)
|
|
375
|
+
end
|
|
376
|
+
elseif Remote:IsA("UnreliableRemoteEvent") then
|
|
377
|
+
if Attached then
|
|
378
|
+
Remote:FireServer(Data, Attached)
|
|
379
|
+
else
|
|
380
|
+
Remote:FireServer(Data)
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
const function Flush()
|
|
386
|
+
const ReliableTarget = ReliableRemote
|
|
387
|
+
const UnreliableTarget = UnreliableRemote
|
|
388
|
+
|
|
389
|
+
if ReliableTarget == nil or UnreliableTarget == nil then
|
|
390
|
+
return
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
FireSegment(ReliableTarget, Reliable)
|
|
394
|
+
|
|
395
|
+
for Index = 1, UnreliableCount do
|
|
396
|
+
FireSegment(UnreliableTarget, Unreliable[Index])
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
UnreliableCount = 1
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
const function Expire()
|
|
403
|
+
const Now = os.clock()
|
|
404
|
+
|
|
405
|
+
for CallIndex, Call in PendingCalls do
|
|
406
|
+
if Now >= Call.Deadline then
|
|
407
|
+
PendingCalls[CallIndex] = nil
|
|
408
|
+
task.defer(Call.Thread, Wire.Unpack(Call.Definition.TimeoutValues))
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
for Index = #HeldBatches, 1, -1 do
|
|
413
|
+
const Entry = HeldBatches[Index]
|
|
414
|
+
|
|
415
|
+
if Now - Entry.ReceivedAt >= Configuration.HoldSeconds then
|
|
416
|
+
table.remove(HeldBatches, Index)
|
|
417
|
+
warn(
|
|
418
|
+
`[swiftpacket] dropped messages after the unknown packet id {Entry.MissingId}, define it on the client too`
|
|
419
|
+
)
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
const function FindRemote(Root: Instance, Name: string): Instance
|
|
425
|
+
const Remote = Root:WaitForChild(Name, 10)
|
|
426
|
+
|
|
427
|
+
if Remote == nil then
|
|
428
|
+
error(`[swiftpacket] could not find {Root:GetFullName()}.{Name}, require swiftpacket on the server first`, 0)
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
return Remote
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
function Client.Start(Root: Instance)
|
|
435
|
+
if ReliableRemote then
|
|
436
|
+
return
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
const ReliableInstance = FindRemote(Root, "Reliable")
|
|
440
|
+
const UnreliableInstance = FindRemote(Root, "Unreliable")
|
|
441
|
+
|
|
442
|
+
if not ReliableInstance:IsA("RemoteEvent") or not UnreliableInstance:IsA("UnreliableRemoteEvent") then
|
|
443
|
+
error(`[swiftpacket] the remotes under {Root:GetFullName()} have the wrong classes`, 0)
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
ReliableRemote = ReliableInstance
|
|
447
|
+
UnreliableRemote = UnreliableInstance
|
|
448
|
+
|
|
449
|
+
ReliableInstance.OnClientEvent:Connect(Receive)
|
|
450
|
+
UnreliableInstance.OnClientEvent:Connect(Receive)
|
|
451
|
+
|
|
452
|
+
ReliableInstance.AttributeChanged:Connect(function(Name: string)
|
|
453
|
+
const Definition = Definitions[Name]
|
|
454
|
+
const Id = ReliableInstance:GetAttribute(Name)
|
|
455
|
+
|
|
456
|
+
if Definition and type(Id) == "number" then
|
|
457
|
+
Assign(Definition, Id)
|
|
458
|
+
end
|
|
459
|
+
end)
|
|
460
|
+
|
|
461
|
+
RunService.Heartbeat:Connect(function(DeltaTime: number)
|
|
462
|
+
Expire()
|
|
463
|
+
Elapsed += DeltaTime
|
|
464
|
+
|
|
465
|
+
if Elapsed >= Configuration.FlushRate then
|
|
466
|
+
Elapsed %= Configuration.FlushRate
|
|
467
|
+
task.defer(Flush)
|
|
468
|
+
end
|
|
469
|
+
end)
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
return Client
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--!optimize 2
|
|
3
|
+
|
|
4
|
+
--[[
|
|
5
|
+
Author: @skaterstudios
|
|
6
|
+
Website: skaterstudios.com
|
|
7
|
+
GitHub: github.com/skatingii/SwiftPacket
|
|
8
|
+
Discord Server: discord.gg/skaterstudios
|
|
9
|
+
|
|
10
|
+
Licensed under the MIT License. You may use, modify, redistribute and sell
|
|
11
|
+
this system freely, as long as this notice and the LICENSE file stay with it.
|
|
12
|
+
]]
|
|
13
|
+
|
|
14
|
+
const RunService = game:GetService("RunService")
|
|
15
|
+
|
|
16
|
+
export type Options = {
|
|
17
|
+
Logging: boolean?,
|
|
18
|
+
IncomingBytesPerSecond: number?,
|
|
19
|
+
UnreliableByteLimit: number?,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const Configuration = {
|
|
23
|
+
Logging = RunService:IsStudio(),
|
|
24
|
+
IncomingBytesPerSecond = 64000,
|
|
25
|
+
UnreliableByteLimit = 900,
|
|
26
|
+
FlushRate = 1 / 60,
|
|
27
|
+
MessageCost = 8,
|
|
28
|
+
HoldSeconds = 10,
|
|
29
|
+
CallCapacity = 32768,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const OptionTypes: { [string]: string } = {
|
|
33
|
+
Logging = "boolean",
|
|
34
|
+
IncomingBytesPerSecond = "number",
|
|
35
|
+
UnreliableByteLimit = "number",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function Configuration.Configure(Options: { [string]: any })
|
|
39
|
+
for Key, Value in Options do
|
|
40
|
+
const Expected = OptionTypes[Key]
|
|
41
|
+
|
|
42
|
+
if Expected == nil then
|
|
43
|
+
error(`[swiftpacket] unknown option {Key}`, 2)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
if type(Value) ~= Expected then
|
|
47
|
+
error(`[swiftpacket] option {Key} must be a {Expected}, got {typeof(Value)}`, 2)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
const Logging = Options.Logging
|
|
52
|
+
const IncomingBytesPerSecond = Options.IncomingBytesPerSecond
|
|
53
|
+
const UnreliableByteLimit = Options.UnreliableByteLimit
|
|
54
|
+
|
|
55
|
+
if Logging ~= nil then
|
|
56
|
+
Configuration.Logging = Logging
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
if IncomingBytesPerSecond ~= nil then
|
|
60
|
+
Configuration.IncomingBytesPerSecond = math.max(IncomingBytesPerSecond, 1000)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
if UnreliableByteLimit ~= nil then
|
|
64
|
+
Configuration.UnreliableByteLimit = math.clamp(UnreliableByteLimit, 64, 900)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
function Configuration.Warn(Message: string)
|
|
69
|
+
if Configuration.Logging then
|
|
70
|
+
warn(Message)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
return Configuration
|