@axpecter/lync 2.2.1 → 2.3.2
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 +109 -147
- package/package.json +1 -1
- package/src/Types.luau +34 -22
- package/src/api/Group.luau +40 -33
- package/src/api/Packet.luau +140 -66
- package/src/api/Query.luau +103 -65
- package/src/api/Scope.luau +26 -10
- package/src/api/Signal.luau +43 -52
- package/src/codec/Base.luau +28 -14
- package/src/codec/composite/Array.luau +296 -115
- package/src/codec/composite/Map.luau +377 -57
- package/src/codec/composite/Optional.luau +10 -2
- package/src/codec/composite/Shared.luau +134 -64
- package/src/codec/composite/Struct.luau +294 -43
- package/src/codec/composite/Tagged.luau +21 -16
- package/src/codec/composite/Tuple.luau +32 -15
- package/src/codec/datatype/Buffer.luau +7 -7
- package/src/codec/datatype/CFrame.luau +34 -122
- package/src/codec/datatype/Color.luau +10 -10
- package/src/codec/datatype/Instance.luau +15 -12
- package/src/codec/datatype/IntVector.luau +2 -2
- package/src/codec/datatype/NumberRange.luau +15 -8
- package/src/codec/datatype/Ray.luau +13 -14
- package/src/codec/datatype/Rect.luau +12 -12
- package/src/codec/datatype/Region.luau +13 -14
- package/src/codec/datatype/Sequence.luau +87 -65
- package/src/codec/datatype/String.luau +17 -10
- package/src/codec/datatype/UDim.luau +10 -8
- package/src/codec/datatype/Vector.luau +20 -59
- package/src/codec/meta/Auto.luau +94 -126
- package/src/codec/meta/Bitfield.luau +12 -14
- package/src/codec/meta/Custom.luau +3 -1
- package/src/codec/meta/DeltaScalar.luau +390 -0
- package/src/codec/meta/Enum.luau +9 -9
- package/src/codec/meta/Float.luau +6 -28
- package/src/codec/meta/Nothing.luau +1 -1
- package/src/codec/meta/Unknown.luau +10 -7
- package/src/codec/primitive/Bool.luau +6 -4
- package/src/codec/primitive/Float16.luau +5 -2
- package/src/codec/primitive/Int.luau +5 -6
- package/src/codec/primitive/Number.luau +6 -4
- package/src/codec/primitive/Signed.luau +2 -2
- package/src/codec/primitive/Varint.luau +76 -39
- package/src/codec/primitive/Zint.luau +99 -0
- package/src/index.d.ts +207 -53
- package/src/init.luau +116 -103
- package/src/internal/Baseline.luau +9 -1
- package/src/internal/Channel.luau +191 -157
- package/src/internal/Middleware.luau +22 -6
- package/src/internal/Pool.luau +12 -4
- package/src/internal/Registry.luau +25 -16
- package/src/internal/Transport.luau +1 -1
- package/src/transport/Bridge.luau +47 -33
- package/src/transport/Client.luau +25 -21
- package/src/transport/Gate.luau +227 -172
- package/src/transport/Reader.luau +174 -106
- package/src/transport/Server.luau +46 -57
- package/src/util/Array.luau +18 -0
- package/src/util/Buffer.luau +90 -0
- package/src/util/Constants.luau +30 -0
- package/src/util/Log.luau +68 -0
- package/src/util/Player.luau +14 -0
- package/src/util/Quantize.luau +84 -0
- package/src/util/Quat.luau +124 -0
- package/src/internal/Util.luau +0 -26
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
--!native
|
|
3
3
|
-- Per-channel buffer state with MSB batch framing, XOR delta, timestamps.
|
|
4
4
|
|
|
5
|
+
local Base = require(script.Parent.Parent.codec.Base)
|
|
6
|
+
local Buf = require(script.Parent.Parent.util.Buffer)
|
|
7
|
+
local Constants = require(script.Parent.Parent.util.Constants)
|
|
8
|
+
local Log = require(script.Parent.Parent.util.Log)
|
|
5
9
|
local Types = require(script.Parent.Parent.Types)
|
|
6
10
|
local Varint = require(script.Parent.Parent.codec.primitive.Varint)
|
|
7
11
|
|
|
@@ -12,16 +16,29 @@ local TS_FRAME = 1
|
|
|
12
16
|
local TS_OFFSET = 2
|
|
13
17
|
local TS_FULL = 3
|
|
14
18
|
|
|
15
|
-
local DEFAULT_MAX = 262144
|
|
16
19
|
local INITIAL_BUF = 1024
|
|
17
|
-
local
|
|
18
|
-
local
|
|
20
|
+
local HEADER_BYTE = 1
|
|
21
|
+
local FRAME_MSB = Constants.FRAME_MSB
|
|
22
|
+
local U8_MAX = Constants.U8_MAX
|
|
23
|
+
local U16_MAX = Constants.U16_MAX
|
|
24
|
+
local MAX_BATCH_ITEMS = Constants.MAX_BATCH_ITEMS
|
|
25
|
+
|
|
26
|
+
-- 65.535s window so round((clock % win) * 1000) cannot overflow u16.
|
|
27
|
+
local OFFSET_WIN = 65.535
|
|
28
|
+
|
|
29
|
+
local TS_BYTES = table.freeze({
|
|
30
|
+
[TS_NONE] = 0,
|
|
31
|
+
[TS_FRAME] = 1,
|
|
32
|
+
[TS_OFFSET] = 2,
|
|
33
|
+
[TS_FULL] = 8,
|
|
34
|
+
})
|
|
19
35
|
|
|
20
36
|
-- State ------------------------------------------------------------------
|
|
21
37
|
|
|
22
|
-
local
|
|
23
|
-
local _anyTimestamp = false
|
|
38
|
+
local _timestampsEnabled = false
|
|
24
39
|
local _statsEnabled = false
|
|
40
|
+
|
|
41
|
+
-- Refreshed by updateTimestamp once per flush.
|
|
25
42
|
local _frameCounter = 0
|
|
26
43
|
local _offsetMs = 0
|
|
27
44
|
local _clock = 0.0
|
|
@@ -30,51 +47,16 @@ local EMPTY_REFS = table.freeze({}) :: { Instance }
|
|
|
30
47
|
|
|
31
48
|
-- Private ----------------------------------------------------------------
|
|
32
49
|
|
|
33
|
-
local
|
|
34
|
-
local
|
|
50
|
+
local alloc = Base.alloc
|
|
51
|
+
local ensure = Base.ensure
|
|
35
52
|
local band = bit32.band
|
|
36
|
-
local bxor = bit32.bxor
|
|
37
53
|
local bor = bit32.bor
|
|
54
|
+
local bufCopy = buffer.copy
|
|
38
55
|
local writeu8 = buffer.writeu8
|
|
39
56
|
local writeu16 = buffer.writeu16
|
|
40
|
-
local writeu32 = buffer.writeu32
|
|
41
57
|
local writef64 = buffer.writef64
|
|
42
|
-
local readu8 = buffer.readu8
|
|
43
|
-
local readu32 = buffer.readu32
|
|
44
58
|
local round = math.round
|
|
45
|
-
local
|
|
46
|
-
|
|
47
|
-
local function alloc(ch: Types.ChannelState, bytes: number): ()
|
|
48
|
-
local needed = ch.cursor + bytes
|
|
49
|
-
if needed <= ch.size then
|
|
50
|
-
return
|
|
51
|
-
end
|
|
52
|
-
if needed > _maxSize then
|
|
53
|
-
local name = ch.currentPacket
|
|
54
|
-
if name then
|
|
55
|
-
error(`[Lync] Channel.alloc("{name}"): buffer overflow ({needed}B, max {_maxSize}B)`)
|
|
56
|
-
end
|
|
57
|
-
error(`[Lync] Channel.alloc: buffer overflow ({needed}B, max {_maxSize}B)`)
|
|
58
|
-
end
|
|
59
|
-
local newSize = lshift(1, 32 - countlz(needed - 1))
|
|
60
|
-
local newBuff = buffer.create(newSize)
|
|
61
|
-
buffer.copy(newBuff, 0, ch.buff, 0, ch.cursor)
|
|
62
|
-
ch.buff = newBuff
|
|
63
|
-
ch.size = newSize
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
local function tsBytes(mode: number): number
|
|
67
|
-
if mode == TS_NONE then
|
|
68
|
-
return 0
|
|
69
|
-
end
|
|
70
|
-
if mode == TS_FRAME then
|
|
71
|
-
return 1
|
|
72
|
-
end
|
|
73
|
-
if mode == TS_OFFSET then
|
|
74
|
-
return 2
|
|
75
|
-
end
|
|
76
|
-
return 8
|
|
77
|
-
end
|
|
59
|
+
local osClock = os.clock
|
|
78
60
|
|
|
79
61
|
local function writeTimestamp(b: buffer, off: number, mode: number): ()
|
|
80
62
|
if mode == TS_FRAME then
|
|
@@ -86,8 +68,103 @@ local function writeTimestamp(b: buffer, off: number, mode: number): ()
|
|
|
86
68
|
end
|
|
87
69
|
end
|
|
88
70
|
|
|
71
|
+
--[[
|
|
72
|
+
Single -> multi upgrade. Patches the previously-emitted single-item
|
|
73
|
+
header, splices a 2-byte count cell after the timestamp, and shifts the
|
|
74
|
+
payload right. Hot only when a packet fires twice in a row to the same
|
|
75
|
+
channel without an intervening flush.
|
|
76
|
+
]]
|
|
77
|
+
local function upgradeSingleToMulti(ch: Types.ChannelState, regId: number, headerSize: number): ()
|
|
78
|
+
local headerPos = ch.singlePos
|
|
79
|
+
writeu8(ch.buff, headerPos, regId)
|
|
80
|
+
|
|
81
|
+
local afterTs = headerPos + headerSize
|
|
82
|
+
local cursor = ch.cursor
|
|
83
|
+
local payloadLen = cursor - afterTs
|
|
84
|
+
ensure(ch, 2)
|
|
85
|
+
-- Re-bind: ensure() may have realloced the buffer.
|
|
86
|
+
local b = ch.buff
|
|
87
|
+
bufCopy(b, afterTs + 2, b, afterTs, payloadLen)
|
|
88
|
+
|
|
89
|
+
ch.countPos = afterTs
|
|
90
|
+
writeu16(b, afterTs, 0)
|
|
91
|
+
ch.cursor = cursor + 2
|
|
92
|
+
ch.singleMode = false
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
-- itemCount bump is left to the caller, AFTER the body write succeeds.
|
|
96
|
+
local function openOrUpgrade(ch: Types.ChannelState, reg: Types.Registration): ()
|
|
97
|
+
if reg.id ~= ch.lastId or ch.itemCount >= MAX_BATCH_ITEMS then
|
|
98
|
+
reg._openFn(ch, reg.id)
|
|
99
|
+
elseif ch.singleMode then
|
|
100
|
+
upgradeSingleToMulti(ch, reg.id, reg.headerSize)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
local function spliceEncoded(ch: Types.ChannelState, payload: buffer, payloadLen: number): ()
|
|
105
|
+
local cursor = ch.cursor
|
|
106
|
+
if cursor + payloadLen > ch.size then
|
|
107
|
+
alloc(ch, payloadLen)
|
|
108
|
+
end
|
|
109
|
+
bufCopy(ch.buff, cursor, payload, 0, payloadLen)
|
|
110
|
+
ch.cursor = cursor + payloadLen
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
--[[
|
|
114
|
+
Hot-swapped by enableStats() so the per-item dispatch avoids a runtime
|
|
115
|
+
branch on the stats flag. itemCount bumps AFTER codec.write so a
|
|
116
|
+
throwing codec leaves the count consistent with the buffer.
|
|
117
|
+
]]
|
|
118
|
+
local writeBatchPlain: (Types.ChannelState, Types.Registration, any) -> ()
|
|
119
|
+
local writeBatchStats: (Types.ChannelState, Types.Registration, any) -> ()
|
|
120
|
+
|
|
121
|
+
writeBatchPlain = function(ch: Types.ChannelState, reg: Types.Registration, data: any): ()
|
|
122
|
+
openOrUpgrade(ch, reg)
|
|
123
|
+
reg.codec.write(ch, data)
|
|
124
|
+
ch.itemCount += 1
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
writeBatchStats = function(ch: Types.ChannelState, reg: Types.Registration, data: any): ()
|
|
128
|
+
openOrUpgrade(ch, reg)
|
|
129
|
+
local before = ch.cursor
|
|
130
|
+
reg.codec.write(ch, data)
|
|
131
|
+
ch.itemCount += 1
|
|
132
|
+
reg.bytesSent += ch.cursor - before
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
--[[
|
|
136
|
+
Splice a pre-encoded payload into the channel. Used by Packet.broadcast
|
|
137
|
+
to encode the codec output once per fanout instead of N times.
|
|
138
|
+
]]
|
|
139
|
+
local writeBatchEncodedPlain: (Types.ChannelState, Types.Registration, buffer, number) -> ()
|
|
140
|
+
local writeBatchEncodedStats: (Types.ChannelState, Types.Registration, buffer, number) -> ()
|
|
141
|
+
|
|
142
|
+
writeBatchEncodedPlain = function(
|
|
143
|
+
ch: Types.ChannelState,
|
|
144
|
+
reg: Types.Registration,
|
|
145
|
+
payload: buffer,
|
|
146
|
+
payloadLen: number
|
|
147
|
+
): ()
|
|
148
|
+
openOrUpgrade(ch, reg)
|
|
149
|
+
spliceEncoded(ch, payload, payloadLen)
|
|
150
|
+
ch.itemCount += 1
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
writeBatchEncodedStats = function(
|
|
154
|
+
ch: Types.ChannelState,
|
|
155
|
+
reg: Types.Registration,
|
|
156
|
+
payload: buffer,
|
|
157
|
+
payloadLen: number
|
|
158
|
+
): ()
|
|
159
|
+
openOrUpgrade(ch, reg)
|
|
160
|
+
spliceEncoded(ch, payload, payloadLen)
|
|
161
|
+
ch.itemCount += 1
|
|
162
|
+
reg.bytesSent += payloadLen
|
|
163
|
+
end
|
|
164
|
+
|
|
89
165
|
-- Public -----------------------------------------------------------------
|
|
90
166
|
|
|
167
|
+
-- Module table is intentionally unfrozen: enableStats() hot-swaps writeBatch.
|
|
91
168
|
local Channel = {}
|
|
92
169
|
|
|
93
170
|
Channel.TS_NONE = TS_NONE
|
|
@@ -114,36 +191,19 @@ function Channel.create(): Types.ChannelState
|
|
|
114
191
|
}
|
|
115
192
|
end
|
|
116
193
|
|
|
117
|
-
function Channel.alloc(ch: Types.ChannelState, bytes: number): ()
|
|
118
|
-
if bytes > 0 then
|
|
119
|
-
alloc(ch, bytes)
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
--[[
|
|
124
|
-
Write a single byte at the cursor and advance. Centralizes the
|
|
125
|
-
ubiquitous `if cursor + 1 > size then alloc(ch, 1) end; writeu8(...);
|
|
126
|
-
cursor += 1` pattern.
|
|
127
|
-
]]
|
|
128
194
|
function Channel.writeByte(ch: Types.ChannelState, byte: number): ()
|
|
129
195
|
local cursor = ch.cursor
|
|
130
|
-
if cursor +
|
|
131
|
-
alloc(ch,
|
|
196
|
+
if cursor + HEADER_BYTE > ch.size then
|
|
197
|
+
alloc(ch, HEADER_BYTE)
|
|
132
198
|
end
|
|
133
199
|
writeu8(ch.buff, cursor, byte)
|
|
134
|
-
ch.cursor = cursor +
|
|
200
|
+
ch.cursor = cursor + HEADER_BYTE
|
|
135
201
|
end
|
|
136
202
|
|
|
137
|
-
--
|
|
138
|
-
|
|
139
|
-
1-based index as a u16 to the buffer, advance cursor. Returns the
|
|
140
|
-
index. Used by the Instance and Unknown codecs.
|
|
141
|
-
]]
|
|
142
|
-
function Channel.pushRef(ch: Types.ChannelState, value: any): number
|
|
203
|
+
-- Append `value` to the sidecar refs, write its 1-based u16 index.
|
|
204
|
+
function Channel.pushRef(ch: Types.ChannelState, value: any): ()
|
|
143
205
|
local idx = ch.refCount + 1
|
|
144
|
-
|
|
145
|
-
error("[Lync] Channel.pushRef: ref overflow (>65535)")
|
|
146
|
-
end
|
|
206
|
+
Log.assert(idx <= U16_MAX, "ref overflow, max 65535")
|
|
147
207
|
ch.refs[idx] = value
|
|
148
208
|
ch.refCount = idx
|
|
149
209
|
|
|
@@ -153,9 +213,12 @@ function Channel.pushRef(ch: Types.ChannelState, value: any): number
|
|
|
153
213
|
end
|
|
154
214
|
writeu16(ch.buff, cursor, idx)
|
|
155
215
|
ch.cursor = cursor + 2
|
|
156
|
-
return idx
|
|
157
216
|
end
|
|
158
217
|
|
|
218
|
+
--[[
|
|
219
|
+
Per-flush reset. Preserves prevDump/prevDumpLen (XOR baseline) and
|
|
220
|
+
ch.deltas (delta caches): both must outlive flushes for the same owner.
|
|
221
|
+
]]
|
|
159
222
|
function Channel.reset(ch: Types.ChannelState): ()
|
|
160
223
|
ch.cursor = 0
|
|
161
224
|
ch.lastId = -1
|
|
@@ -170,20 +233,34 @@ function Channel.reset(ch: Types.ChannelState): ()
|
|
|
170
233
|
end
|
|
171
234
|
end
|
|
172
235
|
|
|
236
|
+
--[[
|
|
237
|
+
Full reset including XOR baseline and delta caches. Pool.acquire calls
|
|
238
|
+
this so a recycled state cannot leak the previous owner's baseline.
|
|
239
|
+
]]
|
|
240
|
+
function Channel.fullReset(ch: Types.ChannelState): ()
|
|
241
|
+
Channel.reset(ch)
|
|
242
|
+
ch.prevDump = nil
|
|
243
|
+
ch.prevDumpLen = 0
|
|
244
|
+
table.clear(ch.deltas)
|
|
245
|
+
end
|
|
246
|
+
|
|
173
247
|
--[[
|
|
174
248
|
Resolve the batch-open closure for a given timestamp mode and packet
|
|
175
|
-
name.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
- records cursor position so writeBatch can upgrade single->multi later.
|
|
249
|
+
name. Returned function seals the prior batch, writes the single-mode
|
|
250
|
+
header [0x80 | id] + optional timestamp, and records the cursor so
|
|
251
|
+
writeBatch can upgrade single -> multi later.
|
|
179
252
|
]]
|
|
180
253
|
function Channel.resolveOpenFn(
|
|
181
254
|
timestampMode: number,
|
|
182
255
|
name: string
|
|
183
256
|
): (ch: Types.ChannelState, id: number) -> ()
|
|
184
|
-
local headerLen =
|
|
257
|
+
local headerLen = HEADER_BYTE + TS_BYTES[timestampMode]
|
|
185
258
|
|
|
186
259
|
return function(ch: Types.ChannelState, id: number): ()
|
|
260
|
+
--[[
|
|
261
|
+
Inline sealCount: this is the only call site that can take the
|
|
262
|
+
multi-mode branch on a non-fresh channel.
|
|
263
|
+
]]
|
|
187
264
|
if ch.lastId >= 0 and not ch.singleMode then
|
|
188
265
|
writeu16(ch.buff, ch.countPos, ch.itemCount)
|
|
189
266
|
end
|
|
@@ -197,7 +274,7 @@ function Channel.resolveOpenFn(
|
|
|
197
274
|
alloc(ch, headerLen)
|
|
198
275
|
end
|
|
199
276
|
local b = ch.buff
|
|
200
|
-
writeu8(b, cursor, bor(
|
|
277
|
+
writeu8(b, cursor, bor(FRAME_MSB, id))
|
|
201
278
|
writeTimestamp(b, cursor + 1, timestampMode)
|
|
202
279
|
|
|
203
280
|
ch.singleMode = true
|
|
@@ -206,95 +283,32 @@ function Channel.resolveOpenFn(
|
|
|
206
283
|
end
|
|
207
284
|
end
|
|
208
285
|
|
|
286
|
+
Channel.writeBatch = writeBatchPlain
|
|
287
|
+
Channel.writeBatchEncoded = writeBatchEncodedPlain
|
|
288
|
+
|
|
209
289
|
--[[
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
header byte's MSB, then memmoves payload right by 2 to insert the
|
|
213
|
-
deferred u16 count between the timestamp and first payload.
|
|
290
|
+
Every caller (Server.flushReliable / flushUnreliable, Client.flush)
|
|
291
|
+
pre-checks `ch.cursor > 0`, so cursor here is always positive.
|
|
214
292
|
]]
|
|
215
|
-
function Channel.writeBatch(ch: Types.ChannelState, reg: Types.Registration, data: any): ()
|
|
216
|
-
if reg.id ~= ch.lastId or ch.itemCount >= 0xFFFF then
|
|
217
|
-
reg._openFn(ch, reg.id)
|
|
218
|
-
elseif ch.singleMode then
|
|
219
|
-
local headerPos = ch.singlePos
|
|
220
|
-
local b = ch.buff
|
|
221
|
-
writeu8(b, headerPos, reg.id)
|
|
222
|
-
|
|
223
|
-
local afterTs = headerPos + reg.headerSize
|
|
224
|
-
local cursor = ch.cursor
|
|
225
|
-
local payloadLen = cursor - afterTs
|
|
226
|
-
if cursor + 2 > ch.size then
|
|
227
|
-
alloc(ch, 2)
|
|
228
|
-
end
|
|
229
|
-
local b2 = ch.buff
|
|
230
|
-
|
|
231
|
-
if payloadLen > 0 then
|
|
232
|
-
buffer.copy(b2, afterTs + 2, b2, afterTs, payloadLen)
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
ch.countPos = afterTs
|
|
236
|
-
writeu16(b2, afterTs, 0)
|
|
237
|
-
ch.cursor = cursor + 2
|
|
238
|
-
ch.singleMode = false
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
ch.itemCount += 1
|
|
242
|
-
reg.codec.write(ch, data)
|
|
243
|
-
end
|
|
244
|
-
|
|
245
293
|
function Channel.sealAndDump(ch: Types.ChannelState): (buffer, { Instance }, number, number)
|
|
246
|
-
if ch.lastId >= 0 and not ch.singleMode
|
|
294
|
+
if ch.lastId >= 0 and not ch.singleMode then
|
|
247
295
|
writeu16(ch.buff, ch.countPos, ch.itemCount)
|
|
248
296
|
end
|
|
249
297
|
|
|
250
298
|
local cursor = ch.cursor
|
|
251
299
|
local snapshot = buffer.create(cursor)
|
|
252
|
-
|
|
253
|
-
buffer.copy(snapshot, 0, ch.buff, 0, cursor)
|
|
254
|
-
end
|
|
300
|
+
bufCopy(snapshot, 0, ch.buff, 0, cursor)
|
|
255
301
|
|
|
256
302
|
local refCount = ch.refCount
|
|
257
303
|
local refs = if refCount > 0 then table.clone(ch.refs) else EMPTY_REFS
|
|
258
304
|
return snapshot, refs, cursor, refCount
|
|
259
305
|
end
|
|
260
306
|
|
|
261
|
-
|
|
262
|
-
XOR `current` against `previous`, returning a fresh buffer of curLen
|
|
263
|
-
bytes. When `previous` is nil, returns `current` unchanged.
|
|
264
|
-
]]
|
|
265
|
-
function Channel.xorApply(
|
|
266
|
-
current: buffer,
|
|
267
|
-
curLen: number,
|
|
268
|
-
previous: buffer?,
|
|
269
|
-
prevLen: number
|
|
270
|
-
): buffer
|
|
271
|
-
if not previous then
|
|
272
|
-
return current
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
local result = buffer.create(curLen)
|
|
276
|
-
local overlap = minN(curLen, prevLen)
|
|
277
|
-
local prev = previous
|
|
278
|
-
|
|
279
|
-
local aligned = band(overlap, -4)
|
|
280
|
-
local i = 0
|
|
281
|
-
while i < aligned do
|
|
282
|
-
writeu32(result, i, bxor(readu32(current, i), readu32(prev, i)))
|
|
283
|
-
i += 4
|
|
284
|
-
end
|
|
285
|
-
while i < overlap do
|
|
286
|
-
writeu8(result, i, bxor(readu8(current, i), readu8(prev, i)))
|
|
287
|
-
i += 1
|
|
288
|
-
end
|
|
289
|
-
if curLen > overlap then
|
|
290
|
-
buffer.copy(result, overlap, current, overlap, curLen - overlap)
|
|
291
|
-
end
|
|
292
|
-
return result
|
|
293
|
-
end
|
|
307
|
+
Channel.xorApply = Buf.xorApply
|
|
294
308
|
|
|
295
309
|
--[[
|
|
296
|
-
|
|
297
|
-
|
|
310
|
+
Query frame. MSB on the id byte = nil response; cleared = payload follows.
|
|
311
|
+
Correlation IDs are varint-encoded.
|
|
298
312
|
]]
|
|
299
313
|
function Channel.writeQuery(
|
|
300
314
|
ch: Types.ChannelState,
|
|
@@ -303,14 +317,19 @@ function Channel.writeQuery(
|
|
|
303
317
|
codec: Types.InternalCodec<any>?,
|
|
304
318
|
data: any
|
|
305
319
|
): ()
|
|
306
|
-
if ch.lastId >= 0 and not ch.singleMode
|
|
320
|
+
if ch.lastId >= 0 and not ch.singleMode then
|
|
307
321
|
writeu16(ch.buff, ch.countPos, ch.itemCount)
|
|
308
322
|
end
|
|
323
|
+
--[[
|
|
324
|
+
lastId = -1 forces the next writeBatch to call _openFn (which resets
|
|
325
|
+
singleMode). Every other reader of singleMode (writeQuery,
|
|
326
|
+
sealAndDump) is gated by `lastId >= 0` first, so stamping singleMode
|
|
327
|
+
here would never be observed before the next reset overwrites it.
|
|
328
|
+
]]
|
|
309
329
|
ch.lastId = -1
|
|
310
|
-
ch.singleMode = false
|
|
311
330
|
|
|
312
331
|
local hasPayload = codec ~= nil and data ~= nil
|
|
313
|
-
local headerByte = if hasPayload then id else bor(
|
|
332
|
+
local headerByte = if hasPayload then id else bor(FRAME_MSB, id)
|
|
314
333
|
|
|
315
334
|
local cursor = ch.cursor
|
|
316
335
|
if cursor + 1 > ch.size then
|
|
@@ -328,29 +347,44 @@ end
|
|
|
328
347
|
-- Configuration ----------------------------------------------------------
|
|
329
348
|
|
|
330
349
|
function Channel.setMaxSize(bytes: number): ()
|
|
331
|
-
|
|
350
|
+
Base.setMaxSize(bytes)
|
|
332
351
|
end
|
|
333
352
|
|
|
334
353
|
function Channel.updateTimestamp(): ()
|
|
335
|
-
_frameCounter = band(_frameCounter + 1,
|
|
336
|
-
|
|
337
|
-
|
|
354
|
+
_frameCounter = band(_frameCounter + 1, U8_MAX)
|
|
355
|
+
local now = osClock()
|
|
356
|
+
_offsetMs = round((now % OFFSET_WIN) * 1000)
|
|
357
|
+
_clock = now
|
|
338
358
|
end
|
|
339
359
|
|
|
340
360
|
function Channel.enableTimestamps(): ()
|
|
341
|
-
|
|
361
|
+
_timestampsEnabled = true
|
|
342
362
|
end
|
|
343
363
|
|
|
344
364
|
function Channel.hasTimestamps(): boolean
|
|
345
|
-
return
|
|
365
|
+
return _timestampsEnabled
|
|
346
366
|
end
|
|
347
367
|
|
|
348
368
|
function Channel.enableStats(): ()
|
|
349
369
|
_statsEnabled = true
|
|
370
|
+
Channel.writeBatch = writeBatchStats
|
|
371
|
+
Channel.writeBatchEncoded = writeBatchEncodedStats
|
|
350
372
|
end
|
|
351
373
|
|
|
352
374
|
function Channel.statsEnabled(): boolean
|
|
353
375
|
return _statsEnabled
|
|
354
376
|
end
|
|
355
377
|
|
|
356
|
-
|
|
378
|
+
-- Module-level config + timestamp counters. Distinct from Channel.reset(ch).
|
|
379
|
+
function Channel.resetGlobals(): ()
|
|
380
|
+
Base.reset()
|
|
381
|
+
_timestampsEnabled = false
|
|
382
|
+
_statsEnabled = false
|
|
383
|
+
_frameCounter = 0
|
|
384
|
+
_offsetMs = 0
|
|
385
|
+
_clock = 0.0
|
|
386
|
+
Channel.writeBatch = writeBatchPlain
|
|
387
|
+
Channel.writeBatchEncoded = writeBatchEncodedPlain
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
return Channel
|
|
@@ -13,13 +13,21 @@ local _dropSignal = Signal.create()
|
|
|
13
13
|
|
|
14
14
|
-- Private ----------------------------------------------------------------
|
|
15
15
|
|
|
16
|
+
-- Mirrors Signal's STATE_DISCONNECTED. Entries in this state are mid-fire disconnects.
|
|
17
|
+
local STATE_DISCONNECTED = 0
|
|
18
|
+
|
|
16
19
|
--[[
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
Snapshot before iterating: a handler can disconnect itself or others,
|
|
21
|
+
and a fresh chain pull would skip or double-fire entries depending on
|
|
22
|
+
swap direction. Snapshotting locks the per-frame view.
|
|
20
23
|
]]
|
|
21
|
-
local function runChain(
|
|
22
|
-
|
|
24
|
+
local function runChain(
|
|
25
|
+
signal: { _count: number, _entries: { any } },
|
|
26
|
+
value: any,
|
|
27
|
+
name: string,
|
|
28
|
+
player: Player?
|
|
29
|
+
): any
|
|
30
|
+
local count = signal._count
|
|
23
31
|
if count == 0 then
|
|
24
32
|
return value
|
|
25
33
|
end
|
|
@@ -31,8 +39,9 @@ local function runChain(signal: any, value: any, name: string, player: Player?):
|
|
|
31
39
|
local result = value
|
|
32
40
|
for i = 1, count do
|
|
33
41
|
local entry = snapshot[i]
|
|
34
|
-
if entry._state ~=
|
|
42
|
+
if entry._state ~= STATE_DISCONNECTED then
|
|
35
43
|
local returned = entry.fn(result, name, player)
|
|
44
|
+
-- Hooks return non-nil to replace the value; nil keeps the prior result.
|
|
36
45
|
if returned ~= nil then
|
|
37
46
|
result = returned
|
|
38
47
|
end
|
|
@@ -73,6 +82,7 @@ function Middleware.fireDrop(player: Player, reason: string, name: string, data:
|
|
|
73
82
|
_dropSignal:fireSync(player, reason, name, data)
|
|
74
83
|
end
|
|
75
84
|
|
|
85
|
+
-- Hot-path callers gate on these to skip runChain's snapshot allocation.
|
|
76
86
|
function Middleware.hasSendHooks(): boolean
|
|
77
87
|
return _sendSignal:hasListeners()
|
|
78
88
|
end
|
|
@@ -81,4 +91,10 @@ function Middleware.hasReceiveHooks(): boolean
|
|
|
81
91
|
return _receiveSignal:hasListeners()
|
|
82
92
|
end
|
|
83
93
|
|
|
94
|
+
function Middleware.reset(): ()
|
|
95
|
+
_sendSignal = Signal.create()
|
|
96
|
+
_receiveSignal = Signal.create()
|
|
97
|
+
_dropSignal = Signal.create()
|
|
98
|
+
end
|
|
99
|
+
|
|
84
100
|
return table.freeze(Middleware)
|
package/src/internal/Pool.luau
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
--!strict
|
|
2
2
|
--!optimize 2
|
|
3
|
-
--
|
|
4
|
-
-- channels reusable without per-release reset cost.
|
|
3
|
+
-- ChannelState pool. Acquire fullResets to wipe prior owner state.
|
|
5
4
|
|
|
6
5
|
local Channel = require(script.Parent.Channel)
|
|
6
|
+
local Log = require(script.Parent.Parent.util.Log)
|
|
7
7
|
local Types = require(script.Parent.Parent.Types)
|
|
8
8
|
|
|
9
9
|
-- Constants --------------------------------------------------------------
|
|
@@ -20,17 +20,19 @@ local _maxSize = DEFAULT_SIZE
|
|
|
20
20
|
|
|
21
21
|
local Pool = {}
|
|
22
22
|
|
|
23
|
+
-- fullReset wipes the prior owner's XOR baseline and delta caches.
|
|
23
24
|
function Pool.acquire(): Types.ChannelState
|
|
24
25
|
if _depth > 0 then
|
|
25
26
|
local ch = _stack[_depth]
|
|
26
27
|
_stack[_depth] = nil
|
|
27
28
|
_depth -= 1
|
|
28
|
-
Channel.
|
|
29
|
+
Channel.fullReset(ch)
|
|
29
30
|
return ch
|
|
30
31
|
end
|
|
31
32
|
return Channel.create()
|
|
32
33
|
end
|
|
33
34
|
|
|
35
|
+
-- Above the cap, drop on the floor; the GC reclaims the buffer.
|
|
34
36
|
function Pool.release(ch: Types.ChannelState): ()
|
|
35
37
|
if _depth >= _maxSize then
|
|
36
38
|
return
|
|
@@ -41,7 +43,7 @@ end
|
|
|
41
43
|
|
|
42
44
|
function Pool.setMaxSize(size: number): ()
|
|
43
45
|
if size < 0 then
|
|
44
|
-
error(`
|
|
46
|
+
Log.error(`size must be non-negative, got {size}`)
|
|
45
47
|
end
|
|
46
48
|
_maxSize = size
|
|
47
49
|
while _depth > _maxSize do
|
|
@@ -54,4 +56,10 @@ function Pool.count(): number
|
|
|
54
56
|
return _depth
|
|
55
57
|
end
|
|
56
58
|
|
|
59
|
+
function Pool.reset(): ()
|
|
60
|
+
table.clear(_stack)
|
|
61
|
+
_depth = 0
|
|
62
|
+
_maxSize = DEFAULT_SIZE
|
|
63
|
+
end
|
|
64
|
+
|
|
57
65
|
return table.freeze(Pool)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
--!strict
|
|
2
2
|
--!optimize 2
|
|
3
|
-
--
|
|
3
|
+
-- Sequential 7-bit packet/query ID assignment.
|
|
4
4
|
|
|
5
|
+
local Log = require(script.Parent.Parent.util.Log)
|
|
5
6
|
local Signal = require(script.Parent.Parent.api.Signal)
|
|
6
7
|
local Types = require(script.Parent.Parent.Types)
|
|
7
8
|
|
|
@@ -10,13 +11,15 @@ local Types = require(script.Parent.Parent.Types)
|
|
|
10
11
|
local KIND_PACKET = 0
|
|
11
12
|
local KIND_REQUEST = 1
|
|
12
13
|
local KIND_RESPONSE = 2
|
|
14
|
+
|
|
15
|
+
-- 7-bit id leaves the MSB free for the frame single/multi flag (see Constants.FRAME_MSB).
|
|
13
16
|
local MAX_ID = 127
|
|
14
17
|
|
|
15
18
|
-- State ------------------------------------------------------------------
|
|
16
19
|
|
|
17
20
|
local _nextId = 0
|
|
18
|
-
local
|
|
19
|
-
local
|
|
21
|
+
local _registrationsById: { [number]: Types.Registration } = {}
|
|
22
|
+
local _registrationsByName: { [string]: Types.Registration } = {}
|
|
20
23
|
|
|
21
24
|
-- Public -----------------------------------------------------------------
|
|
22
25
|
|
|
@@ -45,10 +48,14 @@ export type RegisterOptions = {
|
|
|
45
48
|
function Registry.register(opts: RegisterOptions): Types.Registration
|
|
46
49
|
local nextCandidate = _nextId + 1
|
|
47
50
|
if nextCandidate > MAX_ID then
|
|
48
|
-
error(`
|
|
51
|
+
Log.error(`ID limit ({MAX_ID}) exceeded; cannot register "{opts.name}"`)
|
|
49
52
|
end
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
--[[
|
|
54
|
+
RESPONSE entries share their request's name with a "\0resp" suffix; only
|
|
55
|
+
request/packet names need uniqueness in the by-name index.
|
|
56
|
+
]]
|
|
57
|
+
if opts.kind ~= KIND_RESPONSE and _registrationsByName[opts.name] ~= nil then
|
|
58
|
+
Log.error(`duplicate name "{opts.name}"`)
|
|
52
59
|
end
|
|
53
60
|
|
|
54
61
|
_nextId = nextCandidate
|
|
@@ -75,36 +82,38 @@ function Registry.register(opts: RegisterOptions): Types.Registration
|
|
|
75
82
|
drops = 0,
|
|
76
83
|
}
|
|
77
84
|
|
|
78
|
-
|
|
85
|
+
_registrationsById[nextCandidate] = reg
|
|
79
86
|
if opts.kind ~= KIND_RESPONSE then
|
|
80
|
-
|
|
87
|
+
_registrationsByName[opts.name] = reg
|
|
81
88
|
end
|
|
82
89
|
return reg
|
|
83
90
|
end
|
|
84
91
|
|
|
85
92
|
function Registry.get(id: number): Types.Registration?
|
|
86
|
-
return
|
|
93
|
+
return _registrationsById[id]
|
|
87
94
|
end
|
|
88
95
|
|
|
89
96
|
function Registry.getByName(name: string): Types.Registration?
|
|
90
|
-
return
|
|
97
|
+
return _registrationsByName[name]
|
|
91
98
|
end
|
|
92
99
|
|
|
93
100
|
function Registry.count(): number
|
|
94
101
|
return _nextId
|
|
95
102
|
end
|
|
96
103
|
|
|
104
|
+
-- IDs are sequential and never freed, so the table has no holes.
|
|
97
105
|
function Registry.all(): { Types.Registration }
|
|
98
106
|
local result = table.create(_nextId)
|
|
99
|
-
local n = 0
|
|
100
107
|
for i = 1, _nextId do
|
|
101
|
-
|
|
102
|
-
if reg then
|
|
103
|
-
n += 1
|
|
104
|
-
result[n] = reg
|
|
105
|
-
end
|
|
108
|
+
result[i] = _registrationsById[i]
|
|
106
109
|
end
|
|
107
110
|
return result
|
|
108
111
|
end
|
|
109
112
|
|
|
113
|
+
function Registry.reset(): ()
|
|
114
|
+
table.clear(_registrationsById)
|
|
115
|
+
table.clear(_registrationsByName)
|
|
116
|
+
_nextId = 0
|
|
117
|
+
end
|
|
118
|
+
|
|
110
119
|
return table.freeze(Registry)
|