@axpecter/lync 1.5.2 → 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 +102 -9
- 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 +3 -1
- 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
|
@@ -47,6 +47,14 @@ function Struct.struct(schema: { [string]: Codec<any> }): Codec<any>
|
|
|
47
47
|
local boolByteCount = ceil(boolCount / 8)
|
|
48
48
|
local hasBools = boolCount > 0
|
|
49
49
|
|
|
50
|
+
local hasUnknown = false
|
|
51
|
+
for _, fieldCodec in schema do
|
|
52
|
+
if (fieldCodec :: any)._hasUnknown then
|
|
53
|
+
hasUnknown = true
|
|
54
|
+
break
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
50
58
|
local canDirect = true
|
|
51
59
|
local fixedSize = boolByteCount
|
|
52
60
|
local isAllFixed = true
|
|
@@ -126,6 +134,7 @@ function Struct.struct(schema: { [string]: Codec<any> }): Codec<any>
|
|
|
126
134
|
_size = fixedSize,
|
|
127
135
|
_directWrite = directWriteBody,
|
|
128
136
|
_directRead = directReadBody,
|
|
137
|
+
_hasUnknown = hasUnknown or nil,
|
|
129
138
|
|
|
130
139
|
write = function(ch: ChannelState, value: any): ()
|
|
131
140
|
local c = ch.cursor
|
|
@@ -152,6 +161,7 @@ function Struct.struct(schema: { [string]: Codec<any> }): Codec<any>
|
|
|
152
161
|
|
|
153
162
|
return table.freeze({
|
|
154
163
|
_size = if isAllFixed then fixedSize else nil,
|
|
164
|
+
_hasUnknown = hasUnknown or nil,
|
|
155
165
|
|
|
156
166
|
write = function(ch: ChannelState, value: any): ()
|
|
157
167
|
if isAllFixed then
|
|
@@ -195,6 +205,14 @@ function Struct.deltaStruct(schema: { [string]: Codec<any> }): Codec<any>
|
|
|
195
205
|
local bitmaskBytes = ceil(segCount / 8)
|
|
196
206
|
local deltaId = Shared.allocDeltaId()
|
|
197
207
|
|
|
208
|
+
local hasUnknown = false
|
|
209
|
+
for _, fieldCodec in schema do
|
|
210
|
+
if (fieldCodec :: any)._hasUnknown then
|
|
211
|
+
hasUnknown = true
|
|
212
|
+
break
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
198
216
|
-- Scratch bounds reused every frame(only cloned into cache)
|
|
199
217
|
local _scratchBounds = table.create(segCount + 1) :: { number }
|
|
200
218
|
_scratchBounds[1] = 0
|
|
@@ -237,6 +255,7 @@ function Struct.deltaStruct(schema: { [string]: Codec<any> }): Codec<any>
|
|
|
237
255
|
|
|
238
256
|
return table.freeze({
|
|
239
257
|
_isDelta = true,
|
|
258
|
+
_hasUnknown = hasUnknown or nil,
|
|
240
259
|
|
|
241
260
|
write = function(ch: ChannelState, value: any): ()
|
|
242
261
|
local cache = ch.deltas[deltaId] :: DeltaCache?
|
|
@@ -413,7 +432,10 @@ function Struct.deltaStruct(schema: { [string]: Codec<any> }): Codec<any>
|
|
|
413
432
|
end
|
|
414
433
|
|
|
415
434
|
local cache = Baseline.getCache(deltaId) :: any
|
|
416
|
-
|
|
435
|
+
if not cache then
|
|
436
|
+
error("[Lync] Delta frame without baseline")
|
|
437
|
+
end
|
|
438
|
+
local result = table.clone(cache)
|
|
417
439
|
|
|
418
440
|
local maskStart = absPos
|
|
419
441
|
absPos += bitmaskBytes
|
|
@@ -71,6 +71,14 @@ function Tagged.define(tagField: string, variants: { [string]: Codec<any> }): Co
|
|
|
71
71
|
end
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
+
local hasUnknown = false
|
|
75
|
+
for i = 1, count do
|
|
76
|
+
if (tagCodecs[i] :: any)._hasUnknown then
|
|
77
|
+
hasUnknown = true
|
|
78
|
+
break
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
74
82
|
table.freeze(nameToTag)
|
|
75
83
|
table.freeze(tagCodecs)
|
|
76
84
|
table.freeze(tagNames)
|
|
@@ -86,6 +94,7 @@ function Tagged.define(tagField: string, variants: { [string]: Codec<any> }): Co
|
|
|
86
94
|
if fixedSize then
|
|
87
95
|
return table.freeze({
|
|
88
96
|
_size = fixedSize,
|
|
97
|
+
_hasUnknown = hasUnknown or nil,
|
|
89
98
|
_directWrite = function(b: buffer, offset: number, value: any): ()
|
|
90
99
|
local tag = nameToTag[value[tagField]]
|
|
91
100
|
if not tag then
|
|
@@ -141,6 +150,7 @@ function Tagged.define(tagField: string, variants: { [string]: Codec<any> }): Co
|
|
|
141
150
|
|
|
142
151
|
return table.freeze({
|
|
143
152
|
_size = nil,
|
|
153
|
+
_hasUnknown = hasUnknown or nil,
|
|
144
154
|
write = function(ch: ChannelState, value: any): ()
|
|
145
155
|
local tag = nameToTag[value[tagField]]
|
|
146
156
|
if not tag then
|
|
@@ -183,6 +193,7 @@ function Tagged.define(tagField: string, variants: { [string]: Codec<any> }): Co
|
|
|
183
193
|
|
|
184
194
|
return table.freeze({
|
|
185
195
|
_size = fixedSize,
|
|
196
|
+
_hasUnknown = hasUnknown or nil,
|
|
186
197
|
write = function(ch: ChannelState, value: any): ()
|
|
187
198
|
local tag = nameToTag[value[tagField]]
|
|
188
199
|
if not tag then
|
|
@@ -24,6 +24,14 @@ function Tuple.define(...: Codec<any>): Codec<{ any }>
|
|
|
24
24
|
|
|
25
25
|
table.freeze(codecs)
|
|
26
26
|
|
|
27
|
+
local hasUnknown = false
|
|
28
|
+
for i = 1, count do
|
|
29
|
+
if (codecs[i] :: any)._hasUnknown then
|
|
30
|
+
hasUnknown = true
|
|
31
|
+
break
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
27
35
|
local canDirect = true
|
|
28
36
|
local totalSize: number = 0
|
|
29
37
|
local directWrites = table.create(count) :: { any }
|
|
@@ -92,6 +100,7 @@ function Tuple.define(...: Codec<any>): Codec<{ any }>
|
|
|
92
100
|
_size = totalSize,
|
|
93
101
|
_directWrite = directWriteBody,
|
|
94
102
|
_directRead = directReadBody,
|
|
103
|
+
_hasUnknown = hasUnknown or nil,
|
|
95
104
|
write = function(ch: ChannelState, value: { any }): ()
|
|
96
105
|
local c = ch.cursor
|
|
97
106
|
if c + totalSize > ch.size then
|
|
@@ -119,6 +128,7 @@ function Tuple.define(...: Codec<any>): Codec<{ any }>
|
|
|
119
128
|
|
|
120
129
|
return table.freeze({
|
|
121
130
|
_size = totalSize,
|
|
131
|
+
_hasUnknown = hasUnknown or nil,
|
|
122
132
|
write = function(ch: ChannelState, value: { any }): ()
|
|
123
133
|
if hasFixedTotal then
|
|
124
134
|
alloc(ch, totalSize :: number)
|
package/src/index.d.ts
CHANGED
|
@@ -100,9 +100,9 @@ export type Target = Player | AllTarget | ExceptTarget | GroupObject | Player[];
|
|
|
100
100
|
|
|
101
101
|
export interface Scope {
|
|
102
102
|
add(this: Scope, conn: Connection | RBXScriptConnection): void;
|
|
103
|
-
listen<T>(this: Scope, source: Packet<T>, callback: (data: T, sender: Player | undefined) => void): void;
|
|
104
|
-
once<T>(this: Scope, source: Packet<T>, callback: (data: T, sender: Player | undefined) => void): void;
|
|
105
|
-
listenAll(this: Scope, namespace: Namespace, callback: (name: string, data: unknown, sender: Player | undefined) => void): void;
|
|
103
|
+
listen<T>(this: Scope, source: Packet<T>, callback: (data: T, sender: Player | undefined, timestamp: number | undefined) => void): void;
|
|
104
|
+
once<T>(this: Scope, source: Packet<T>, callback: (data: T, sender: Player | undefined, timestamp: number | undefined) => void): void;
|
|
105
|
+
listenAll(this: Scope, namespace: Namespace, callback: (name: string, data: unknown, sender: Player | undefined, timestamp: number | undefined) => void): void;
|
|
106
106
|
destroy(this: Scope): void;
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -120,14 +120,26 @@ export interface PacketConfig<T> {
|
|
|
120
120
|
rateLimit?: RateLimitConfig;
|
|
121
121
|
validate?: (data: T, player: Player) => LuaTuple<[boolean, string?]>;
|
|
122
122
|
maxPayloadBytes?: number;
|
|
123
|
+
timestamp?: "frame" | "offset" | "full";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface PlayerStats {
|
|
127
|
+
bytesSent: number;
|
|
128
|
+
bytesReceived: number;
|
|
123
129
|
}
|
|
124
130
|
|
|
125
131
|
export interface Packet<T> {
|
|
126
132
|
send(this: Packet<T>, data: T, target?: Target): void;
|
|
127
|
-
listen(this: Packet<T>, callback: (data: T, sender: Player | undefined) => void): Connection;
|
|
128
|
-
once(this: Packet<T>, callback: (data: T, sender: Player | undefined) => void): Connection;
|
|
133
|
+
listen(this: Packet<T>, callback: (data: T, sender: Player | undefined, timestamp: number | undefined) => void): Connection;
|
|
134
|
+
once(this: Packet<T>, callback: (data: T, sender: Player | undefined, timestamp: number | undefined) => void): Connection;
|
|
129
135
|
wait(this: Packet<T>): LuaTuple<[T, Player | undefined]>;
|
|
130
136
|
disconnectAll(this: Packet<T>): void;
|
|
137
|
+
|
|
138
|
+
getBytesSent(this: Packet<T>): number;
|
|
139
|
+
getBytesReceived(this: Packet<T>): number;
|
|
140
|
+
getFires(this: Packet<T>): number;
|
|
141
|
+
getRecvFires(this: Packet<T>): number;
|
|
142
|
+
getDrops(this: Packet<T>): number;
|
|
131
143
|
}
|
|
132
144
|
|
|
133
145
|
// -- Query -------------------------------------------------------------
|
|
@@ -182,7 +194,7 @@ export interface Namespace {
|
|
|
182
194
|
readonly queries: Record<string, Query<unknown, unknown>>;
|
|
183
195
|
listenAll(
|
|
184
196
|
this: Namespace,
|
|
185
|
-
callback: (name: string, data: unknown, sender: Player | undefined) => void,
|
|
197
|
+
callback: (name: string, data: unknown, sender: Player | undefined, timestamp: number | undefined) => void,
|
|
186
198
|
): Connection;
|
|
187
199
|
onSend(
|
|
188
200
|
this: Namespace,
|
|
@@ -306,13 +318,30 @@ declare namespace Lync {
|
|
|
306
318
|
// Scope
|
|
307
319
|
export function scope(): Scope;
|
|
308
320
|
|
|
309
|
-
// Configuration
|
|
321
|
+
// Configuration (call before start)
|
|
310
322
|
export function setChannelMaxSize(bytes: number): void;
|
|
311
323
|
export function setValidationDepth(depth: number): void;
|
|
312
324
|
export function setPoolSize(count: number): void;
|
|
313
325
|
|
|
326
|
+
// Runtime configuration
|
|
327
|
+
export function flush(): void;
|
|
328
|
+
export function setFlushRate(hz: number): void;
|
|
329
|
+
|
|
330
|
+
// Stats (default off; call enableStats() to activate)
|
|
331
|
+
export function enableStats(): void;
|
|
332
|
+
export function getPlayerStats(player: Player): PlayerStats | undefined;
|
|
333
|
+
export function resetStats(): void;
|
|
334
|
+
|
|
335
|
+
// Security (server-only)
|
|
336
|
+
export function setBandwidthLimit(softLimit: number, maxStrikes: number): void;
|
|
337
|
+
|
|
314
338
|
// Introspection
|
|
315
339
|
export function queryPendingCount(): number;
|
|
340
|
+
|
|
341
|
+
// Packet capture (server-only, debug)
|
|
342
|
+
export function startCapture(label?: string): void;
|
|
343
|
+
export function stopCapture(): void;
|
|
344
|
+
export function dumpCaptures(): void;
|
|
316
345
|
}
|
|
317
346
|
|
|
318
347
|
export default Lync;
|
package/src/init.luau
CHANGED
|
@@ -103,8 +103,38 @@ end
|
|
|
103
103
|
|
|
104
104
|
-- Public --------------------------------------------------------------
|
|
105
105
|
|
|
106
|
+
local IS_SERVER = RunService:IsServer()
|
|
107
|
+
|
|
108
|
+
local function flush(): ()
|
|
109
|
+
if IS_SERVER then
|
|
110
|
+
Server.flush()
|
|
111
|
+
else
|
|
112
|
+
Client.flush()
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
local function setFlushRate(hz: number): ()
|
|
117
|
+
if IS_SERVER then
|
|
118
|
+
Server.setFlushRate(hz)
|
|
119
|
+
else
|
|
120
|
+
Client.setFlushRate(hz)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
local function getPlayerStats(player: Player): any?
|
|
125
|
+
return Server.getPlayerStats(player)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
local function resetStats(): ()
|
|
129
|
+
Server.resetStats()
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
local function setBandwidthLimit(softLimit: number, maxStrikes: number): ()
|
|
133
|
+
Server.setBandwidthLimit(softLimit, maxStrikes)
|
|
134
|
+
end
|
|
135
|
+
|
|
106
136
|
local Lync = {
|
|
107
|
-
VERSION = "
|
|
137
|
+
VERSION = "2.0.0",
|
|
108
138
|
|
|
109
139
|
-- Lifecycle
|
|
110
140
|
start = start,
|
|
@@ -187,13 +217,30 @@ local Lync = {
|
|
|
187
217
|
-- Scope
|
|
188
218
|
scope = ScopeModule.create,
|
|
189
219
|
|
|
190
|
-
-- Configuration(call before start)
|
|
220
|
+
-- Configuration (call before start)
|
|
191
221
|
setChannelMaxSize = Channel.setMaxSize,
|
|
192
222
|
setValidationDepth = Gate.setMaxDepth,
|
|
193
223
|
setPoolSize = Pool.setMaxSize,
|
|
194
224
|
|
|
225
|
+
-- Runtime configuration
|
|
226
|
+
flush = flush,
|
|
227
|
+
setFlushRate = setFlushRate,
|
|
228
|
+
|
|
229
|
+
-- Stats (default off; call enableStats() to activate)
|
|
230
|
+
enableStats = Channel.enableStats,
|
|
231
|
+
getPlayerStats = getPlayerStats,
|
|
232
|
+
resetStats = resetStats,
|
|
233
|
+
|
|
234
|
+
-- Security (server-only)
|
|
235
|
+
setBandwidthLimit = setBandwidthLimit,
|
|
236
|
+
|
|
195
237
|
-- Introspection
|
|
196
238
|
queryPendingCount = QueryModule.pendingCount,
|
|
239
|
+
|
|
240
|
+
-- Packet capture (server-only, debug)
|
|
241
|
+
startCapture = if IS_SERVER then Server.startCapture else nil,
|
|
242
|
+
stopCapture = if IS_SERVER then Server.stopCapture else nil,
|
|
243
|
+
dumpCaptures = if IS_SERVER then Server.dumpCaptures else nil,
|
|
197
244
|
}
|
|
198
245
|
|
|
199
246
|
Lync.default = Lync
|
|
@@ -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)
|