@axpecter/lync 2.0.0 → 2.1.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.
Files changed (55) hide show
  1. package/README.md +475 -426
  2. package/package.json +1 -1
  3. package/src/Types.luau +63 -31
  4. package/src/api/Group.luau +101 -106
  5. package/src/api/Packet.luau +187 -186
  6. package/src/api/Query.luau +223 -284
  7. package/src/api/Scope.luau +46 -57
  8. package/src/api/Signal.luau +104 -137
  9. package/src/codec/Base.luau +69 -20
  10. package/src/codec/composite/Array.luau +179 -270
  11. package/src/codec/composite/Map.luau +97 -347
  12. package/src/codec/composite/Optional.luau +27 -24
  13. package/src/codec/composite/Shared.luau +96 -65
  14. package/src/codec/composite/Struct.luau +200 -360
  15. package/src/codec/composite/Tagged.luau +62 -187
  16. package/src/codec/composite/Tuple.luau +79 -103
  17. package/src/codec/datatype/Buffer.luau +49 -21
  18. package/src/codec/datatype/CFrame.luau +207 -30
  19. package/src/codec/datatype/Color.luau +21 -11
  20. package/src/codec/datatype/Instance.luau +28 -21
  21. package/src/codec/datatype/IntVector.luau +33 -21
  22. package/src/codec/datatype/NumberRange.luau +19 -10
  23. package/src/codec/datatype/Ray.luau +26 -22
  24. package/src/codec/datatype/Rect.luau +20 -16
  25. package/src/codec/datatype/Region.luau +51 -45
  26. package/src/codec/datatype/Sequence.luau +64 -56
  27. package/src/codec/datatype/String.luau +89 -56
  28. package/src/codec/datatype/UDim.luau +40 -22
  29. package/src/codec/datatype/Vector.luau +181 -17
  30. package/src/codec/meta/Auto.luau +295 -253
  31. package/src/codec/meta/Bitfield.luau +104 -148
  32. package/src/codec/meta/Custom.luau +8 -4
  33. package/src/codec/meta/Enum.luau +29 -57
  34. package/src/codec/meta/Float.luau +64 -0
  35. package/src/codec/meta/Nothing.luau +9 -5
  36. package/src/codec/meta/Unknown.luau +44 -36
  37. package/src/codec/primitive/Bool.luau +16 -26
  38. package/src/codec/primitive/Float16.luau +58 -64
  39. package/src/codec/primitive/Int.luau +68 -0
  40. package/src/codec/primitive/Number.luau +21 -43
  41. package/src/codec/primitive/Varint.luau +67 -46
  42. package/src/index.d.ts +249 -335
  43. package/src/init.luau +319 -207
  44. package/src/internal/Baseline.luau +29 -24
  45. package/src/internal/Channel.luau +333 -278
  46. package/src/internal/Middleware.luau +60 -85
  47. package/src/internal/Pool.luau +19 -30
  48. package/src/internal/Registry.luau +56 -113
  49. package/src/transport/Bridge.luau +64 -43
  50. package/src/transport/Client.luau +59 -170
  51. package/src/transport/Gate.luau +282 -142
  52. package/src/transport/Reader.luau +148 -140
  53. package/src/transport/Server.luau +138 -488
  54. package/src/api/Namespace.luau +0 -256
  55. package/src/codec/meta/Quantized.luau +0 -174
package/src/init.luau CHANGED
@@ -1,247 +1,359 @@
1
1
  --!strict
2
2
  --!optimize 2
3
- -- Public API.
3
+ -- Lync: buffer networking for Roblox.
4
4
 
5
5
  local RunService = game:GetService("RunService")
6
6
 
