@rbxts/replecs 0.4.0-rc.3 → 0.4.0-rc.4

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.
@@ -1532,8 +1532,8 @@ local function prune_component_serialization(self: Client, component: Component,
1532
1532
  end
1533
1533
  end
1534
1534
 
1535
- -- serdes take part in the handshake, preprocessors and delta serializers are
1536
- -- local to each side and never mark shared dirty
1535
+ -- serdes and preprocessors take part in the handshake and mark shared dirty,
1536
+ -- delta serializers are local to each side and never do
1537
1537
  function client.set_serdes(self: Client, component: Component, serdes: types.Serdes)
1538
1538
  ensure_component_serialization(self, component).serdes = serdes
1539
1539
  self.is_shared_dirty = true
@@ -1551,6 +1551,7 @@ end
1551
1551
 
1552
1552
  function client.set_preprocessor(self: Client, component: Component, preprocessor: types.Preprocessor)
1553
1553
  ensure_component_serialization(self, component).preprocessor = preprocessor
1554
+ self.is_shared_dirty = true
1554
1555
  end
1555
1556
 
1556
1557
  function client.remove_preprocessor(self: Client, component: Component)
@@ -1560,6 +1561,7 @@ function client.remove_preprocessor(self: Client, component: Component)
1560
1561
  end
1561
1562
  options.preprocessor = nil
1562
1563
  prune_component_serialization(self, component, options)
1564
+ self.is_shared_dirty = true
1563
1565
  end
1564
1566
 
1565
1567
  function client.set_delta_serializer(self: Client, component: Component, delta_serializer: types.DeltaSerializer)
@@ -1575,21 +1577,26 @@ function client.remove_delta_serializer(self: Client, component: Component)
1575
1577
  prune_component_serialization(self, component, options)
1576
1578
  end
1577
1579
 
1578
- -- rebuilds the shared lookups (component ids, custom ids, serdes) when
1579
- -- flagged; cheap no-op otherwise, so every deserialize entry point calls it
1580
+ -- rebuilds the shared lookups (component ids, custom ids, serdes, preprocessors)
1581
+ -- when flagged; cheap no-op otherwise, so every deserialize entry point calls it
1580
1582
  function client.resolve_dirty(self: Client)
1581
1583
  if not self.is_shared_dirty then
1582
1584
  return
1583
1585
  end
1584
1586
 
1585
1587
  local serdes: { [Component]: types.Serdes } = {}
1588
+ local preprocessors: { [Component]: true } = {}
1586
1589
  for component, options in self.component_serialization do
1587
1590
  if options.serdes then
1588
1591
  serdes[component] = options.serdes
1589
1592
  end
1593
+ if options.preprocessor then
1594
+ preprocessors[component] = true
1595
+ end
1590
1596
  end
1591
1597
 
1592
- self.shared = shared.resolve_shared(self.world, self.components, self.registered_custom_ids, serdes)
1598
+ self.shared =
1599
+ shared.resolve_shared(self.world, self.components, self.registered_custom_ids, serdes, preprocessors)
1593
1600
  self.is_shared_dirty = false
1594
1601
  end
1595
1602
 
@@ -13,6 +13,7 @@ function handshake.generate_handshake(shared: types.Shared): types.HandshakeInfo
13
13
  components = {},
14
14
  custom_ids = {},
15
15
  serdes = {},
16
+ preprocessors = {},
16
17
  }
17
18
 
18
19
  for component_name, component in shared.components.lookup do
@@ -36,6 +37,16 @@ function handshake.generate_handshake(shared: types.Shared): types.HandshakeInfo
36
37
  }
37
38
  end
38
39
 
40
+ -- preprocessors are functions, so only their presence is shared
41
+ for component in shared.preprocessors do
42
+ local name = component_to_name[component]
43
+ if not name then
44
+ continue
45
+ end
46
+
47
+ handshake.preprocessors[name] = true
48
+ end
49
+
39
50
  return handshake
40
51
  end
41
52
 
