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

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.
@@ -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)
@@ -1882,7 +1876,11 @@ function push_relation_updates(
1882
1876
  local is_component = storage.is_component
1883
1877
 
1884
1878
  for target in storage.dirty_targets do
1885
- if WORLD_HAS(world, entity, PAIR(rel, target)) then
1879
+ -- the pair id only keeps the target's index: a target deleted and
1880
+ -- recycled in the same frame aliases the live pair, so the pair must
1881
+ -- resolve back to this exact generation to count as present
1882
+ local id = PAIR(rel, target)
1883
+ if WORLD_HAS(world, entity, id) and PAIR_SECOND(world, id) == target then
1886
1884
  if is_component then
1887
1885
  local changed = bucket.changed[rel]
1888
1886
  if not changed then
@@ -1890,7 +1888,7 @@ function push_relation_updates(
1890
1888
  bucket.changed[rel] = changed
1891
1889
  end
1892
1890
 
1893
- local value = WORLD_GET(world, entity, PAIR(rel, target))
1891
+ local value = WORLD_GET(world, entity, id)
1894
1892
  table.insert(changed.targets, target)
1895
1893
  table.insert(changed.values, SET_VALUE(value))
1896
1894
  else
@@ -2398,31 +2396,47 @@ local function serialize_unreliable_group(
2398
2396
  commit()
2399
2397
  end
2400
2398
 
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
2399
+ -- resolves every bound filter node once and re-applies the ones that moved.
2400
+ -- steady state is one integer compare per distinct node. must run before
2401
+ -- dirty_networked is walked and before the active epoch rolls, since
2402
+ -- edit_*_filter snapshots against last_active_bitmask
2403
+ local function resolve_filters(self: Server)
2404
+ self.registry.cycle += 1
2405
+
2406
+ for node, bindings in self.filter_bindings do
2407
+ local value = filter_groups.resolve(node)
2408
+ local version = node.version
2409
+ if bindings.applied_version == version then
2407
2410
  continue
2408
2411
  end
2409
2412
 
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)
2413
+ for _, entry in bindings.entries do
2414
+ if entry.applied_version == version then
2415
+ continue
2416
+ end
2417
+ entry.applied_version = version
2418
+
2419
+ local tracking_component = entry.tracking
2420
+ if tracking_component then
2421
+ -- routes to the right dirty list via the storage's track_type
2422
+ edit_tracking_filter(
2423
+ self,
2424
+ entry.entity,
2425
+ tracking_component,
2426
+ entry.networked,
2427
+ entry.tracking_storage :: TrackingStorageBase,
2428
+ bitmasks.clone(value)
2429
+ )
2430
+ else
2431
+ edit_networked_filter(self, entry.entity, entry.networked, bitmasks.clone(value))
2432
+ end
2424
2433
  end
2434
+ bindings.applied_version = version
2425
2435
  end
2436
+ end
2437
+
2438
+ local function collect_updates_groups(self: Server): Map<MaskHash, SerializeGroup>
2439
+ resolve_filters(self)
2426
2440
 
2427
2441
  local serialize_groups: Map<MaskHash, SerializeGroup> = {}
2428
2442
  local networked_entities = self.networked
@@ -2931,21 +2945,25 @@ local function serialize_unreliable_groups(self: Server, groups: Map<MaskHash, U
2931
2945
  end
2932
2946
  end
2933
2947
 
2934
- -- rebuilds the shared lookups (component ids, custom ids, serdes) when
2935
- -- flagged; no-op otherwise.
2948
+ -- rebuilds the shared lookups (component ids, custom ids, serdes, preprocessors)
2949
+ -- when flagged; no-op otherwise.
2936
2950
  function server.resolve_dirty(self: Server)
2937
2951
  if not self.is_shared_dirty then
2938
2952
  return
2939
2953
  end
2940
2954
 
2941
2955
  local serdes: { [Component]: types.Serdes } = {}
2956
+ local preprocessors: { [Component]: true } = {}
2942
2957
  for component, options in self.component_serialization do
2943
2958
  if options.serdes then
2944
2959
  serdes[component] = options.serdes
2945
2960
  end
2961
+ if options.preprocessor then
2962
+ preprocessors[component] = true
2963
+ end
2946
2964
  end
2947
2965
 
2948
- self.shared = shared.resolve_shared(self.world, self.components, self.registered_custom_ids, serdes)
2966
+ self.shared = shared.resolve_shared(self.world, self.components, self.registered_custom_ids, serdes, preprocessors)
2949
2967
  self.is_shared_dirty = false
2950
2968
  end
2951
2969
 
@@ -3137,6 +3155,7 @@ end
3137
3155
 
3138
3156
  function server.get_full(self: Server, player: Player): (buffer, Array<any>)
3139
3157
  self:resolve_dirty()
3158
+ resolve_filters(self)
3140
3159
 
3141
3160
  local index = self.registry.client_indexes[player]
3142
3161
  if index == nil then
@@ -3473,6 +3492,7 @@ end
3473
3492
 
3474
3493
  function server.collect_entity(self: Server, entity: Entity): PacketIterator
3475
3494
  self:resolve_dirty()
3495
+ resolve_filters(self)
3476
3496
  local groups = collect_entity_groups(self, entity)
3477
3497
 
3478
3498
  local member_updates: Map<number, MemberUpdatePackets> = {}
@@ -3564,11 +3584,24 @@ end
3564
3584
 
3565
3585
  function server.collect_unreliable(self: Server): PacketIterator
3566
3586
  self:resolve_dirty()
3587
+ resolve_filters(self)
3567
3588
  local groups = collect_unreliable_groups(self)
3568
3589
  serialize_unreliable_groups(self, groups)
3569
3590
  return create_unreliable_packet_iterator(self, distribute_unreliable_groups(groups))
3570
3591
  end
3571
3592
 
3593
+ function server.filter(self: Server, initial: FilterSimple?): FilterNode
3594
+ return filter_groups.create_source(self.registry, initial)
3595
+ end
3596
+
3597
+ function server.filter_predicator(self: Server, predicator: FilterPredicator): FilterNode
3598
+ return filter_groups.create_predicate(self.registry, predicator)
3599
+ end
3600
+
3601
+ function server.everyone(self: Server): FilterNode
3602
+ return filter_groups.create_everyone(self.registry)
3603
+ end
3604
+
3572
3605
  function server.register_player(self: Server, player: Player)
3573
3606
  self.registry:register_client(player)
3574
3607
  end
@@ -3891,7 +3924,7 @@ function server.create(world: World?, components: types.Components?, max_clients
3891
3924
  self.hooked_unreliable_pairs = {}
3892
3925
 
3893
3926
  self.dirty_networked = {}
3894
- self.function_filters = {}
3927
+ self.filter_bindings = {}
3895
3928
 
3896
3929
  self.empty_bitmask = buffer.create(self.bitmask_size)
3897
3930
  self.draft_bitmask = buffer.create(self.bitmask_size)
@@ -3926,12 +3959,19 @@ return {
3926
3959
  end,
3927
3960
  collect_unreliable_groups = function(self: Server): Map<MaskHash, UnreliableSerializeGroup>
3928
3961
  self:resolve_dirty()
3962
+ resolve_filters(self)
3929
3963
  return collect_unreliable_groups(self)
3930
3964
  end,
3931
3965
  serialize_update_groups = serialize_update_groups,
3932
3966
  serialize_unreliable_groups = serialize_unreliable_groups,
3933
3967
  collect_entity_groups = function(self: Server, entity: Entity): Map<MaskHash, EntitySerializeGroup>
3934
3968
  self:resolve_dirty()
3969
+ resolve_filters(self)
3935
3970
  return collect_entity_groups(self, entity)
3936
3971
  end,
3972
+
3973
+ -- unbound source for module level construction, binds on first set_*
3974
+ create_filter = function(initial: FilterSimple?): FilterNode
3975
+ return filter_groups.create_source(nil, initial)
3976
+ end,
3937
3977
  }
@@ -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.5"
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.5",
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",