@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.
@@ -38,6 +38,7 @@ function Array.deltaArray(element: Codec<any>, maxCount: number?): Codec<{ any }
38
38
 
39
39
  return table.freeze({
40
40
  _isDelta = true,
41
+ _hasUnknown = (element :: any)._hasUnknown or nil,
41
42
 
42
43
  write = function(ch: ChannelState, value: { any }): ()
43
44
  local len = #value
@@ -110,12 +111,14 @@ function Array.deltaArray(element: Codec<any>, maxCount: number?): Codec<{ any }
110
111
  ch.cursor += 1
111
112
  Varint.write(ch, dirtyN)
112
113
 
114
+ local prevIdx = 0
113
115
  for j = 1, dirtyN do
114
116
  local i = dirty[j]
115
117
  local newOff = bounds[i]
116
118
  local newLen = bounds[i + 1] - newOff
117
119
 
118
- Varint.write(ch, i - 1)
120
+ Varint.write(ch, (i - 1) - prevIdx)
121
+ prevIdx = i - 1
119
122
  alloc(ch, newLen)
120
123
  buffer.copy(ch.buff, ch.cursor, scratch.buff, newOff, newLen)
121
124
  ch.cursor += newLen
@@ -167,21 +170,26 @@ function Array.deltaArray(element: Codec<any>, maxCount: number?): Codec<{ any }
167
170
  end
168
171
 
169
172
  local cache = Baseline.getCache(deltaId) :: { any }?
170
- local result = if cache then table.clone(cache) else {}
173
+ if not cache then
174
+ error("[Lync] Delta frame without baseline")
175
+ end
176
+ local result = table.clone(cache)
171
177
 
172
178
  local dirtyN, dnBytes = Varint.read(src, absPos)
173
179
  absPos += dnBytes
174
180
 
181
+ local runningIdx = 0
175
182
  for _ = 1, dirtyN do
176
- local idx, idxBytes = Varint.read(src, absPos)
177
- absPos += idxBytes
183
+ local delta, deltaBytes = Varint.read(src, absPos)
184
+ absPos += deltaBytes
185
+ runningIdx += delta
178
186
 
179
187
  if elemRead and elemSize then
180
- result[idx + 1] = elemRead(src, absPos)
188
+ result[runningIdx + 1] = elemRead(src, absPos)
181
189
  absPos += elemSize
182
190
  else
183
191
  local val, n = readElement(src, absPos, refs)
184
- result[idx + 1] = val
192
+ result[runningIdx + 1] = val
185
193
  absPos += n
186
194
  end
187
195
  end
@@ -198,8 +206,78 @@ function Array.array(element: Codec<any>, maxCount: number?): Codec<{ any }>
198
206
  local directWrite = internal._directWrite
199
207
  local directRead = internal._directRead
200
208
 
209
+ local band = bit32.band
210
+ local bor = bit32.bor
211
+ local lshift = bit32.lshift
212
+ local rshift = bit32.rshift
213
+ local ceil = math.ceil
214
+
215
+ -- Bitpacked bool array: 8 bools per byte
216
+ if internal._isBool then
217
+ return table.freeze({
218
+ _hasUnknown = nil,
219
+ write = function(ch: ChannelState, value: { any }): ()
220
+ local len = #value
221
+ Varint.write(ch, len)
222
+ if len == 0 then
223
+ return
224
+ end
225
+
226
+ local byteCount = ceil(len / 8)
227
+ local c = ch.cursor
228
+ if c + byteCount > ch.size then
229
+ alloc(ch, byteCount)
230
+ end
231
+
232
+ local b = ch.buff
233
+ local byteIdx = 0
234
+ local acc = 0
235
+ for i = 1, len do
236
+ local bit = (i - 1) % 8
237
+ if value[i] then
238
+ acc = bor(acc, lshift(1, bit))
239
+ end
240
+ if bit == 7 then
241
+ buffer.writeu8(b, c + byteIdx, acc)
242
+ byteIdx += 1
243
+ acc = 0
244
+ end
245
+ end
246
+ -- Flush remaining bits
247
+ if len % 8 ~= 0 then
248
+ buffer.writeu8(b, c + byteIdx, acc)
249
+ end
250
+ ch.cursor = c + byteCount
251
+ end,
252
+ read = function(src: buffer, pos: number, _refs: { Instance }?): ({ boolean }, number)
253
+ local len, lenBytes = Varint.read(src, pos)
254
+
255
+ if maxCount and len > maxCount then
256
+ error(`[Lync] Array count {len} exceeds max {maxCount}`)
257
+ end
258
+
259
+ local result = table.create(len)
260
+ if len == 0 then
261
+ return result, lenBytes
262
+ end
263
+
264
+ local byteCount = ceil(len / 8)
265
+ local c = pos + lenBytes
266
+ for i = 1, len do
267
+ local byteIdx = rshift(i - 1, 3)
268
+ local bit = (i - 1) % 8
269
+ local byte = buffer.readu8(src, c + byteIdx)
270
+ result[i] = band(byte, lshift(1, bit)) ~= 0
271
+ end
272
+
273
+ return result, lenBytes + byteCount
274
+ end,
275
+ })
276
+ end
277
+
201
278
  if elementSize and directWrite and directRead then
202
279
  return table.freeze({
280
+ _hasUnknown = (element :: any)._hasUnknown or nil,
203
281
  write = function(ch: ChannelState, value: { any }): ()
204
282
  local len = #value
205
283
  Varint.write(ch, len)
@@ -244,6 +322,7 @@ function Array.array(element: Codec<any>, maxCount: number?): Codec<{ any }>
244
322
  local readElement = element.read
245
323
 
246
324
  return table.freeze({
325
+ _hasUnknown = (element :: any)._hasUnknown or nil,
247
326
  write = function(ch: ChannelState, value: { any }): ()
248
327
  local len = #value
249
328
  Varint.write(ch, len)
@@ -44,6 +44,7 @@ function Map.map(
44
44
  local entrySize = keySize + valSize
45
45
 
46
46
  return table.freeze({
47
+ _hasUnknown = (keyCodec :: any)._hasUnknown or (valueCodec :: any)._hasUnknown or nil,
47
48
  write = function(ch: ChannelState, value: { [any]: any }): ()
48
49
  local count = 0
49
50
  for _ in value do
@@ -100,6 +101,7 @@ function Map.map(
100
101
  local readVal = valueCodec.read
101
102
 
102
103
  return table.freeze({
104
+ _hasUnknown = (keyCodec :: any)._hasUnknown or (valueCodec :: any)._hasUnknown or nil,
103
105
  write = function(ch: ChannelState, value: { [any]: any }): ()
104
106
  local count = 0
105
107
  for _ in value do
@@ -185,6 +187,7 @@ function Map.deltaMap(
185
187
 
186
188
  return table.freeze({
187
189
  _isDelta = true,
190
+ _hasUnknown = (keyCodec :: any)._hasUnknown or (valueCodec :: any)._hasUnknown or nil,
188
191
 
189
192
  write = function(ch: ChannelState, value: { [any]: any }): ()
190
193
  local scratch = acquireScratch()
@@ -378,7 +381,10 @@ function Map.deltaMap(
378
381
  end
379
382
 
380
383
  local cache = Baseline.getCache(deltaId) :: { [any]: any }?
381
- local result = if cache then table.clone(cache) else {}
384
+ if not cache then
385
+ error("[Lync] Delta frame without baseline")
386
+ end
387
+ local result = table.clone(cache)
382
388
 
383
389
  local upsertN, unBytes = Varint.read(src, absPos)
384
390
  absPos += unBytes
@@ -19,6 +19,7 @@ function Optional.optional(inner: Codec<any>): Codec<any>
19
19
  local innerRead = inner.read
20
20
 
21
21
  return table.freeze({
22
+ _hasUnknown = (inner :: any)._hasUnknown or nil,
22
23
  write = function(ch: ChannelState, value: any): ()
23
24
  local c = ch.cursor
24
25
  if c + 1 > ch.size then
@@ -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
- local result = if cache then table.clone(cache) else {}
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)
@@ -41,6 +41,9 @@ InstanceCodec.inst = table.freeze({
41
41
  if not inst then
42
42
  error(`[Lync] Instance ref index out of bounds: {idx}`)
43
43
  end
44
+ if typeof(inst) ~= "Instance" then
45
+ error("[Lync] Invalid ref: expected Instance")
46
+ end
44
47
 
45
48
  return inst, 2
46
49
  end,
@@ -11,6 +11,7 @@ local alloc = Channel.alloc
11
11
 
12
12
  return table.freeze({
13
13
  unknown = table.freeze({
14
+ _hasUnknown = true,
14
15
  write = function(ch: ChannelState, value: any): ()
15
16
  local refs = ch.refs
16
17
  local idx = #refs + 1
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 = "1.5.1",
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