@@ -102,6 +113,30 @@ function handshake.verify_handshake(
102
113
  end
103
114
  end
104
115
 
116
+ for component_name in handshake.preprocessors do
117
+ local component = shared.components.lookup[component_name]
118
+ if not component then
119
+ common.log_warn(
120
+ `{handshake_side} registered a preprocessor for "{component_name}" that {verify_side} doesn't have`
121
+ )
122
+ continue
123
+ end
124
+
125
+ if not shared.preprocessors[component] then
126
+ return false, `missing preprocessor for component "{component_name}" in {verify_side}`
127
+ end
128
+ end
129
+ for component in shared.preprocessors do
130
+ local component_name = component_to_name[component]
131
+ if not component_name then
132
+ continue
133
+ end
134
+
135
+ if not handshake.preprocessors[component_name] then
136
+ return false, `missing preprocessor for component "{component_name}" in {handshake_side}`
137
+ end
138
+ end
139
+
105
140
  return true, nil
106
141
  end
107
142
 
@@ -34,10 +34,42 @@ declare namespace Replecs {
34
34
  | Player
35
35
  | MemberFilterMap
36
36
  | MemberFilterPredicate
37
+ | FilterNode
37
38
  | Record<string, never>
38
39
  | undefined;
39
40
  type Member = unknown;
40
41
 
42
+ /** What a filter combinator accepts: another node or a raw player / set. */
43
+ export type FilterOperand = FilterNode | Player | MemberFilterMap;
44
+
45
+ /**
46
+ * One node of the reactive filter graph. Sources own a player set; derived
47
+ * nodes combine their inputs and re-evaluate lazily on the next collect.
48
+ * Structure is fixed at construction, only the value is reactive.
49
+ */
50
+ export interface FilterNode {
51
+ /** Replaces the member set. A map of `false` values makes a blacklist. Sources only. */
52
+ set(filter: Player | MemberFilterMap): void;
53
+ /** Adds to the member set (on a blacklist source this excludes the player). Sources only. */
54
+ add(player: Player): void;
55
+ /** Sources only. */
56
+ remove(player: Player): void;
57
+ /** Sources only. */
58
+ clear(): void;
59
+
60
+ /** Players in this node and in every operand. */
61
+ filter(...operands: FilterOperand[]): FilterNode;
62
+ /** Players in this node or in any operand. */
63
+ extend(...operands: FilterOperand[]): FilterNode;
64
+ /** Players in this node and in no operand. */
65
+ exclude(...operands: FilterOperand[]): FilterNode;
66
+
67
+ /** Resolves the node and returns its current players. Errors if the node is not bound to a server yet. */
68
+ members(): Map<Player, true>;
69
+ /** Resolves the node and tests one player (aliases accepted). */
70
+ contains(player: Player): boolean;
71
+ }
72
+
41
73
  export interface SharedInfo<T> {
42
74
  lookup: Record<string, T>;
43
75
  keys: string[];
@@ -64,6 +96,7 @@ declare namespace Replecs {
64
96
  components: SharedInfo<Entity>;
65
97
  custom_ids: SharedInfo<CustomId>;
66
98
  serdes: Map<Id, SerdesTable>;
99
+ preprocessors: Map<Id, true>;
67
100
  }
68
101
  interface HandshakeSerdesInfo {
69
102
  includes_variants?: boolean;
@@ -74,6 +107,7 @@ declare namespace Replecs {
74
107
  components: Record<string, boolean>;
75
108
  custom_ids: Record<string, boolean>;
76
109
  serdes: Record<string, HandshakeSerdesInfo>;
110
+ preprocessors: Record<string, boolean>;
77
111
  }
78
112
 
79
113
  export interface Components {
@@ -194,6 +228,13 @@ declare namespace Replecs {
194
228
  }
195
229
 
196
230
  export interface ServerImp extends SerializationOptions {
231
+ /** A source filter bound to this server. */
232
+ filter(initial?: Player | MemberFilterMap): FilterNode;
233
+ /** A filter driven by a predicate over registered players, re-evaluated once per collect. */
234
+ filter_predicator(predicate: MemberFilterPredicate): FilterNode;
235
+ /** Every registered player; the base for blacklists. */
236
+ everyone(): FilterNode;
237
+
197
238
  set_networked(entity: Entity, filter?: MemberFilter): void;
198
239
  set_reliable(
199
240
  entity: Entity,
@@ -289,6 +330,8 @@ declare namespace Replecs {
289
330
  identifier: string,
290
331
  handler?: (ctx: HandleContext) => Entity,
291
332
  ) => CustomId;
333
+ /** An unbound source filter; it attaches to a server the first time a `set_*` call receives it. */
334
+ filter: (initial?: Player | MemberFilterMap) => FilterNode;
292
335
  }
293
336
  }
294
337
 
@@ -10,6 +10,7 @@ local VERSION = require (script:WaitForChild('ver'))
10
10
 
11
11
  export type Server = server.Server
12
12
  export type Client = client.Client
13
+ export type FilterNode = server.FilterNode
13
14
 
14
15
  type Entity = common.Entity
15
16
  type World = common.World
@@ -46,6 +47,7 @@ type Replecs = types.Components & {
46
47
  create_server: (world: World?) -> Server,
47
48
  create_client: (world: World?) -> Client,
48
49
  create_custom_id: (identifier: string, handler: (ctx: HandleContext) -> Entity?) -> CustomId,
50
+ filter: (initial: types.FilterSimple?) -> FilterNode,
49
51
  }
50
52
 
51
53
  local replecs = {}
@@ -103,6 +105,12 @@ function replecs.create_custom_id(identifier: string, handler: (ctx: HandleConte
103
105
  return customid.create(identifier, handler)
104
106
  end
105
107
 
108
+ -- an unbound source filter: usable from any module, it attaches to the server
109
+ -- the first time a set_* call receives it (directly or through a derived node)
110
+ function replecs.filter(initial: types.FilterSimple?): FilterNode
111
+ return server.create_filter(initial)
112
+ end
113
+
106
114
  function replecs.after_replication(lib: ReplecsLib, callback: () -> ())
107
115
  local lib_client = lib.client
108
116
  if lib_client then
@@ -1,196 +1,370 @@
1
+ --!optimize 2
1
2
  local types = require (script.Parent.Parent:WaitForChild('types'))
2
3
  local registry = require (script.Parent:WaitForChild('registry'))
3
4
  local bitmasks = require (script.Parent:WaitForChild('bitmasks'))
4
5
  local utils = require (script.Parent:WaitForChild('utils'))
5
- local common = require (script.Parent.Parent:WaitForChild('common'))
6
6
 
7
7
  type Bitmask = buffer
8
+ type Player = any
8
9
 
9
10
  type FilterSimple = types.FilterSimple
11
+ type FilterPredicator = types.FilterPredicator
10
12
 
11
13
  type Set<T> = { [T]: boolean }
12
- type Map<K, V> = { [K]: V }
13
14
  type Array<T> = { T }
14
-
15
- type Listener = (Bitmask) -> ()
16
-
17
- type ListenerConnections = {
18
- listeners: { Listener },
19
- listeners_indexes: { [Listener]: number },
15
+ type Registry = registry.Registry
16
+
17
+ local KINDS = {
18
+ source = 1,
19
+ predicate = 2,
20
+ intersect = 3,
21
+ union = 4,
22
+ exclude = 5,
20
23
  }
21
24
 
22
- export type FilterMode = number
25
+ export type FilterNodeClass = {
26
+ kind: number,
27
+ registry: Registry?,
23
28
 
24
- export type FilterEntry = {
25
- mode: FilterMode,
26
- filter: FilterSimple,
27
- }
29
+ inputs: Array<FilterNode>,
30
+ input_versions: Array<number>,
31
+ dependents: Set<FilterNode>,
28
32
 
29
- export type FilterGroupClass = {
30
- registry: registry.Registry,
31
- parent: FilterGroup?,
32
33
 
33
- derived: Set<FilterGroup>,
34
- assigned_filter: FilterEntry?,
35
- connected_listeners: ListenerConnections,
36
- }
34
+ value: Bitmask?,
35
+ scratch: Bitmask?,
36
+ version: number,
37
+ dirty: boolean,
38
+ resolved_epoch: number,
39
+ resolved_cycle: number,
37
40
 
38
- local FILTER_MODES = {
39
- extend = 1,
40
- exclude = 2,
41
- include = 3,
41
+ -- source only
42
+ member_set: Set<Player>?,
43
+ inverted: boolean?,
44
+ predicator: FilterPredicator?,
42
45
  }
43
46
 
44
- local filter_group = {}
45
- filter_group.__index = filter_group
47
+ local filter_node = {}
48
+ filter_node.__index = filter_node
46
49
 
47
- export type FilterGroup = FilterGroupClass & typeof(filter_group)
50
+ export type FilterNode = FilterNodeClass & typeof(filter_node)
51
+ export type FilterOperand = FilterNode | FilterSimple
48
52
 
49
- function filter_group.is_filter_group(value: any): boolean
50
- return typeof(value) == "table" and getmetatable(value) == filter_group
53
+ function filter_node.is_filter_node(value: any): boolean
54
+ return typeof(value) == "table" and getmetatable(value) == filter_node
51
55
  end
52
56
 
53
- local function fire_listeners(self: FilterGroup, bitmask: Bitmask)
54
- for _, listener in self.connected_listeners.listeners do
55
- listener(bitmask)
57
+ function filter_node.bind(node: FilterNode, reg: Registry)
58
+ if node.registry == reg then
59
+ return
60
+ end
61
+ if node.registry then
62
+ error "filter is already bound to a different server"
63
+ end
64
+
65
+ node.registry = reg
66
+ node.value = buffer.create(reg.bitmask_size)
67
+ node.scratch = buffer.create(reg.bitmask_size)
68
+
69
+ for _, input in node.inputs do
70
+ filter_node.bind(input, reg)
56
71
  end
57
72
  end
58
73
 
59
- function filter_group.connect(self: FilterGroup, listener: Listener)
60
- local connected = self.connected_listeners
61
- local index = #connected.listeners + 1
62
- connected.listeners[index] = listener
63
- connected.listeners_indexes[listener] = index
74
+ local function create_node(kind: number, reg: Registry?): FilterNode
75
+ local self: FilterNodeClass = {
76
+ kind = kind,
77
+ registry = nil,
78
+
79
+ inputs = {},
80
+ input_versions = {},
81
+ dependents = setmetatable({}, { __mode = "k" }) :: any,
82
+
83
+ value = nil,
84
+ scratch = nil,
85
+ version = 0,
86
+ dirty = true,
87
+ resolved_epoch = -1,
88
+ resolved_cycle = -1,
89
+
90
+ member_set = nil,
91
+ inverted = nil,
92
+ predicator = nil,
93
+ }
94
+ setmetatable(self, filter_node)
95
+
96
+ local node = self :: any
97
+ if reg then
98
+ filter_node.bind(node, reg)
99
+ end
100
+ return node
64
101
  end
65
102
 
66
- function filter_group.disconnect(self: FilterGroup, listener: Listener)
67
- local connected = self.connected_listeners
68
- local index = connected.listeners_indexes[listener]
69
- if not index then
103
+ local function mark_dirty(node: FilterNode)
104
+ if node.dirty then
70
105
  return
71
106
  end
72
-
73
- local last_listener = table.remove(connected.listeners)
74
- if last_listener and last_listener ~= listener then
75
- connected.listeners[index] = last_listener
76
- connected.listeners_indexes[last_listener] = index
107
+ node.dirty = true
108
+ for dependent in node.dependents do
109
+ mark_dirty(dependent)
77
110
  end
78
- connected.listeners_indexes[listener] = nil
79
111
  end
80
112
 
81
- local function create_filter_bitmask(self: FilterGroup, filter: FilterSimple): Bitmask
82
- if typeof(filter) == "table" then
83
- return self.registry:create_bitmask_from_set(filter)
84
- else
85
- return self.registry:create_bitmask_from_member(filter)
113
+ local function assert_source(node: FilterNode, method: string)
114
+ if node.kind ~= KINDS.source then
115
+ error(`{method}() is only available on source filters, derived filters are read only`)
86
116
  end
87
117
  end
88
118
 
89
- -- the filter bitmask is resolved from the raw filter here, at calculation
90
- -- time, so clients registered after the filter was assigned still resolve
91
- function filter_group.apply_filter(self: FilterGroup, bitmask: Bitmask)
92
- local filter = self.assigned_filter
93
- if not filter then
94
- return
119
+
120
+ local function resolve_source(node: FilterNode, reg: Registry, scratch: Bitmask)
121
+ bitmasks.clear(scratch)
122
+ for member in node.member_set :: Set<Player> do
123
+ local index = registry.resolve_member(reg, member)
124
+ if index then
125
+ bitmasks.set(scratch, index)
126
+ end
95
127
  end
128
+ if node.inverted then
129
+ bitmasks.exclude(reg.registered_bitmask, scratch, scratch)
130
+ end
131
+ end
96
132
 
97
- local filter_bitmask = create_filter_bitmask(self, filter.filter)
98
133
 
99
- if filter.mode == FILTER_MODES.extend then
100
- bitmasks.bor(bitmask, filter_bitmask, bitmask)
101
- elseif filter.mode == FILTER_MODES.exclude then
102
- bitmasks.exclude(bitmask, filter_bitmask, bitmask)
103
- elseif filter.mode == FILTER_MODES.include then
104
- bitmasks.band(bitmask, filter_bitmask, bitmask)
134
+ local function resolve_predicate(node: FilterNode, reg: Registry, scratch: Bitmask)
135
+ bitmasks.clear(scratch)
136
+ local predicator = node.predicator :: FilterPredicator
137
+ for member, index in reg.client_indexes do
138
+ if predicator(member) then
139
+ bitmasks.set(scratch, index)
140
+ end
105
141
  end
106
142
  end
107
143
 
108
- function filter_group.calculate_up(self: FilterGroup): Bitmask
109
- local parent = self.parent
110
- local bitmask: Bitmask
111
- if parent then
112
- bitmask = filter_group.calculate_up(parent)
113
- else
114
- bitmask = bitmasks.create_filled(self.registry.client_next_index)
144
+
145
+ local function resolve_computed(node: FilterNode, scratch: Bitmask): boolean
146
+ local inputs = node.inputs
147
+ local input_versions = node.input_versions
148
+
149
+ local changed = false
150
+ for i, input in inputs do
151
+ filter_node.resolve(input)
152
+ if input.version ~= input_versions[i] then
153
+ changed = true
154
+ end
155
+ end
156
+ if not changed then
157
+ return false
158
+ end
159
+
160
+ for i, input in inputs do
161
+ input_versions[i] = input.version
115
162
  end
116
163
 
117
- filter_group.apply_filter(self, bitmask)
118
- return bitmask
164
+ bitmasks.load(inputs[1].value :: Bitmask, scratch)
165
+ local kind = node.kind
166
+ local op: (Bitmask, Bitmask, Bitmask) -> () = if kind == KINDS.intersect
167
+ then bitmasks.band
168
+ elseif kind == KINDS.union then bitmasks.bor
169
+ else bitmasks.exclude
170
+ for i = 2, #inputs do
171
+ op(scratch, inputs[i].value :: Bitmask, scratch)
172
+ end
173
+ return true
119
174
  end
120
175
 
121
- function filter_group.calculate_down(self: FilterGroup, parent_bitmask: Bitmask)
122
- local bitmask = bitmasks.clone(parent_bitmask, bitmasks.get_buffer_size(self.registry.client_next_index))
123
- filter_group.apply_filter(self, bitmask)
124
176
 
125
- fire_listeners(self, bitmask)
177
+ function filter_node.resolve(node: FilterNode): Bitmask
178
+ local reg = node.registry
179
+ if not reg then
180
+ error "filter is not bound to a server, pass it to a set_* call first"
181
+ end
182
+ local value = node.value :: Bitmask
183
+ local kind = node.kind
184
+
185
+ if kind == KINDS.predicate then
186
+ if node.resolved_cycle == reg.cycle then
187
+ return value
188
+ end
189
+ node.resolved_cycle = reg.cycle
190
+ elseif not node.dirty and node.resolved_epoch == reg.epoch then
191
+ return value
192
+ end
193
+ node.dirty = false
194
+ node.resolved_epoch = reg.epoch
195
+
196
+ local scratch = node.scratch :: Bitmask
197
+ if kind == KINDS.source then
198
+ resolve_source(node, reg, scratch)
199
+ elseif kind == KINDS.predicate then
200
+ resolve_predicate(node, reg, scratch)
201
+ elseif not resolve_computed(node, scratch) then
202
+ return value
203
+ end
126
204
 
127
- for derived in self.derived do
128
- filter_group.calculate_down(derived, bitmask)
205
+ if bitmasks.equals(scratch, value) then
206
+ return value
129
207
  end
208
+ node.value = scratch
209
+ node.scratch = value
210
+ node.version += 1
211
+ return scratch
130
212
  end
131
213
 
132
- local function recalculate_filter_group(group: FilterGroup)
133
- local parent = group.parent
214
+ function filter_node.set(self: FilterNode, filter: FilterSimple)
215
+ assert_source(self, "set")
134
216
 
135
- local parent_bitmask: Bitmask
136
- if parent then
137
- parent_bitmask = filter_group.calculate_up(parent)
217
+ local members: Set<Player> = {}
218
+ local inverted = false
219
+ if typeof(filter) == "table" then
220
+ inverted = not utils.is_include_filter(filter)
221
+ for member, allow in filter :: Set<Player> do
222
+ if allow == inverted then
223
+ error "a filter set cannot mix included and excluded players"
224
+ end
225
+ members[member] = true
226
+ end
138
227
  else
139
- parent_bitmask = bitmasks.create_filled(group.registry.client_next_index)
228
+ members[filter] = true
140
229
  end
141
230
 
142
- filter_group.calculate_down(group, parent_bitmask)
231
+ self.member_set = members
232
+ self.inverted = inverted
233
+ mark_dirty(self)
143
234
  end
144
235
 
145
- function filter_group.extend(self: FilterGroup, filter: FilterSimple)
146
- if not utils.get_used_filter_type(filter) then
147
- common.log_error "extend() only accepts include filters"
236
+ function filter_node.add(self: FilterNode, player: Player)
237
+ assert_source(self, "add")
238
+ local members = self.member_set :: Set<Player>
239
+ if members[player] then
240
+ return
148
241
  end
242
+ members[player] = true
243
+ mark_dirty(self)
244
+ end
149
245
 
150
- self.assigned_filter = {
151
- mode = FILTER_MODES.extend,
152
- filter = filter,
153
- }
154
- recalculate_filter_group(self)
246
+ function filter_node.remove(self: FilterNode, player: Player)
247
+ assert_source(self, "remove")
248
+ local members = self.member_set :: Set<Player>
249
+ if not members[player] then
250
+ return
251
+ end
252
+ members[player] = nil
253
+ mark_dirty(self)
254
+ end
255
+
256
+ function filter_node.clear(self: FilterNode)
257
+ assert_source(self, "clear")
258
+ local members = self.member_set :: Set<Player>
259
+ if next(members) == nil then
260
+ return
261
+ end
262
+ table.clear(members)
263
+ mark_dirty(self)
155
264
  end
156
265
 
157
- function filter_group.filter(self: FilterGroup, filter: FilterSimple)
158
- local mode = FILTER_MODES.exclude
159
266
 
160
- if utils.get_used_filter_type(filter) then
161
- mode = FILTER_MODES.include
267
+ function filter_node.create_source(reg: Registry?, initial: FilterSimple?): FilterNode
268
+ local node = create_node(KINDS.source, reg)
269
+ node.member_set = {}
270
+ node.inverted = false
271
+ if initial ~= nil then
272
+ filter_node.set(node, initial)
162
273
  end
274
+ return node
275
+ end
163
276
 
164
- self.assigned_filter = {
165
- mode = mode,
166
- filter = filter,
167
- }
168
- recalculate_filter_group(self)
277
+ function filter_node.create_everyone(reg: Registry?): FilterNode
278
+ local node = create_node(KINDS.source, reg)
279
+ node.member_set = {}
280
+ node.inverted = true
281
+ return node
169
282
  end
170
283
 
171
- function filter_group.create(client_registry: registry.Registry): FilterGroup
172
- local self: FilterGroupClass = {
173
- registry = client_registry,
284
+ function filter_node.create_predicate(reg: Registry?, predicator: FilterPredicator): FilterNode
285
+ local node = create_node(KINDS.predicate, reg)
286
+ node.predicator = predicator
287
+ return node
288
+ end
174
289
 
175
- parent = nil,
176
- connected_listeners = {
177
- listeners = {},
178
- listeners_indexes = {},
179
- },
180
- derived = setmetatable({}, { __mode = "k" }) :: any,
181
- assigned_filter = nil,
182
- }
290
+ local function to_node(operand: FilterOperand): FilterNode
291
+ if filter_node.is_filter_node(operand) then
292
+ return operand :: FilterNode
293
+ end
294
+ return filter_node.create_source(nil, operand :: FilterSimple)
295
+ end
296
+
297
+ local function create_computed(kind: number, base: FilterNode, operands: Array<FilterOperand>): FilterNode
298
+ local node = create_node(kind, nil)
299
+ local inputs = node.inputs
300
+ local input_versions = node.input_versions
301
+
302
+ inputs[1] = base
303
+ input_versions[1] = -1
304
+ local reg = base.registry
305
+
306
+ for i, operand in operands do
307
+ local input = to_node(operand)
308
+ inputs[i + 1] = input
309
+ input_versions[i + 1] = -1
310
+ reg = reg or input.registry
311
+ end
183
312
 
184
- setmetatable(self, filter_group)
185
- return self :: any
313
+ for _, input in inputs do
314
+ input.dependents[node] = true
315
+ end
316
+ if reg then
317
+ filter_node.bind(node, reg)
318
+ end
319
+ return node
320
+ end
321
+
322
+ function filter_node.filter(self: FilterNode, ...: FilterOperand): FilterNode
323
+ return create_computed(KINDS.intersect, self, { ... })
324
+ end
325
+
326
+ function filter_node.extend(self: FilterNode, ...: FilterOperand): FilterNode
327
+ return create_computed(KINDS.union, self, { ... })
328
+ end
329
+
330
+ function filter_node.exclude(self: FilterNode, ...: FilterOperand): FilterNode
331
+ return create_computed(KINDS.exclude, self, { ... })
186
332
  end
187
333
 
188
- function filter_group.derive(self: FilterGroup): FilterGroup
189
- local derived = filter_group.create(self.registry)
190
- derived.parent = self
191
334
 
192
- self.derived[derived] = true
193
- return derived
335
+ function filter_node.members(self: FilterNode): Set<Player>
336
+ local value = filter_node.resolve(self)
337
+ local index_clients = (self.registry :: Registry).index_clients
338
+
339
+ local members: Set<Player> = {}
340
+ bitmasks.iter_set(value, function(index)
341
+ local client = index_clients[index]
342
+ if client ~= nil then
343
+ members[client] = true
344
+ end
345
+ end)
346
+ return members
347
+ end
348
+
349
+ local function lookup_index(reg: Registry, player: Player): number?
350
+ local index = reg.client_indexes[player]
351
+ if index then
352
+ return index
353
+ end
354
+ local aliased = reg.client_aliases[player]
355
+ if aliased == nil then
356
+ return nil
357
+ end
358
+ return reg.client_indexes[aliased]
359
+ end
360
+
361
+ function filter_node.contains(self: FilterNode, player: Player): boolean
362
+ local value = filter_node.resolve(self)
363
+ local index = lookup_index(self.registry :: Registry, player)
364
+ if index == nil then
365
+ return false
366
+ end
367
+ return bitmasks.test(value, index)
194
368
  end
195
369
 
196
- return filter_group
370
+ return filter_node
@@ -21,10 +21,11 @@ type FilterAny = types.FilterAny
21
21
  type FilterSet = types.FilterSet
22
22
  type FilterPredicator = types.FilterPredicator
23
23
  type FilterPlayer = types.FilterPlayer
24
- type FilterGroup = filter_groups.FilterGroup
24
+ type FilterSimple = types.FilterSimple
25
+ export type FilterNode = filter_groups.FilterNode
25
26
 
26
27
  -- what the public set_* API accepts as a filter
27
- type FilterInput = FilterAny | FilterGroup
28
+ type FilterInput = FilterAny | FilterNode
28
29
 
29
30
  type MaskHash = string
30
31
 
@@ -57,6 +58,10 @@ export type ServerImp = {
57
58
 
58
59
  force_change: (self: Server, entity: Entity, component: Component) -> (),
59
60
 
61
+ filter: (self: Server, initial: FilterSimple?) -> FilterNode,
62
+ filter_predicator: (self: Server, predicator: FilterPredicator) -> FilterNode,
63
+ everyone: (self: Server) -> FilterNode,
64
+
60
65
  set_global: (self: Server, entity: Entity, global_id: number) -> (),
61
66
  remove_global: (self: Server, entity: Entity) -> (),
62
67
  set_custom: (self: Server, entity: Entity, custom: Component | CustomId) -> (),
@@ -76,10 +81,10 @@ type StorageBase = {
76
81
  filter: Bitmask?,
77
82
  last_replicated: Bitmask?,
78
83
 
79
- -- set while a filter group drives this storage's filter; the listener is
80
- -- kept so it can be disconnected when the filter changes or the storage dies
81
- filter_group: FilterGroup?,
82
- filter_listener: ((Bitmask) -> ())?,
84
+ -- set while a filter node drives this storage's filter, with the slot of
85
+ -- this storage in the node's binding list so unbind is a swap remove
86
+ filter_node: FilterNode?,
87
+ binding_index: number?,
83
88
  filter_input: FilterInput?,
84
89
  }
85
90
 
@@ -135,15 +140,23 @@ type PostponedComponent = {
135
140
  once: boolean?,
136
141
  }
137
142
 
138
- -- a predicate-driven filter, re-evaluated before every collect_updates cycle
139
- type FunctionFilterEntry = {
140
- predicator: registry.MemberPredicator,
143
+ -- a storage driven by a filter node, re-applied whenever the node's version moves
144
+ type FilterBinding = {
141
145
  entity: Entity,
142
146
  networked: NetworkedStorage,
147
+ storage: StorageBase,
143
148
 
144
- -- set for reliable component filters, nil for entity-level ones
149
+ -- set for component filters, nil for entity-level ones
145
150
  tracking: Component?,
146
151
  tracking_storage: TrackingStorageBase?,
152
+ -- node version last written into the storage filter
153
+ applied_version: number,
154
+ }
155
+
156
+ type NodeBindings = {
157
+ entries: Array<FilterBinding>,
158
+ -- every entry sits at this version once a resolve pass completes
159
+ applied_version: number,
147
160
  }
148
161
 
149
162
  type NetworkedReferencesInfo = {
@@ -204,7 +217,7 @@ export type Server = ServerImp & {
204
217
  hooked_relations: Map<Component, HookedInfo>,
205
218
 
206
219
  dirty_networked: Array<Entity>,
207
- function_filters: Map<StorageBase, FunctionFilterEntry>,
220
+ filter_bindings: Map<FilterNode, NodeBindings>,
208
221
 
209
222
  empty_bitmask: Bitmask,
210
223
  draft_bitmask: Bitmask,
@@ -443,20 +456,28 @@ local function resolve_filter_bitmask(self: Server, filter: FilterAny): Bitmask
443
456
  end
444
457
  end
445
458
 
446
- local function disconnect_filter_group(storage: StorageBase)
447
- local group = storage.filter_group
448
- local listener = storage.filter_listener
449
- if group and listener then
450
- group:disconnect(listener)
459
+ -- releases the node driving this storage's filter, if any. the entry is swap
460
+ -- removed and the moved entry's storage takes over the freed slot
461
+ local function clear_filter_source(self: Server, storage: StorageBase)
462
+ local node = storage.filter_node
463
+ if not node then
464
+ return
451
465
  end
452
- storage.filter_group = nil
453
- storage.filter_listener = nil
454
- end
466
+ local bindings = self.filter_bindings[node]
467
+ local index = storage.binding_index :: number
468
+ local entries = bindings.entries
455
469
 
456
- -- releases whatever was driving this storage's filter (group or predicate)
457
- local function clear_filter_source(self: Server, storage: StorageBase)
458
- disconnect_filter_group(storage)
459
- self.function_filters[storage] = nil
470
+ local last = table.remove(entries)
471
+ if last and last.storage ~= storage then
472
+ entries[index] = last
473
+ last.storage.binding_index = index
474
+ end
475
+ if #entries == 0 then
476
+ self.filter_bindings[node] = nil
477
+ end
478
+
479
+ storage.filter_node = nil
480
+ storage.binding_index = nil
460
481
  end
461
482
 
462
483
  -- a nil-filter storage inherits entity visibility, so its replicated set is
@@ -528,29 +549,62 @@ local function edit_tracking_filter(
528
549
  mark_tracking_storage_dirty(self, entity, component, networked, storage, DIRTY_FILTER_FLAG)
529
550
  end
530
551
 
531
- local function connect_networked_group(self: Server, entity: Entity, networked: NetworkedStorage, group: FilterGroup)
532
- local function listener(bitmask: Bitmask)
533
- edit_networked_filter(self, entity, networked, bitmask)
534
- end
535
- networked.filter_group = group
536
- networked.filter_listener = listener
537
- group:connect(listener)
552
+ -- attaches a node to a storage and returns the node's current mask as the
553
+ -- storage's own copy: storage filters get holes punched at remove_player and
554
+ -- are snapshotted in place, so they never alias the node buffer
555
+ local function bind_filter(
556
+ self: Server,
557
+ entity: Entity,
558
+ component: Component?,
559
+ networked: NetworkedStorage,
560
+ storage: StorageBase,
561
+ node: FilterNode
562
+ ): Bitmask
563
+ filter_groups.bind(node, self.registry)
564
+ local value = filter_groups.resolve(node)
565
+
566
+ local bindings = self.filter_bindings[node]
567
+ if not bindings then
568
+ bindings = { entries = {}, applied_version = node.version }
569
+ self.filter_bindings[node] = bindings
570
+ end
571
+ local entries = bindings.entries
572
+ table.insert(entries, {
573
+ entity = entity,
574
+ networked = networked,
575
+ storage = storage,
576
+ tracking = component,
577
+ tracking_storage = if component then storage :: TrackingStorageBase else nil,
578
+ applied_version = node.version,
579
+ })
580
+ storage.filter_node = node
581
+ storage.binding_index = #entries
582
+
583
+ return bitmasks.clone(value)
538
584
  end
539
585
 
540
- local function connect_tracking_group(
586
+ -- resolves a filter input into a bitmask and, for nodes and predicates,
587
+ -- binds the source so later changes keep driving the storage. nil means
588
+ -- "replicate to all active clients", resolved at collect
589
+ local function resolve_filter_input(
541
590
  self: Server,
542
591
  entity: Entity,
543
- component: Component,
592
+ component: Component?,
544
593
  networked: NetworkedStorage,
545
- storage: TrackingStorageBase,
546
- group: FilterGroup
547
- )
548
- local function listener(bitmask: Bitmask)
549
- edit_tracking_filter(self, entity, component, networked, storage, bitmask)
594
+ storage: StorageBase,
595
+ filter: FilterInput?
596
+ ): Bitmask?
597
+ if filter == nil then
598
+ return nil
599
+ end
600
+ -- a node is a table, so this check goes before the set one
601
+ if filter_groups.is_filter_node(filter) then
602
+ return bind_filter(self, entity, component, networked, storage, filter :: FilterNode)
603
+ elseif typeof(filter) == "function" then
604
+ local node = filter_groups.create_predicate(self.registry, filter :: FilterPredicator)
605
+ return bind_filter(self, entity, component, networked, storage, node)
550
606
  end
551
- storage.filter_group = group
552
- storage.filter_listener = listener
553
- group:connect(listener)
607
+ return resolve_filter_bitmask(self, filter :: FilterAny)
554
608
  end
555
609
 
556
610
  function ensure_hooked_component(self: Server, component: Component): HookedInfo
@@ -796,9 +850,6 @@ local function clear_networked_references(self: Server, entity: Entity, networke
796
850
  end
797
851
  end
798
852
 
799
- -- resolves a filter input into a bitmask and attaches its driving source
800
- -- (group listener or predicate entry) to the storage; nil filter means
801
- -- "replicate to all active clients", resolved at collect
802
853
  local function apply_tracking_filter(
803
854
  self: Server,
804
855
  entity: Entity,
@@ -807,31 +858,7 @@ local function apply_tracking_filter(
807
858
  storage: TrackingStorageBase,
808
859
  filter: FilterInput?
809
860
  ): Bitmask?
810
- local bitmask: Bitmask?
811
-
812
- if filter then
813
- if filter_groups.is_filter_group(filter) then
814
- local group = filter :: FilterGroup
815
- bitmask = group:calculate_up()
816
- connect_tracking_group(self, entity, component, networked, storage, group)
817
- elseif typeof(filter) == "function" then
818
- local predicator = filter :: registry.MemberPredicator
819
- bitmask = self.registry:create_bitmask_from_function(predicator)
820
-
821
- self.function_filters[storage] = {
822
- predicator = predicator,
823
- entity = entity,
824
- networked = networked,
825
-
826
- tracking = component,
827
- tracking_storage = storage,
828
- }
829
- else
830
- bitmask = resolve_filter_bitmask(self, filter :: FilterAny)
831
- end
832
- end
833
-
834
- return bitmask
861
+ return resolve_filter_input(self, entity, component, networked, storage, filter)
835
862
  end
836
863
 
837
864
  local function change_tracking(
@@ -1097,28 +1124,8 @@ function start_networked(self: Server, entity: Entity, filter: FilterInput?)
1097
1124
  -- get a removal delta instead of the pending deletion
1098
1125
  change_networked(self, entity, networked, filter)
1099
1126
  else
1100
- local bitmask: Bitmask?
1101
-
1102
- if filter then
1103
- if filter_groups.is_filter_group(filter) then
1104
- local group = filter :: FilterGroup
1105
- bitmask = group:calculate_up()
1106
- connect_networked_group(self, entity, networked, group)
1107
- elseif typeof(filter) == "function" then
1108
- local predicator = filter :: registry.MemberPredicator
1109
- bitmask = self.registry:create_bitmask_from_function(predicator)
1110
-
1111
- self.function_filters[networked] = {
1112
- predicator = predicator,
1113
- entity = entity,
1114
- networked = networked,
1115
- }
1116
- else
1117
- bitmask = resolve_filter_bitmask(self, filter :: FilterAny)
1118
- end
1119
- -- bitmask stays nil: replicate to all active clients, resolved at collect
1120
-
1121
- end
1127
+ -- nil bitmask: replicate to all active clients, resolved at collect
1128
+ local bitmask = resolve_filter_input(self, entity, nil, networked, networked, filter)
1122
1129
 
1123
1130
  networked.filter = bitmask
1124
1131
  mark_networked_dirty(self, entity, networked, DIRTY_FILTER_FLAG)
@@ -1164,24 +1171,7 @@ function change_networked(self: Server, entity: Entity, networked: NetworkedStor
1164
1171
  -- old filter source stops driving this storage before the new one takes over
1165
1172
  clear_filter_source(self, networked)
1166
1173
 
1167
- local bitmask: Bitmask?
1168
- if filter then
1169
- if filter_groups.is_filter_group(filter) then
1170
- local group = filter :: FilterGroup
1171
- bitmask = group:calculate_up()
1172
- connect_networked_group(self, entity, networked, group)
1173
- elseif typeof(filter) == "function" then
1174
- local predicator = filter :: registry.MemberPredicator
1175
- bitmask = self.registry:create_bitmask_from_function(predicator)
1176
- self.function_filters[networked] = {
1177
- predicator = predicator,
1178
- entity = entity,
1179
- networked = networked,
1180
- }
1181
- else
1182
- bitmask = resolve_filter_bitmask(self, filter :: FilterAny)
1183
- end
1184
- end
1174
+ local bitmask = resolve_filter_input(self, entity, nil, networked, networked, filter)
1185
1175
 
1186
1176
  edit_networked_filter(self, entity, networked, bitmask)
1187
1177
  end
@@ -1605,8 +1595,11 @@ function server.remove_serdes(self: Server, component: Component)
1605
1595
  self.is_shared_dirty = true
1606
1596
  end
1607
1597
 
1598
+ -- serdes and preprocessors take part in the handshake and mark shared dirty,
1599
+ -- delta serializers are local to each side and never do
1608
1600
  function server.set_preprocessor(self: Server, component: Component, preprocessor: types.Preprocessor)
1609
1601
  ensure_component_serialization(self, component).preprocessor = preprocessor
1602
+ self.is_shared_dirty = true
1610
1603
  end
1611
1604
 
1612
1605
  function server.remove_preprocessor(self: Server, component: Component)
@@ -1616,6 +1609,7 @@ function server.remove_preprocessor(self: Server, component: Component)
1616
1609
  end
1617
1610
  options.preprocessor = nil
1618
1611
  prune_component_serialization(self, component, options)
1612
+ self.is_shared_dirty = true
1619
1613
  end
1620
1614
 
1621
1615
  function server.set_delta_serializer(self: Server, component: Component, delta_serializer: types.DeltaSerializer)
@@ -2398,31 +2392,47 @@ local function serialize_unreliable_group(
2398
2392
  commit()
2399
2393
  end
2400
2394
 
2401
- local function collect_updates_groups(self: Server): Map<MaskHash, SerializeGroup>
2402
- local client_registry = self.registry
2403
- for storage, entry in self.function_filters do
2404
- local bitmask = client_registry:create_bitmask_from_function(entry.predicator)
2405
- local current = storage.filter
2406
- if current and bitmasks.equals(current, bitmask) then
2395
+ -- resolves every bound filter node once and re-applies the ones that moved.
2396
+ -- steady state is one integer compare per distinct node. must run before
2397
+ -- dirty_networked is walked and before the active epoch rolls, since
2398
+ -- edit_*_filter snapshots against last_active_bitmask
2399
+ local function resolve_filters(self: Server)
2400
+ self.registry.cycle += 1
2401
+
2402
+ for node, bindings in self.filter_bindings do
2403
+ local value = filter_groups.resolve(node)
2404
+ local version = node.version
2405
+ if bindings.applied_version == version then
2407
2406
  continue
2408
2407
  end
2409
2408
 
2410
- local tracking_component = entry.tracking
2411
-
2412
- if tracking_component then
2413
- -- routes to the right dirty list via the storage's track_type
2414
- edit_tracking_filter(
2415
- self,
2416
- entry.entity,
2417
- tracking_component,
2418
- entry.networked,
2419
- entry.tracking_storage :: TrackingStorageBase,
2420
- bitmask
2421
- )
2422
- else
2423
- edit_networked_filter(self, entry.entity, entry.networked, bitmask)
2409
+ for _, entry in bindings.entries do
2410
+ if entry.applied_version == version then
2411
+ continue
2412
+ end
2413
+ entry.applied_version = version
2414
+
2415
+ local tracking_component = entry.tracking
2416
+ if tracking_component then
2417
+ -- routes to the right dirty list via the storage's track_type
2418
+ edit_tracking_filter(
2419
+ self,
2420
+ entry.entity,
2421
+ tracking_component,
2422
+ entry.networked,
2423
+ entry.tracking_storage :: TrackingStorageBase,
2424
+ bitmasks.clone(value)
2425
+ )
2426
+ else
2427
+ edit_networked_filter(self, entry.entity, entry.networked, bitmasks.clone(value))
2428
+ end
2424
2429
  end
2430
+ bindings.applied_version = version
2425
2431
  end
2432
+ end
2433
+
2434
+ local function collect_updates_groups(self: Server): Map<MaskHash, SerializeGroup>
2435
+ resolve_filters(self)
2426
2436
 
2427
2437
  local serialize_groups: Map<MaskHash, SerializeGroup> = {}
2428
2438
  local networked_entities = self.networked
@@ -2931,21 +2941,26 @@ local function serialize_unreliable_groups(self: Server, groups: Map<MaskHash, U
2931
2941
  end
2932
2942
  end
2933
2943
 
2934
- -- rebuilds the shared lookups (component ids, custom ids, serdes) when
2935
- -- flagged; no-op otherwise.
2944
+ -- rebuilds the shared lookups (component ids, custom ids, serdes, preprocessors)
2945
+ -- when flagged; no-op otherwise.
2936
2946
  function server.resolve_dirty(self: Server)
2937
2947
  if not self.is_shared_dirty then
2938
2948
  return
2939
2949
  end
2940
2950
 
2941
2951
  local serdes: { [Component]: types.Serdes } = {}
2952
+ local preprocessors: { [Component]: true } = {}
2942
2953
  for component, options in self.component_serialization do
2943
2954
  if options.serdes then
2944
2955
  serdes[component] = options.serdes
2945
2956
  end
2957
+ if options.preprocessor then
2958
+ preprocessors[component] = true
2959
+ end
2946
2960
  end
2947
2961
 
2948
- self.shared = shared.resolve_shared(self.world, self.components, self.registered_custom_ids, serdes)
2962
+ self.shared =
2963
+ shared.resolve_shared(self.world, self.components, self.registered_custom_ids, serdes, preprocessors)
2949
2964
  self.is_shared_dirty = false
2950
2965
  end
2951
2966
 
@@ -3137,6 +3152,7 @@ end
3137
3152
 
3138
3153
  function server.get_full(self: Server, player: Player): (buffer, Array<any>)
3139
3154
  self:resolve_dirty()
3155
+ resolve_filters(self)
3140
3156
 
3141
3157
  local index = self.registry.client_indexes[player]
3142
3158
  if index == nil then
@@ -3473,6 +3489,7 @@ end
3473
3489
 
3474
3490
  function server.collect_entity(self: Server, entity: Entity): PacketIterator
3475
3491
  self:resolve_dirty()
3492
+ resolve_filters(self)
3476
3493
  local groups = collect_entity_groups(self, entity)
3477
3494
 
3478
3495
  local member_updates: Map<number, MemberUpdatePackets> = {}
@@ -3564,11 +3581,24 @@ end
3564
3581
 
3565
3582
  function server.collect_unreliable(self: Server): PacketIterator
3566
3583
  self:resolve_dirty()
3584
+ resolve_filters(self)
3567
3585
  local groups = collect_unreliable_groups(self)
3568
3586
  serialize_unreliable_groups(self, groups)
3569
3587
  return create_unreliable_packet_iterator(self, distribute_unreliable_groups(groups))
3570
3588
  end
3571
3589
 
3590
+ function server.filter(self: Server, initial: FilterSimple?): FilterNode
3591
+ return filter_groups.create_source(self.registry, initial)
3592
+ end
3593
+
3594
+ function server.filter_predicator(self: Server, predicator: FilterPredicator): FilterNode
3595
+ return filter_groups.create_predicate(self.registry, predicator)
3596
+ end
3597
+
3598
+ function server.everyone(self: Server): FilterNode
3599
+ return filter_groups.create_everyone(self.registry)
3600
+ end
3601
+
3572
3602
  function server.register_player(self: Server, player: Player)
3573
3603
  self.registry:register_client(player)
3574
3604
  end
@@ -3891,7 +3921,7 @@ function server.create(world: World?, components: types.Components?, max_clients
3891
3921
  self.hooked_unreliable_pairs = {}
3892
3922
 
3893
3923
  self.dirty_networked = {}
3894
- self.function_filters = {}
3924
+ self.filter_bindings = {}
3895
3925
 
3896
3926
  self.empty_bitmask = buffer.create(self.bitmask_size)
3897
3927
  self.draft_bitmask = buffer.create(self.bitmask_size)
@@ -3926,12 +3956,19 @@ return {
3926
3956
  end,
3927
3957
  collect_unreliable_groups = function(self: Server): Map<MaskHash, UnreliableSerializeGroup>
3928
3958
  self:resolve_dirty()
3959
+ resolve_filters(self)
3929
3960
  return collect_unreliable_groups(self)
3930
3961
  end,
3931
3962
  serialize_update_groups = serialize_update_groups,
3932
3963
  serialize_unreliable_groups = serialize_unreliable_groups,
3933
3964
  collect_entity_groups = function(self: Server, entity: Entity): Map<MaskHash, EntitySerializeGroup>
3934
3965
  self:resolve_dirty()
3966
+ resolve_filters(self)
3935
3967
  return collect_entity_groups(self, entity)
3936
3968
  end,
3969
+
3970
+ -- unbound source for module level construction, binds on first set_*
3971
+ create_filter = function(initial: FilterSimple?): FilterNode
3972
+ return filter_groups.create_source(nil, initial)
3973
+ end,
3937
3974
  }
@@ -29,6 +29,18 @@ type RegistryClass = {
29
29
  -- exactly this many bits, so an index past it would fall off the buffer
30
30
  max_clients: number,
31
31
  bitmask_size: number,
32
+
33
+ -- bumped when a client registers, leaves or an alias changes: filter
34
+ -- nodes re-resolve their raw members against it
35
+ epoch: number,
36
+ -- bumped once per server resolve pass, predicate nodes recompute per cycle
37
+ cycle: number,
38
+ -- bit per registered client, the base a blacklist filter subtracts from
39
+ registered_bitmask: Bitmask,
40
+ -- clients dropped by unregister_client. a filter may still name them and
41
+ -- is_client_valid holds inside PlayerRemoving, so resolve_member must not
42
+ -- register them again (possibly onto their freed index)
43
+ departed: Set<Client>,
32
44
  }
33
45
 
34
46
  local registry = {}
@@ -56,20 +68,23 @@ local function resolve_member(self: Registry, member: Client): number?
56
68
  end
57
69
  end
58
70
 
59
- if not utils.is_client_valid(member) then
71
+ if self.departed[member] or not utils.is_client_valid(member) then
60
72
  return nil
61
73
  end
62
74
 
63
75
  registry.register_client(self, member)
64
76
  return self.client_indexes[member]
65
77
  end
78
+ registry.resolve_member = resolve_member
66
79
 
67
80
  function registry.add_alias(self: Registry, client: Client, alias: any)
68
81
  self.client_aliases[alias] = client
82
+ self.epoch += 1
69
83
  end
70
84
 
71
85
  function registry.remove_alias(self: Registry, alias: any)
72
86
  self.client_aliases[alias] = nil
87
+ self.epoch += 1
73
88
  end
74
89
 
75
90
  -- drops every alias pointing at the client
@@ -80,6 +95,7 @@ function registry.remove_client_aliases(self: Registry, client: Client)
80
95
  aliases[alias] = nil
81
96
  end
82
97
  end
98
+ self.epoch += 1
83
99
  end
84
100
 
85
101
 
@@ -143,6 +159,10 @@ function registry.register_client(self: Registry, member: Client)
143
159
  self.client_indexes[member] = index
144
160
  self.index_clients[index] = member
145
161
  self.client_count += 1
162
+
163
+ self.departed[member] = nil
164
+ bitmasks.set(self.registered_bitmask, index)
165
+ self.epoch += 1
146
166
  end
147
167
 
148
168
  function registry.unregister_client(self: Registry, member: Client)
@@ -155,6 +175,10 @@ function registry.unregister_client(self: Registry, member: Client)
155
175
  self.client_indexes[member] = nil
156
176
  self.index_clients[index] = nil
157
177
  self.client_count -= 1
178
+
179
+ self.departed[member] = true
180
+ bitmasks.reset(self.registered_bitmask, index)
181
+ self.epoch += 1
158
182
  end
159
183
 
160
184
  local DEFAULT_MAX_CLIENTS = 128
@@ -172,6 +196,11 @@ function registry.create(max_clients: number?): Registry
172
196
  self.free_indexes = {}
173
197
  self.client_aliases = {}
174
198
 
199
+ self.epoch = 0
200
+ self.cycle = 0
201
+ self.registered_bitmask = buffer.create(self.bitmask_size)
202
+ self.departed = setmetatable({}, { __mode = "k" }) :: any
203
+
175
204
  return setmetatable(self, registry) :: any
176
205
  end
177
206
 
@@ -38,7 +38,8 @@ function shared.resolve_shared(
38
38
  world: World,
39
39
  components: types.Components,
40
40
  registered_custom_ids: { [customid.CustomId]: boolean },
41
- serdes: { [Component]: types.Serdes }
41
+ serdes: { [Component]: types.Serdes },
42
+ preprocessors: { [Component]: true }
42
43
  ): types.Shared
43
44
  local shared = {} :: types.Shared
44
45
 
@@ -61,6 +62,7 @@ function shared.resolve_shared(
61
62
  end
62
63
 
63
64
  shared.serdes = serdes
65
+ shared.preprocessors = preprocessors
64
66
  shared.components = create_shared_lookup(components_lookup)
65
67
  shared.custom_ids = create_shared_lookup(custom_ids_lookup) :: types.SharedInfo<customid.CustomId>
66
68
 
@@ -29,6 +29,8 @@ export type HandshakeInfo = {
29
29
  includes_variants: boolean?,
30
30
  bytespan: number?,
31
31
  } },
32
+ -- preprocessors are functions, so only their presence is compared
33
+ preprocessors: { [string]: boolean },
32
34
  }
33
35
 
34
36
  export type Serdes<T = any> = {
@@ -77,6 +79,7 @@ export type Shared = {
77
79
  custom_ids: SharedInfo<customid.CustomId>,
78
80
 
79
81
  serdes: { [Component]: Serdes },
82
+ preprocessors: { [Component]: true },
80
83
  }
81
84
 
82
85
  return nil
@@ -1 +1 @@
1
- return "v0.4.0-rc.3"
1
+ return "v0.4.0-rc.4"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rbxts/replecs",
3
- "version": "0.4.0-rc.3",
3
+ "version": "0.4.0-rc.4",
4
4
  "description": "Replication library for Jecs",
5
5
  "main": "out/init.luau",
6
6
  "types": "out/index.d.ts",
@@ -24,7 +24,8 @@
24
24
  "!**/*.tsbuildinfo"
25
25
  ],
26
26
  "scripts": {
27
- "serve": "rojo sourcemap serve.project.json -o sourcemap.json --watch && rojo serve serve.project.json"
27
+ "serve": "rojo sourcemap serve.project.json -o sourcemap.json --watch && rojo serve serve.project.json",
28
+ "docs:dev": "npm run dev --prefix docs"
28
29
  },
29
30
  "repository": {
30
31
  "type": "git",