@axpecter/lync 1.5.1 → 2.0.0
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/README.md +105 -12
- package/package.json +2 -2
- package/src/Types.luau +10 -0
- package/src/api/Namespace.luau +8 -3
- package/src/api/Packet.luau +76 -43
- package/src/api/Query.luau +35 -14
- package/src/api/Signal.luau +4 -0
- package/src/codec/composite/Array.luau +85 -6
- package/src/codec/composite/Map.luau +7 -1
- package/src/codec/composite/Optional.luau +1 -0
- package/src/codec/composite/Struct.luau +23 -1
- package/src/codec/composite/Tagged.luau +11 -0
- package/src/codec/composite/Tuple.luau +10 -0
- package/src/codec/datatype/Instance.luau +3 -0
- package/src/codec/meta/Unknown.luau +1 -0
- package/src/index.d.ts +36 -7
- package/src/init.luau +49 -2
- package/src/internal/Channel.luau +172 -42
- package/src/internal/Registry.luau +20 -3
- package/src/transport/Client.luau +65 -11
- package/src/transport/Gate.luau +9 -0
- package/src/transport/Reader.luau +75 -24
- package/src/transport/Server.luau +273 -68
|
@@ -17,23 +17,35 @@ local bxor = bit32.bxor
|
|
|
17
17
|
local band32 = bit32.band
|
|
18
18
|
local countlz = bit32.countlz
|
|
19
19
|
local lshift = bit32.lshift
|
|
20
|
+
local bor = bit32.bor
|
|
21
|
+
local rshift = bit32.rshift
|
|
22
|
+
|
|
23
|
+
local TS_NONE = 0
|
|
24
|
+
local TS_FRAME = 1
|
|
25
|
+
local TS_OFFSET = 2
|
|
26
|
+
local TS_FULL = 3
|
|
20
27
|
|
|
21
28
|
-- State ---------------------------------------------------------------
|
|
22
29
|
|
|
23
30
|
local _maxSize = MAX_SIZE
|
|
24
|
-
|
|
25
|
-
-- Packet name for overflow diagnostics. Not coroutine-safe: concurrent
|
|
26
|
-
-- serializers may overwrite before the error fires.
|
|
27
31
|
local _currentPacket = ""
|
|
28
32
|
|
|
33
|
+
-- Timestamp state updated once per flush via updateTimestamp().
|
|
34
|
+
local _frameCounter: number = 0
|
|
35
|
+
local _offsetMs: number = 0
|
|
36
|
+
local _clockTime: number = 0
|
|
37
|
+
local _anyTimestamp = false
|
|
38
|
+
|
|
39
|
+
-- Stats gating. Default off; set via Lync.enableStats().
|
|
40
|
+
local _statsEnabled = false
|
|
41
|
+
|
|
29
42
|
-- Public --------------------------------------------------------------
|
|
30
43
|
|
|
31
44
|
local Channel = {}
|
|
32
45
|
|
|
33
|
-
-- Override the maximum buffer size. Call before start(). Range: 4096–1048576.
|
|
34
46
|
function Channel.setMaxSize(bytes: number): ()
|
|
35
47
|
if bytes < 4096 or bytes > 1048576 then
|
|
36
|
-
error(`[Lync] Channel max size must be 4096
|
|
48
|
+
error(`[Lync] Channel max size must be 4096-1048576, got {bytes}`)
|
|
37
49
|
end
|
|
38
50
|
_maxSize = bytes
|
|
39
51
|
end
|
|
@@ -56,7 +68,6 @@ function Channel.create(): ChannelState
|
|
|
56
68
|
}
|
|
57
69
|
end
|
|
58
70
|
|
|
59
|
-
-- Grows the channel buffer to fit the requested bytes. Power-of-two sizing.
|
|
60
71
|
function Channel.alloc(ch: ChannelState, bytes: number): ()
|
|
61
72
|
local needed = ch.cursor + bytes
|
|
62
73
|
if needed <= ch.size then
|
|
@@ -95,52 +106,126 @@ function Channel.sealAndDump(ch: ChannelState): (buffer, { Instance })
|
|
|
95
106
|
return out, if #refs > 0 then table.clone(refs) else {}
|
|
96
107
|
end
|
|
97
108
|
|
|
98
|
-
-- Sets the packet name for overflow diagnostics.
|
|
99
109
|
function Channel.setCurrentPacket(name: string): ()
|
|
100
110
|
_currentPacket = name
|
|
101
111
|
end
|
|
102
112
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
end
|
|
113
|
+
function Channel.updateTimestamp(frameCounter: number, offsetMs: number, clockTime: number): ()
|
|
114
|
+
_frameCounter = frameCounter
|
|
115
|
+
_offsetMs = offsetMs
|
|
116
|
+
_clockTime = clockTime
|
|
117
|
+
end
|
|
109
118
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
119
|
+
function Channel.enableTimestamps(): ()
|
|
120
|
+
_anyTimestamp = true
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
function Channel.hasTimestamps(): boolean
|
|
124
|
+
return _anyTimestamp
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
function Channel.enableStats(): ()
|
|
128
|
+
_statsEnabled = true
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
function Channel.statsEnabled(): boolean
|
|
132
|
+
return _statsEnabled
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
-- Batch opener: no timestamp variant. Resolved at define time via reg._openFn (#2)
|
|
136
|
+
local function openBatchPlain(ch: ChannelState, id: number, name: string): ()
|
|
137
|
+
if ch.lastId >= 0 then
|
|
138
|
+
buffer.writeu16(ch.buff, ch.countPos, ch.itemCount)
|
|
139
|
+
end
|
|
140
|
+
_currentPacket = name
|
|
141
|
+
alloc(ch, 3)
|
|
142
|
+
local c = ch.cursor
|
|
143
|
+
buffer.writeu8(ch.buff, c, id)
|
|
144
|
+
ch.countPos = c + 1
|
|
145
|
+
ch.cursor = c + 3
|
|
146
|
+
ch.lastId = id
|
|
147
|
+
ch.itemCount = 0
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
local function openBatchFrame(ch: ChannelState, id: number, name: string): ()
|
|
151
|
+
if ch.lastId >= 0 then
|
|
152
|
+
buffer.writeu16(ch.buff, ch.countPos, ch.itemCount)
|
|
153
|
+
end
|
|
154
|
+
_currentPacket = name
|
|
155
|
+
alloc(ch, 4)
|
|
156
|
+
local c = ch.cursor
|
|
157
|
+
buffer.writeu8(ch.buff, c, id)
|
|
158
|
+
ch.countPos = c + 1
|
|
159
|
+
buffer.writeu8(ch.buff, c + 3, _frameCounter)
|
|
160
|
+
ch.cursor = c + 4
|
|
161
|
+
ch.lastId = id
|
|
162
|
+
ch.itemCount = 0
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
local function openBatchOffset(ch: ChannelState, id: number, name: string): ()
|
|
166
|
+
if ch.lastId >= 0 then
|
|
167
|
+
buffer.writeu16(ch.buff, ch.countPos, ch.itemCount)
|
|
168
|
+
end
|
|
169
|
+
_currentPacket = name
|
|
170
|
+
alloc(ch, 5)
|
|
171
|
+
local c = ch.cursor
|
|
172
|
+
buffer.writeu8(ch.buff, c, id)
|
|
173
|
+
ch.countPos = c + 1
|
|
174
|
+
buffer.writeu16(ch.buff, c + 3, _offsetMs)
|
|
175
|
+
ch.cursor = c + 5
|
|
176
|
+
ch.lastId = id
|
|
177
|
+
ch.itemCount = 0
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
local function openBatchFull(ch: ChannelState, id: number, name: string): ()
|
|
181
|
+
if ch.lastId >= 0 then
|
|
182
|
+
buffer.writeu16(ch.buff, ch.countPos, ch.itemCount)
|
|
183
|
+
end
|
|
184
|
+
_currentPacket = name
|
|
185
|
+
alloc(ch, 11)
|
|
186
|
+
local c = ch.cursor
|
|
187
|
+
buffer.writeu8(ch.buff, c, id)
|
|
188
|
+
ch.countPos = c + 1
|
|
189
|
+
buffer.writef64(ch.buff, c + 3, _clockTime)
|
|
190
|
+
ch.cursor = c + 11
|
|
191
|
+
ch.lastId = id
|
|
192
|
+
ch.itemCount = 0
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
-- Returns the correct openBatch function for a given timestamp mode.
|
|
196
|
+
function Channel.resolveOpenFn(tsMode: number): (ch: ChannelState, id: number, name: string) -> ()
|
|
197
|
+
if tsMode == TS_FRAME then
|
|
198
|
+
return openBatchFrame
|
|
199
|
+
elseif tsMode == TS_OFFSET then
|
|
200
|
+
return openBatchOffset
|
|
201
|
+
elseif tsMode == TS_FULL then
|
|
202
|
+
return openBatchFull
|
|
203
|
+
end
|
|
204
|
+
return openBatchPlain
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
-- Writes a single value into the batch. Zero branching on the hot path.
|
|
208
|
+
function Channel.writeBatch(ch: ChannelState, reg: any, data: any): ()
|
|
209
|
+
local id = reg.id
|
|
210
|
+
if id ~= ch.lastId or ch.itemCount >= MAX_BATCH then
|
|
211
|
+
reg._openFn(ch, id, reg.name)
|
|
118
212
|
end
|
|
119
213
|
|
|
120
214
|
ch.itemCount += 1
|
|
121
|
-
codec.write(ch, data)
|
|
215
|
+
reg.codec.write(ch, data)
|
|
122
216
|
end
|
|
123
217
|
|
|
124
218
|
-- Appends pre-serialized bytes into a batch. Used by broadcast fan-out.
|
|
125
219
|
function Channel.writeBatchRaw(
|
|
126
220
|
ch: ChannelState,
|
|
127
|
-
|
|
221
|
+
reg: any,
|
|
128
222
|
src: buffer,
|
|
129
223
|
srcLen: number,
|
|
130
224
|
srcRefs: { Instance }?
|
|
131
225
|
): ()
|
|
226
|
+
local id = reg.id
|
|
132
227
|
if id ~= ch.lastId or ch.itemCount >= MAX_BATCH then
|
|
133
|
-
|
|
134
|
-
buffer.writeu16(ch.buff, ch.countPos, ch.itemCount)
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
alloc(ch, 3)
|
|
138
|
-
local c = ch.cursor
|
|
139
|
-
buffer.writeu8(ch.buff, c, id)
|
|
140
|
-
ch.countPos = c + 1
|
|
141
|
-
ch.cursor = c + 3
|
|
142
|
-
ch.lastId = id
|
|
143
|
-
ch.itemCount = 0
|
|
228
|
+
reg._openFn(ch, id, reg.name)
|
|
144
229
|
end
|
|
145
230
|
|
|
146
231
|
ch.itemCount += 1
|
|
@@ -168,7 +253,7 @@ end
|
|
|
168
253
|
|
|
169
254
|
--[[
|
|
170
255
|
Writes a query frame. Seals any open batch, then writes:
|
|
171
|
-
id(u8) + correlationId(
|
|
256
|
+
id(u8) + correlationId(varint) + status(u8)
|
|
172
257
|
Status 0 = payload follows. Status 1 = nil response.
|
|
173
258
|
]]
|
|
174
259
|
function Channel.writeQuery(
|
|
@@ -182,18 +267,39 @@ function Channel.writeQuery(
|
|
|
182
267
|
_currentPacket = name
|
|
183
268
|
sealOpen(ch)
|
|
184
269
|
|
|
185
|
-
alloc(ch,
|
|
270
|
+
alloc(ch, 2)
|
|
186
271
|
local c = ch.cursor
|
|
187
272
|
buffer.writeu8(ch.buff, c, id)
|
|
188
|
-
|
|
273
|
+
ch.cursor = c + 1
|
|
274
|
+
|
|
275
|
+
-- Inline varint write to avoid circular dependency (Varint requires Channel)
|
|
276
|
+
local v = correlationId
|
|
277
|
+
if v < 0x80 then
|
|
278
|
+
alloc(ch, 1)
|
|
279
|
+
buffer.writeu8(ch.buff, ch.cursor, v)
|
|
280
|
+
ch.cursor += 1
|
|
281
|
+
else
|
|
282
|
+
alloc(ch, 5)
|
|
283
|
+
local b = ch.buff
|
|
284
|
+
local vc = ch.cursor
|
|
285
|
+
while v >= 0x80 do
|
|
286
|
+
buffer.writeu8(b, vc, bor(band32(v, 0x7F), 0x80))
|
|
287
|
+
v = rshift(v, 7)
|
|
288
|
+
vc += 1
|
|
289
|
+
end
|
|
290
|
+
buffer.writeu8(b, vc, v)
|
|
291
|
+
ch.cursor = vc + 1
|
|
292
|
+
end
|
|
189
293
|
|
|
294
|
+
local c2 = ch.cursor
|
|
295
|
+
alloc(ch, 1)
|
|
190
296
|
if codec then
|
|
191
|
-
buffer.writeu8(ch.buff,
|
|
192
|
-
ch.cursor =
|
|
297
|
+
buffer.writeu8(ch.buff, c2, 0)
|
|
298
|
+
ch.cursor = c2 + 1
|
|
193
299
|
codec.write(ch, data)
|
|
194
300
|
else
|
|
195
|
-
buffer.writeu8(ch.buff,
|
|
196
|
-
ch.cursor =
|
|
301
|
+
buffer.writeu8(ch.buff, c2, 1)
|
|
302
|
+
ch.cursor = c2 + 1
|
|
197
303
|
end
|
|
198
304
|
end
|
|
199
305
|
|
|
@@ -229,12 +335,36 @@ function Channel.xorApply(current: buffer, previous: buffer?): buffer
|
|
|
229
335
|
return result
|
|
230
336
|
end
|
|
231
337
|
|
|
338
|
+
-- Conditional refs clear (#6)
|
|
232
339
|
function Channel.reset(ch: ChannelState): ()
|
|
233
340
|
ch.cursor = 0
|
|
234
341
|
ch.lastId = -1
|
|
235
342
|
ch.countPos = 0
|
|
236
343
|
ch.itemCount = 0
|
|
237
|
-
|
|
344
|
+
if #ch.refs > 0 then
|
|
345
|
+
table.clear(ch.refs)
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
-- Test helper: builds a minimal reg-like object for writeBatch.
|
|
350
|
+
function Channel.mockReg(id: number, name: string, codec: any): any
|
|
351
|
+
return {
|
|
352
|
+
id = id,
|
|
353
|
+
name = name,
|
|
354
|
+
codec = codec,
|
|
355
|
+
timestampMode = 0,
|
|
356
|
+
_openFn = openBatchPlain,
|
|
357
|
+
drops = 0,
|
|
358
|
+
bytesSent = 0,
|
|
359
|
+
bytesReceived = 0,
|
|
360
|
+
fires = 0,
|
|
361
|
+
recvFires = 0,
|
|
362
|
+
}
|
|
238
363
|
end
|
|
239
364
|
|
|
365
|
+
Channel.TS_NONE = TS_NONE
|
|
366
|
+
Channel.TS_FRAME = TS_FRAME
|
|
367
|
+
Channel.TS_OFFSET = TS_OFFSET
|
|
368
|
+
Channel.TS_FULL = TS_FULL
|
|
369
|
+
|
|
240
370
|
return table.freeze(Channel)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
--!optimize 2
|
|
3
3
|
-- Deterministic ID assignment for packets and queries.
|
|
4
4
|
|
|
5
|
+
local Channel = require(script.Parent.Channel)
|
|
5
6
|
local Types = require(script.Parent.Parent.Types)
|
|
6
7
|
|
|
7
8
|
type Codec<T> = Types.Codec<T>
|
|
@@ -32,7 +33,8 @@ local function assign(
|
|
|
32
33
|
signal: any,
|
|
33
34
|
rateLimit: RateLimitConfig?,
|
|
34
35
|
validate: ((data: any, player: Player) -> (boolean, string?))?,
|
|
35
|
-
maxPayloadBytes: number
|
|
36
|
+
maxPayloadBytes: number?,
|
|
37
|
+
timestampMode: number?
|
|
36
38
|
): Registration
|
|
37
39
|
if _names[name] then
|
|
38
40
|
error(`[Lync] Duplicate registration: "{name}"`)
|
|
@@ -70,6 +72,13 @@ local function assign(
|
|
|
70
72
|
validate = validate,
|
|
71
73
|
needsGate = rateLimit ~= nil or validate ~= nil,
|
|
72
74
|
maxPayloadBytes = maxPayloadBytes,
|
|
75
|
+
timestampMode = timestampMode or 0,
|
|
76
|
+
_openFn = Channel.resolveOpenFn(timestampMode or 0),
|
|
77
|
+
bytesSent = 0,
|
|
78
|
+
bytesReceived = 0,
|
|
79
|
+
fires = 0,
|
|
80
|
+
recvFires = 0,
|
|
81
|
+
drops = 0,
|
|
73
82
|
}
|
|
74
83
|
|
|
75
84
|
_entries[id] = reg
|
|
@@ -92,7 +101,8 @@ function Registry.register(
|
|
|
92
101
|
signal: any,
|
|
93
102
|
rateLimit: RateLimitConfig?,
|
|
94
103
|
validate: ((data: any, player: Player) -> (boolean, string?))?,
|
|
95
|
-
maxPayloadBytes: number
|
|
104
|
+
maxPayloadBytes: number?,
|
|
105
|
+
timestampMode: number?
|
|
96
106
|
): Registration
|
|
97
107
|
return assign(
|
|
98
108
|
name,
|
|
@@ -103,7 +113,8 @@ function Registry.register(
|
|
|
103
113
|
signal,
|
|
104
114
|
rateLimit,
|
|
105
115
|
validate,
|
|
106
|
-
maxPayloadBytes
|
|
116
|
+
maxPayloadBytes,
|
|
117
|
+
timestampMode
|
|
107
118
|
)
|
|
108
119
|
end
|
|
109
120
|
|
|
@@ -143,4 +154,10 @@ function Registry.getByName(name: string): Registration?
|
|
|
143
154
|
return if id then _entries[id] else nil
|
|
144
155
|
end
|
|
145
156
|
|
|
157
|
+
function Registry.forEach(fn: (reg: Registration) -> ()): ()
|
|
158
|
+
for _, reg in _entries do
|
|
159
|
+
fn(reg)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
146
163
|
return table.freeze(Registry)
|
|
@@ -12,6 +12,7 @@ local Types = require(script.Parent.Parent.Types)
|
|
|
12
12
|
|
|
13
13
|
type ChannelState = Types.ChannelState
|
|
14
14
|
type Codec<T> = Types.Codec<T>
|
|
15
|
+
type Registration = Types.Registration
|
|
15
16
|
|
|
16
17
|
-- State ---------------------------------------------------------------
|
|
17
18
|
|
|
@@ -22,6 +23,16 @@ local _unreliable = nil :: ChannelState?
|
|
|
22
23
|
local _prevRecv = nil :: buffer?
|
|
23
24
|
local _isStarted = false
|
|
24
25
|
|
|
26
|
+
-- Flush control
|
|
27
|
+
local _flushRate = 60
|
|
28
|
+
local _flushInterval = 1 / 60
|
|
29
|
+
local _useAccumulator = false
|
|
30
|
+
local _flushAccum = 0
|
|
31
|
+
local _flushedThisFrame = false
|
|
32
|
+
|
|
33
|
+
-- Timestamp
|
|
34
|
+
local _frameCounter: number = 0
|
|
35
|
+
|
|
25
36
|
-- Private -------------------------------------------------------------
|
|
26
37
|
|
|
27
38
|
local function receive(data: any, refs: any, applyXor: boolean): ()
|
|
@@ -64,6 +75,11 @@ local function flushChannel(remote: any, ch: ChannelState, applyXor: boolean): (
|
|
|
64
75
|
end
|
|
65
76
|
|
|
66
77
|
local function flush(): ()
|
|
78
|
+
if Channel.hasTimestamps() then
|
|
79
|
+
_frameCounter = (_frameCounter + 1) % 256
|
|
80
|
+
Channel.updateTimestamp(_frameCounter, 0, os.clock())
|
|
81
|
+
end
|
|
82
|
+
|
|
67
83
|
flushChannel(Bridge.reliable, _reliable :: ChannelState, true)
|
|
68
84
|
flushChannel(Bridge.unreliable, _unreliable :: ChannelState, false)
|
|
69
85
|
end
|
|
@@ -89,23 +105,39 @@ function Client.start(): ()
|
|
|
89
105
|
Bridge.unreliable.OnClientEvent:Connect(function(data: any, refs: any): ()
|
|
90
106
|
receive(data, refs, false)
|
|
91
107
|
end)
|
|
92
|
-
|
|
108
|
+
|
|
109
|
+
if _useAccumulator then
|
|
110
|
+
RunService.Heartbeat:Connect(function(dt: number)
|
|
111
|
+
if _flushedThisFrame then
|
|
112
|
+
_flushedThisFrame = false
|
|
113
|
+
return
|
|
114
|
+
end
|
|
115
|
+
_flushAccum += dt
|
|
116
|
+
if _flushAccum < _flushInterval then
|
|
117
|
+
return
|
|
118
|
+
end
|
|
119
|
+
_flushAccum -= _flushInterval
|
|
120
|
+
flush()
|
|
121
|
+
end)
|
|
122
|
+
else
|
|
123
|
+
RunService.Heartbeat:Connect(function()
|
|
124
|
+
if _flushedThisFrame then
|
|
125
|
+
_flushedThisFrame = false
|
|
126
|
+
return
|
|
127
|
+
end
|
|
128
|
+
flush()
|
|
129
|
+
end)
|
|
130
|
+
end
|
|
93
131
|
end
|
|
94
132
|
|
|
95
|
-
function Client.write(
|
|
96
|
-
id: number,
|
|
97
|
-
name: string,
|
|
98
|
-
codec: Codec<any>,
|
|
99
|
-
data: any,
|
|
100
|
-
isUnreliable: boolean
|
|
101
|
-
): ()
|
|
133
|
+
function Client.write(reg: Registration, data: any, isUnreliable: boolean): ()
|
|
102
134
|
if not _isStarted then
|
|
103
135
|
error(NOT_STARTED)
|
|
104
136
|
end
|
|
105
137
|
|
|
106
138
|
local final: any
|
|
107
139
|
if Middleware.hasSend then
|
|
108
|
-
final = Middleware.runSend(data, name, nil)
|
|
140
|
+
final = Middleware.runSend(data, reg.name, nil)
|
|
109
141
|
if final == nil then
|
|
110
142
|
return
|
|
111
143
|
end
|
|
@@ -114,10 +146,16 @@ function Client.write(
|
|
|
114
146
|
end
|
|
115
147
|
|
|
116
148
|
local ch = if isUnreliable then _unreliable :: ChannelState else _reliable :: ChannelState
|
|
117
|
-
Channel.
|
|
149
|
+
if Channel.statsEnabled() then
|
|
150
|
+
local before = ch.cursor
|
|
151
|
+
Channel.writeBatch(ch, reg, final)
|
|
152
|
+
reg.bytesSent += ch.cursor - before
|
|
153
|
+
reg.fires += 1
|
|
154
|
+
else
|
|
155
|
+
Channel.writeBatch(ch, reg, final)
|
|
156
|
+
end
|
|
118
157
|
end
|
|
119
158
|
|
|
120
|
-
-- Shared for both query requests and responses(identical wire format).
|
|
121
159
|
function Client.writeQuery(
|
|
122
160
|
id: number,
|
|
123
161
|
name: string,
|
|
@@ -149,4 +187,20 @@ function Client.writeQueryNil(id: number, name: string, correlationId: number):
|
|
|
149
187
|
Channel.writeQuery(_reliable :: ChannelState, id, name, correlationId, nil, nil)
|
|
150
188
|
end
|
|
151
189
|
|
|
190
|
+
function Client.flush(): ()
|
|
191
|
+
_flushAccum = 0
|
|
192
|
+
_flushedThisFrame = true
|
|
193
|
+
flush()
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
function Client.setFlushRate(hz: number): ()
|
|
197
|
+
_flushRate = math.clamp(hz, 1, 60)
|
|
198
|
+
_flushInterval = 1 / _flushRate
|
|
199
|
+
_useAccumulator = _flushRate < 60
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
function Client.getFlushRate(): number
|
|
203
|
+
return _flushRate
|
|
204
|
+
end
|
|
205
|
+
|
|
152
206
|
return table.freeze(Client)
|
package/src/transport/Gate.luau
CHANGED
|
@@ -162,11 +162,17 @@ end
|
|
|
162
162
|
|
|
163
163
|
function Gate.check(value: any, reg: Registration, player: Player): boolean
|
|
164
164
|
if not checkRate(player, reg) then
|
|
165
|
+
if reg.drops then
|
|
166
|
+
reg.drops += 1
|
|
167
|
+
end
|
|
165
168
|
fireDrop(player, "rate", reg.name, nil)
|
|
166
169
|
return false
|
|
167
170
|
end
|
|
168
171
|
|
|
169
172
|
if not isClean(value, 1) then
|
|
173
|
+
if reg.drops then
|
|
174
|
+
reg.drops += 1
|
|
175
|
+
end
|
|
170
176
|
fireDrop(player, "nan", reg.name, value)
|
|
171
177
|
return false
|
|
172
178
|
end
|
|
@@ -175,6 +181,9 @@ function Gate.check(value: any, reg: Registration, player: Player): boolean
|
|
|
175
181
|
if validate then
|
|
176
182
|
local ok, reason = validate(value, player)
|
|
177
183
|
if not ok then
|
|
184
|
+
if reg.drops then
|
|
185
|
+
reg.drops += 1
|
|
186
|
+
end
|
|
178
187
|
fireDrop(player, reason or "validate", reg.name, value)
|
|
179
188
|
return false
|
|
180
189
|
end
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
-- Deserializes incoming buffers and routes by registration kind.
|
|
4
4
|
|
|
5
5
|
local Baseline = require(script.Parent.Parent.internal.Baseline)
|
|
6
|
+
local Channel = require(script.Parent.Parent.internal.Channel)
|
|
6
7
|
local Gate = require(script.Parent.Gate)
|
|
7
8
|
local Middleware = require(script.Parent.Parent.internal.Middleware)
|
|
8
9
|
local Registry = require(script.Parent.Parent.internal.Registry)
|
|
10
|
+
local Varint = require(script.Parent.Parent.codec.primitive.Varint)
|
|
9
11
|
|
|
10
12
|
local registryGet = Registry.get
|
|
11
13
|
local gateCheck = Gate.check
|
|
@@ -16,8 +18,14 @@ local gateReportDrop = Gate.reportDrop
|
|
|
16
18
|
local KIND_PACKET = Registry.KIND_PACKET
|
|
17
19
|
local KIND_REQUEST = Registry.KIND_REQUEST
|
|
18
20
|
|
|
21
|
+
local TS_FRAME = Channel.TS_FRAME
|
|
22
|
+
local TS_OFFSET = Channel.TS_OFFSET
|
|
23
|
+
local TS_FULL = Channel.TS_FULL
|
|
24
|
+
|
|
19
25
|
local MAX_INCOMING = 65536
|
|
20
26
|
|
|
27
|
+
local floor = math.floor
|
|
28
|
+
|
|
21
29
|
-- Public --------------------------------------------------------------
|
|
22
30
|
|
|
23
31
|
local Reader = {}
|
|
@@ -47,9 +55,9 @@ function Reader.process(
|
|
|
47
55
|
Baseline.setReadKey(player or false)
|
|
48
56
|
end
|
|
49
57
|
|
|
50
|
-
-- Cache flags once per frame-buffer, not per packet
|
|
51
58
|
local hasReceiveMiddleware = Middleware.hasReceive
|
|
52
59
|
local isServer = player ~= nil
|
|
60
|
+
local statsOn = Channel.statsEnabled()
|
|
53
61
|
|
|
54
62
|
while pos < length do
|
|
55
63
|
local id = buffer.readu8(incoming, pos)
|
|
@@ -80,21 +88,62 @@ function Reader.process(
|
|
|
80
88
|
local batchStart = pos
|
|
81
89
|
local maxBytes = reg.maxPayloadBytes
|
|
82
90
|
|
|
91
|
+
-- Timestamp is per-batch, read once after count
|
|
92
|
+
local timestamp: number? = nil
|
|
93
|
+
local tsMode = reg.timestampMode
|
|
94
|
+
if tsMode == TS_FRAME then
|
|
95
|
+
timestamp = buffer.readu8(incoming, pos)
|
|
96
|
+
pos += 1
|
|
97
|
+
elseif tsMode == TS_OFFSET then
|
|
98
|
+
timestamp = buffer.readu16(incoming, pos)
|
|
99
|
+
pos += 2
|
|
100
|
+
elseif tsMode == TS_FULL then
|
|
101
|
+
timestamp = buffer.readf64(incoming, pos)
|
|
102
|
+
pos += 8
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
-- Cap count for fixed-size codecs to prevent wasted iterations
|
|
106
|
+
local fixedSize = (codec :: any)._size
|
|
107
|
+
if fixedSize and fixedSize > 0 then
|
|
108
|
+
local maxItems = floor((length - pos) / fixedSize)
|
|
109
|
+
if count > maxItems then
|
|
110
|
+
count = maxItems
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
83
114
|
-- Hot path: no gate, no middleware, no byte budget
|
|
84
115
|
if not (isServer and reg.needsGate) and not hasReceiveMiddleware and not maxBytes then
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
116
|
+
if statsOn then
|
|
117
|
+
local batchBytes = 0
|
|
118
|
+
for _ = 1, count do
|
|
119
|
+
if pos >= length then
|
|
120
|
+
break
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
local value, consumed = codec.read(incoming, pos, refs)
|
|
124
|
+
pos += consumed
|
|
125
|
+
batchBytes += consumed
|
|
126
|
+
signal:fire(value, player, timestamp)
|
|
88
127
|
end
|
|
128
|
+
reg.bytesReceived += batchBytes
|
|
129
|
+
reg.recvFires += count
|
|
130
|
+
else
|
|
131
|
+
for _ = 1, count do
|
|
132
|
+
if pos >= length then
|
|
133
|
+
break
|
|
134
|
+
end
|
|
89
135
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
136
|
+
local value, consumed = codec.read(incoming, pos, refs)
|
|
137
|
+
pos += consumed
|
|
138
|
+
signal:fire(value, player, timestamp)
|
|
139
|
+
end
|
|
93
140
|
end
|
|
94
141
|
else
|
|
95
142
|
-- Cold path: gate and/or middleware and/or byte budget active
|
|
96
143
|
local baseName = reg.baseName
|
|
97
144
|
local needsGate = isServer and reg.needsGate
|
|
145
|
+
local batchBytes = if statsOn then 0 else nil
|
|
146
|
+
local recvCount = if statsOn then 0 else nil
|
|
98
147
|
|
|
99
148
|
for _ = 1, count do
|
|
100
149
|
if pos >= length then
|
|
@@ -109,6 +158,11 @@ function Reader.process(
|
|
|
109
158
|
local value, consumed = codec.read(incoming, pos, refs)
|
|
110
159
|
pos += consumed
|
|
111
160
|
|
|
161
|
+
if batchBytes then
|
|
162
|
+
batchBytes += consumed
|
|
163
|
+
recvCount += 1
|
|
164
|
+
end
|
|
165
|
+
|
|
112
166
|
if needsGate and not gateCheck(value, reg, player :: Player) then
|
|
113
167
|
continue
|
|
114
168
|
end
|
|
@@ -118,31 +172,32 @@ function Reader.process(
|
|
|
118
172
|
if final == nil then
|
|
119
173
|
continue
|
|
120
174
|
end
|
|
121
|
-
signal:fire(final, player)
|
|
175
|
+
signal:fire(final, player, timestamp)
|
|
122
176
|
else
|
|
123
|
-
signal:fire(value, player)
|
|
177
|
+
signal:fire(value, player, timestamp)
|
|
124
178
|
end
|
|
125
179
|
end
|
|
180
|
+
if batchBytes then
|
|
181
|
+
reg.bytesReceived += batchBytes
|
|
182
|
+
reg.recvFires += recvCount :: number
|
|
183
|
+
end
|
|
126
184
|
end
|
|
127
185
|
else
|
|
128
|
-
-- Query frame: id(u8, already read) + correlationId(
|
|
129
|
-
if pos +
|
|
186
|
+
-- Query frame: id(u8, already read) + correlationId(varint) + status(u8)
|
|
187
|
+
if pos + 2 > length then
|
|
130
188
|
break
|
|
131
189
|
end
|
|
132
190
|
|
|
133
191
|
local queryCodec = reg.codec
|
|
134
192
|
local querySignal = reg.signal
|
|
135
193
|
|
|
136
|
-
local correlationId =
|
|
137
|
-
|
|
138
|
-
|
|
194
|
+
local correlationId, corrBytes = Varint.read(incoming, pos)
|
|
195
|
+
pos += corrBytes
|
|
196
|
+
local status = buffer.readu8(incoming, pos)
|
|
197
|
+
pos += 1
|
|
139
198
|
|
|
140
199
|
if status == 1 then
|
|
141
|
-
|
|
142
|
-
querySignal:fire(nil, player, correlationId)
|
|
143
|
-
else
|
|
144
|
-
querySignal:fire(nil, correlationId)
|
|
145
|
-
end
|
|
200
|
+
querySignal:fire(nil, player, correlationId)
|
|
146
201
|
continue
|
|
147
202
|
end
|
|
148
203
|
|
|
@@ -163,11 +218,7 @@ function Reader.process(
|
|
|
163
218
|
final = value
|
|
164
219
|
end
|
|
165
220
|
|
|
166
|
-
|
|
167
|
-
querySignal:fire(final, player, correlationId)
|
|
168
|
-
else
|
|
169
|
-
querySignal:fire(final, correlationId)
|
|
170
|
-
end
|
|
221
|
+
querySignal:fire(final, player, correlationId)
|
|
171
222
|
end
|
|
172
223
|
end
|
|
173
224
|
end
|