7
- -- Codec: primitive
8
- local BoolCodec = require(script.codec.primitive.Bool)
9
- local Float16 = require(script.codec.primitive.Float16)
10
- local NumberCodec = require(script.codec.primitive.Number)
11
-
12
- -- Codec: datatype
13
- local BufferCodec = require(script.codec.datatype.Buffer)
14
- local CFrameCodec = require(script.codec.datatype.CFrame)
15
- local ColorCodec = require(script.codec.datatype.Color)
16
- local InstanceCodec = require(script.codec.datatype.Instance)
17
- local IntVectorCodec = require(script.codec.datatype.IntVector)
18
- local NumberRangeCodec = require(script.codec.datatype.NumberRange)
19
- local RayCodec = require(script.codec.datatype.Ray)
20
- local RectCodec = require(script.codec.datatype.Rect)
21
- local RegionCodec = require(script.codec.datatype.Region)
22
- local SequenceCodec = require(script.codec.datatype.Sequence)
23
- local StringCodec = require(script.codec.datatype.String)
24
- local UDimCodec = require(script.codec.datatype.UDim)
25
- local VectorCodec = require(script.codec.datatype.Vector)
26
-
27
- -- Codec: composite
28
- local Array = require(script.codec.composite.Array)
29
- local Map = require(script.codec.composite.Map)
30
- local Optional = require(script.codec.composite.Optional)
31
- local Struct = require(script.codec.composite.Struct)
32
- local Tagged = require(script.codec.composite.Tagged)
33
- local Tuple = require(script.codec.composite.Tuple)
34
-
35
- -- Codec: meta
36
- local Auto = require(script.codec.meta.Auto)
37
- local Bitfield = require(script.codec.meta.Bitfield)
38
- local Custom = require(script.codec.meta.Custom)
39
- local EnumCodec = require(script.codec.meta.Enum)
40
- local Nothing = require(script.codec.meta.Nothing)
41
- local Quantized = require(script.codec.meta.Quantized)
42
- local Unknown = require(script.codec.meta.Unknown)
43
-
44
- -- Internal
7
+ local Base = require(script.codec.Base)
45
8
  local Channel = require(script.internal.Channel)
9
+ local Gate = require(script.transport.Gate)
10
+ local Group = require(script.api.Group)
46
11
  local Middleware = require(script.internal.Middleware)
12
+ local Packet = require(script.api.Packet)
47
13
  local Pool = require(script.internal.Pool)
48
-
49
- -- Transport
50
- local Client = require(script.transport.Client)
51
- local Gate = require(script.transport.Gate)
52
- local Server = require(script.transport.Server)
53
-
54
- -- API
55
- local GroupModule = require(script.api.Group)
56
- local NamespaceModule = require(script.api.Namespace)
57
- local PacketModule = require(script.api.Packet)
58
- local QueryModule = require(script.api.Query)
59
- local ScopeModule = require(script.api.Scope)
60
- local SignalModule = require(script.api.Signal)
61
-
62
- -- Type re-exports -----------------------------------------------------
63
-
14
+ local Query = require(script.api.Query)
15
+ local Registry = require(script.internal.Registry)
16
+ local Scope = require(script.api.Scope)
64
17
  local Types = require(script.Types)
65
18
 
66
- export type Codec<T> = Types.Codec<T>
67
- export type InternalCodec<T> = Types.InternalCodec<T>
68
- export type Connection = Types.Connection
69
- export type RateLimitConfig = Types.RateLimitConfig
70
- export type PacketConfig<T> = Types.PacketConfig<T>
71
- export type QueryConfig<R, S> = Types.QueryConfig<R, S>
72
-
73
- export type Packet = PacketModule.Packet
74
- export type Query = QueryModule.Query
75
- export type Group = GroupModule.Group
76
- export type Scope = ScopeModule.Scope
77
- export type Namespace = NamespaceModule.Namespace
78
- export type Signal = SignalModule.Signal
79
- export type NamespaceConfig = Types.NamespaceConfig
19
+ -- Codecs
20
+ local ArrayC = require(script.codec.composite.Array)
21
+ local AutoC = require(script.codec.meta.Auto)
22
+ local BitfieldC = require(script.codec.meta.Bitfield)
23
+ local BoolC = require(script.codec.primitive.Bool)
24
+ local BufferC = require(script.codec.datatype.Buffer)
25
+ local CFrameC = require(script.codec.datatype.CFrame)
26
+ local ColorC = require(script.codec.datatype.Color)
27
+ local CustomC = require(script.codec.meta.Custom)
28
+ local EnumC = require(script.codec.meta.Enum)
29
+ local Float16C = require(script.codec.primitive.Float16)
30
+ local FloatC = require(script.codec.meta.Float)
31
+ local IVC = require(script.codec.datatype.IntVector)
32
+ local InstanceC = require(script.codec.datatype.Instance)
33
+ local IntC = require(script.codec.primitive.Int)
34
+ local MapC = require(script.codec.composite.Map)
35
+ local NRC = require(script.codec.datatype.NumberRange)
36
+ local NothingC = require(script.codec.meta.Nothing)
37
+ local NumberC = require(script.codec.primitive.Number)
38
+ local OptionalC = require(script.codec.composite.Optional)
39
+ local RayC = require(script.codec.datatype.Ray)
40
+ local RectC = require(script.codec.datatype.Rect)
41
+ local RegionC = require(script.codec.datatype.Region)
42
+ local SeqC = require(script.codec.datatype.Sequence)
43
+ local StringC = require(script.codec.datatype.String)
44
+ local StructC = require(script.codec.composite.Struct)
45
+ local TaggedC = require(script.codec.composite.Tagged)
46
+ local TupleC = require(script.codec.composite.Tuple)
47
+ local UDimC = require(script.codec.datatype.UDim)
48
+ local UnknownC = require(script.codec.meta.Unknown)
49
+ local VectorC = require(script.codec.datatype.Vector)
80
50
 
81
51
  -- Constants -----------------------------------------------------------
82
52
 
83
- local ALL = table.freeze({ _tag = "all" })
53
+ local IS_SERVER = RunService:IsServer()
54
+
55
+ -- State ---------------------------------------------------------------
56
+
57
+ local _started = false
58
+ local _flushRate = 60
59
+ local _flushAccum = 0
60
+ local _flushConn: RBXScriptConnection? = nil
84
61
 
85
62
  -- Private -------------------------------------------------------------
86
63
 
64
+ local DROP = table.freeze({ _lyncDrop = true })
65
+ local ALL = table.freeze({ _lyncAll = true })
66
+
87
67
  local function except(...: any): any
88
- local count = select("#", ...)
89
- local set = {} :: { [Player]: true }
90
- for i = 1, count do
91
- set[select(i, ...)] = true
68
+ local excluded: { [Player]: boolean } = {}
69
+
70
+ for i = 1, select("#", ...) do
71
+ local arg = select(i, ...)
72
+ if typeof(arg) == "Instance" and (arg :: Instance):IsA("Player") then
73
+ excluded[arg :: Player] = true
74
+ elseif typeof(arg) == "table" and (arg :: any)._lyncGroup then
75
+ for player in (arg :: any)._members do
76
+ excluded[player] = true
77
+ end
78
+ else
79
+ error("[Lync] except: argument must be a Player or Group")
80
+ end
92
81
  end
93
- return { _tag = "except", _set = set }
82
+
83
+ return table.freeze({ _lyncExcept = true, _excluded = excluded })
94
84
  end
95
85
 
96
- local function start(): ()
97
- if RunService:IsServer() then
98
- Server.start()
86
+ local function setupFlush(): ()
87
+ local flushFn: () -> ()
88
+
89
+ if IS_SERVER then
90
+ local Server = require(script.transport.Server)
91
+ flushFn = function(): ()
92
+ Server.flush()
93
+ end
99
94
  else
100
- Client.start()
95
+ local Client = require(script.transport.Client)
96
+ flushFn = function(): ()
97
+ Client.flush()
98
+ end
101
99
  end
100
+
101
+ _flushConn = RunService.Heartbeat:Connect(function(dt: number)
102
+ if _flushRate >= 60 then
103
+ flushFn()
104
+ else
105
+ _flushAccum += dt
106
+ local interval = 1 / _flushRate
107
+ if _flushAccum >= interval then
108
+ _flushAccum -= interval
109
+ -- Prevent drift accumulation
110
+ if _flushAccum >= interval then
111
+ _flushAccum = 0
112
+ end
113
+ flushFn()
114
+ end
115
+ end
116
+ end)
102
117
  end
103
118
 
104
119
  -- Public --------------------------------------------------------------
105
120
 
106
- local IS_SERVER = RunService:IsServer()
121
+ local Lync: any = {}
107
122
 
108
- local function flush(): ()
109
- if IS_SERVER then
110
- Server.flush()
111
- else
112
- Client.flush()
123
+ -- Lifecycle -----------------------------------------------------------
124
+
125
+ function Lync.configure(options: Types.ConfigureOptions): ()
126
+ if _started then
127
+ error("[Lync] Lync.configure: must be called before Lync.start()")
128
+ end
129
+
130
+ if options.channelMaxSize then
131
+ local size = options.channelMaxSize
132
+ if size < 4096 or size > 1048576 then
133
+ error("[Lync] Lync.configure: channelMaxSize must be 4096-1048576")
134
+ end
135
+ Channel.setMaxSize(size)
136
+ Base.setMaxSize(size)
137
+ end
138
+
139
+ if options.validationDepth then
140
+ local depth = options.validationDepth
141
+ if depth < 4 or depth > 32 then
142
+ error("[Lync] Lync.configure: validationDepth must be 4-32")
143
+ end
144
+ Gate.setDepth(depth)
145
+ end
146
+
147
+ if options.poolSize then
148
+ local size = options.poolSize
149
+ if size < 2 or size > 128 then
150
+ error("[Lync] Lync.configure: poolSize must be 2-128")
151
+ end
152
+ Pool.setMaxSize(size)
153
+ end
154
+
155
+ if options.bandwidthLimit then
156
+ Gate.configureBandwidth(options.bandwidthLimit.softLimit, options.bandwidthLimit.maxStrikes)
157
+ end
158
+
159
+ if options.globalRateLimit then
160
+ Gate.configureGlobalRateLimit(options.globalRateLimit.maxPerSecond)
161
+ end
162
+
163
+ if options.stats then
164
+ Channel.enableStats()
165
+ if IS_SERVER then
166
+ local Server = require(script.transport.Server)
167
+ Server.enableStats()
168
+ end
113
169
  end
114
170
  end
115
171
 
116
- local function setFlushRate(hz: number): ()
172
+ function Lync.start(): ()
173
+ if _started then
174
+ error("[Lync] Lync.start: already started")
175
+ end
176
+ _started = true
177
+
117
178
  if IS_SERVER then
118
- Server.setFlushRate(hz)
179
+ local Server = require(script.transport.Server)
180
+ Server.start()
119
181
  else
120
- Client.setFlushRate(hz)
182
+ local Client = require(script.transport.Client)
183
+ Client.start()
121
184
  end
122
- end
123
185
 
124
- local function getPlayerStats(player: Player): any?
125
- return Server.getPlayerStats(player)
186
+ setupFlush()
126
187
  end
127
188
 
128
- local function resetStats(): ()
129
- Server.resetStats()
189
+ -- `Lync.started` is a read-only property backed by _started
190
+ Lync.started = false
191
+
192
+ -- Definitions ---------------------------------------------------------
193
+
194
+ Lync.packet = Packet.define
195
+ Lync.query = Query.define
196
+ Lync.group = Group.create
197
+ Lync.scope = Scope.create
198
+
199
+ -- Targeting -----------------------------------------------------------
200
+
201
+ Lync.all = ALL
202
+ Lync.except = except
203
+ Lync.DROP = DROP
204
+
205
+ -- Middleware ----------------------------------------------------------
206
+
207
+ Lync.onSend = Middleware.onSend
208
+ Lync.onReceive = Middleware.onReceive
209
+ Lync.onDrop = Middleware.onDrop
210
+
211
+ -- Runtime control -----------------------------------------------------
212
+
213
+ function Lync.flush(): ()
214
+ if not _started then
215
+ error("[Lync] Lync.flush: must call Lync.start() first")
216
+ end
217
+
218
+ if IS_SERVER then
219
+ local Server = require(script.transport.Server)
220
+ Server.flush()
221
+ else
222
+ local Client = require(script.transport.Client)
223
+ Client.flush()
224
+ end
225
+
226
+ -- Reset accumulator to prevent double-flush
227
+ _flushAccum = 0
130
228
  end
131
229
 
132
- local function setBandwidthLimit(softLimit: number, maxStrikes: number): ()
133
- Server.setBandwidthLimit(softLimit, maxStrikes)
230
+ function Lync.flushRate(hz: number): ()
231
+ if hz < 1 or hz > 60 then
232
+ error("[Lync] Lync.flushRate: hz must be 1-60")
233
+ end
234
+ _flushRate = hz
235
+ _flushAccum = 0
134
236
  end
135
237
 
136
- local Lync = {
137
- VERSION = "2.0.0",
138
-
139
- -- Lifecycle
140
- start = start,
141
-
142
- -- Definition
143
- definePacket = PacketModule.define,
144
- defineQuery = QueryModule.define,
145
- defineNamespace = NamespaceModule.define,
146
-
147
- -- Primitives
148
- u8 = NumberCodec.u8,
149
- u16 = NumberCodec.u16,
150
- u32 = NumberCodec.u32,
151
- i8 = NumberCodec.i8,
152
- i16 = NumberCodec.i16,
153
- i32 = NumberCodec.i32,
154
- f32 = NumberCodec.f32,
155
- f64 = NumberCodec.f64,
156
- bool = BoolCodec.bool,
157
- f16 = Float16.f16,
158
-
159
- -- Datatypes
160
- string = StringCodec.string,
161
- boundedString = StringCodec.bounded,
162
- vec2 = VectorCodec.vec2,
163
- vec3 = VectorCodec.vec3,
164
- cframe = CFrameCodec.cframe,
165
- color3 = ColorCodec.color3,
166
- inst = InstanceCodec.inst,
167
- buff = BufferCodec.buff,
168
- udim = UDimCodec.udim,
169
- udim2 = UDimCodec.udim2,
170
- numberRange = NumberRangeCodec.numberRange,
171
- rect = RectCodec.rect,
172
- vec2int16 = IntVectorCodec.vec2int16,
173
- vec3int16 = IntVectorCodec.vec3int16,
174
- region3 = RegionCodec.region3,
175
- region3int16 = RegionCodec.region3int16,
176
- ray = RayCodec.ray,
177
- numberSequence = SequenceCodec.numberSequence,
178
- colorSequence = SequenceCodec.colorSequence,
179
-
180
- -- Composites
181
- struct = Struct.struct,
182
- deltaStruct = Struct.deltaStruct,
183
- deltaArray = Array.deltaArray,
184
- deltaMap = Map.deltaMap,
185
- array = Array.array,
186
- map = Map.map,
187
- optional = Optional.optional,
188
- tuple = Tuple.define,
189
- tagged = Tagged.define,
190
-
191
- -- Meta
192
- enum = EnumCodec.define,
193
- quantizedFloat = Quantized.float,
194
- quantizedVec3 = Quantized.vec3,
195
- bitfield = Bitfield.define,
196
- custom = Custom.define,
197
-
198
- nothing = Nothing.nothing,
199
- unknown = Unknown.unknown,
200
- auto = Auto.auto,
201
-
202
- -- Hooks
203
- onDrop = Gate.onDrop,
204
- onSend = Middleware.addSend,
205
- onReceive = Middleware.addReceive,
206
-
207
- -- Target descriptors
208
- all = ALL,
209
- except = except,
210
-
211
- -- Middleware sentinel
212
- DROP = Middleware.DROP,
213
-
214
- -- Groups
215
- createGroup = GroupModule.create,
216
-
217
- -- Scope
218
- scope = ScopeModule.create,
219
-
220
- -- Configuration (call before start)
221
- setChannelMaxSize = Channel.setMaxSize,
222
- setValidationDepth = Gate.setMaxDepth,
223
- setPoolSize = Pool.setMaxSize,
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
-
237
- -- Introspection
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,
244
- }
245
-
246
- Lync.default = Lync
247
- return table.freeze(Lync)
238
+ -- Stats ---------------------------------------------------------------
239
+
240
+ Lync.stats = table.freeze({
241
+ player = function(player: Player): Types.PlayerStats?
242
+ if not IS_SERVER then
243
+ return nil
244
+ end
245
+ local Server = require(script.transport.Server)
246
+ return Server.getPlayerStats(player)
247
+ end,
248
+
249
+ reset = function(): ()
250
+ if IS_SERVER then
251
+ local Server = require(script.transport.Server)
252
+ Server.resetStats()
253
+ end
254
+
255
+ -- Reset per-registration stats
256
+ for _, reg in Registry.all() do
257
+ reg.bytesSent = 0
258
+ reg.bytesReceived = 0
259
+ reg.fires = 0
260
+ reg.recvFires = 0
261
+ reg.drops = 0
262
+ end
263
+ end,
264
+ })
265
+
266
+ -- Debug ---------------------------------------------------------------
267
+
268
+ Lync.debug = table.freeze({
269
+ capture = function(_label: string?): ()
270
+ -- TODO: Implement capture recording
271
+ end,
272
+
273
+ stop = function(): ()
274
+ -- TODO: Implement capture stop
275
+ end,
276
+
277
+ dump = function(): ()
278
+ -- TODO: Implement capture dump
279
+ end,
280
+
281
+ pending = function(): number
282
+ return Query.pendingCount()
283
+ end,
284
+
285
+ registrations = function(): { any }
286
+ local regs = Registry.all()
287
+ local result = table.create(#regs)
288
+
289
+ for i, reg in regs do
290
+ result[i] = table.freeze({
291
+ name = reg.name,
292
+ id = reg.id,
293
+ kind = reg.kind,
294
+ isUnreliable = reg.isUnreliable,
295
+ })
296
+ end
297
+
298
+ return table.freeze(result)
299
+ end,
300
+ })
301
+
302
+ -- Codecs --------------------------------------------------------------
303
+
304
+ Lync.int = IntC.int
305
+ Lync.float = FloatC.float
306
+ Lync.f16 = Float16C.f16
307
+ Lync.f32 = NumberC.f32
308
+ Lync.f64 = NumberC.f64
309
+ Lync.bool = BoolC.bool
310
+ Lync.string = StringC
311
+ Lync.buff = BufferC.buff
312
+ Lync.vec2 = VectorC.vec2
313
+ Lync.vec3 = VectorC.vec3
314
+ Lync.cframe = CFrameC.cframe
315
+ Lync.color3 = ColorC.color3
316
+ Lync.inst = InstanceC.inst
317
+ Lync.udim = UDimC.udim
318
+ Lync.udim2 = UDimC.udim2
319
+ Lync.numberRange = NRC.numberRange
320
+ Lync.rect = RectC.rect
321
+ Lync.ray = RayC.ray
322
+ Lync.region3 = RegionC.region3
323
+ Lync.region3int16 = RegionC.region3int16
324
+ Lync.vec2int16 = IVC.vec2int16
325
+ Lync.vec3int16 = IVC.vec3int16
326
+ Lync.numberSequence = SeqC.numberSequence
327
+ Lync.colorSequence = SeqC.colorSequence
328
+ Lync.struct = StructC.struct
329
+ Lync.deltaStruct = StructC.deltaStruct
330
+ Lync.array = ArrayC.array
331
+ Lync.deltaArray = ArrayC.deltaArray
332
+ Lync.map = MapC.map
333
+ Lync.deltaMap = MapC.deltaMap
334
+ Lync.optional = OptionalC.optional
335
+ Lync.tuple = TupleC.define
336
+ Lync.tagged = TaggedC.define
337
+ Lync.enum = EnumC.define
338
+ Lync.bitfield = BitfieldC.define
339
+ Lync.custom = CustomC.define
340
+ Lync.nothing = NothingC.nothing
341
+ Lync.unknown = UnknownC.unknown
342
+ Lync.auto = AutoC.auto
343
+
344
+ -- Wrap in metatable for `Lync.started` property -----------------------
345
+
346
+ local LyncProxy = setmetatable({}, {
347
+ __index = function(_self: any, key: string): any
348
+ if key == "started" then
349
+ return _started
350
+ end
351
+ return Lync[key]
352
+ end,
353
+
354
+ __newindex = function(): ()
355
+ error("[Lync] Lync: module table is read-only")
356
+ end,
357
+ })
358
+
359
+ return LyncProxy
@@ -1,45 +1,50 @@
1
1
  --!strict
2
2
  --!optimize 2
3
- -- Read-side baseline cache keyed by source(player or client sentinel).
3
+ -- Read-side delta cache for delta codecs.
4
4
 
5
5
  -- State ---------------------------------------------------------------
6
6
 
7
- local _readKey = false :: any
8
- local _readCaches = {} :: { [any]: { [number]: any } }
7
+ local _caches: { [number]: { [any]: any } } = {}
8
+ local _currentKey: any = false
9
+ local _hasDelta = false
9
10
 
10
11
  -- Public --------------------------------------------------------------
11
12
 
12
13
  local Baseline = {}
13
14
 
14
- -- One-way latch: set to true on first delta write, never reset. This
15
- -- avoids a setReadKey call on every non-delta frame at the cost of a
16
- -- cheap branch once deltas are first used in the session.
17
- Baseline.hasDelta = false
18
-
19
15
  function Baseline.setReadKey(key: any): ()
20
- _readKey = key
16
+ _currentKey = key
21
17
  end
22
18
 
23
- function Baseline.getCache(deltaId: number): any
24
- local map = _readCaches[_readKey]
25
- return map and map[deltaId]
19
+ function Baseline.getCache(deltaId: number): any?
20
+ local perPlayer = _caches[deltaId]
21
+ if not perPlayer then
22
+ return nil
23
+ end
24
+ return perPlayer[_currentKey]
26
25
  end
27
26
 
28
- function Baseline.setCache(deltaId: number, data: any): ()
29
- if not Baseline.hasDelta then
30
- Baseline.hasDelta = true
27
+ function Baseline.setCache(deltaId: number, value: any): ()
28
+ local perPlayer = _caches[deltaId]
29
+ if not perPlayer then
30
+ perPlayer = {}
31
+ _caches[deltaId] = perPlayer
31
32
  end
32
- local map = _readCaches[_readKey]
33
- if not map then
34
- map = {}
35
- _readCaches[_readKey] = map
33
+ perPlayer[_currentKey] = value
34
+ end
35
+
36
+ function Baseline.clearPlayer(key: any): ()
37
+ for _, perPlayer in _caches do
38
+ perPlayer[key] = nil
36
39
  end
37
- map[deltaId] = data
38
40
  end
39
41
 
40
- function Baseline.clearSource(key: any): ()
41
- _readCaches[key] = nil
42
+ function Baseline.markHasDelta(): ()
43
+ _hasDelta = true
44
+ end
45
+
46
+ function Baseline.hasDelta(): boolean
47
+ return _hasDelta
42
48
  end
43
49
 
44
- -- Not frozen: hasDelta is mutated at runtime by setCache.
45
- return Baseline
50
+ return table.freeze(Baseline)