@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.
@@ -777,13 +777,18 @@ local function process_full_entity(
777
777
  end
778
778
  end
779
779
 
780
+ -- removals resolve their target with find_entity: a target this client never
781
+ -- saw (filtered out, or deleted earlier in the same packet) has nothing to
782
+ -- remove, and creating a placeholder for it would leak a ghost entity
783
+
780
784
  -- pair tag removal: [target id, relation]
781
785
  local function process_entity_pair_removal(self: Client, entity: Entity, c: Cursor, custom_ids: CustomIdMap)
782
786
  local relation = read_component_id(self, c)
783
787
  local relation_hooks = self.network_hooks.removed[PAIR(self.components.relation, relation)]
784
- process_entity(self, c, custom_ids, function(target)
788
+ local target = find_entity(self, c)
789
+ if target then
785
790
  entity_remove_pair(self, entity, relation, target, relation_hooks)
786
- end)
791
+ end
787
792
  end
788
793
 
789
794
  -- relation target removals: [target ids..., vlq count, relation]
@@ -793,9 +798,10 @@ local function process_entity_relation_removals(self: Client, entity: Entity, c:
793
798
  local relation_hooks = self.network_hooks.removed[PAIR(self.components.relation, relation)]
794
799
 
795
800
  for _ = 1, total_targets do
796
- process_entity(self, c, custom_ids, function(target)
801
+ local target = find_entity(self, c)
802
+ if target then
797
803
  entity_remove_pair(self, entity, relation, target, relation_hooks)
798
- end)
804
+ end
799
805
  end
800
806
  end
801
807
 
@@ -1532,8 +1538,8 @@ local function prune_component_serialization(self: Client, component: Component,
1532
1538
  end
1533
1539
  end
1534
1540
 
1535
- -- serdes take part in the handshake, preprocessors and delta serializers are
1536
- -- local to each side and never mark shared dirty
1541
+ -- serdes and preprocessors take part in the handshake and mark shared dirty,
1542
+ -- delta serializers are local to each side and never do
1537
1543
  function client.set_serdes(self: Client, component: Component, serdes: types.Serdes)
1538
1544
  ensure_component_serialization(self, component).serdes = serdes
1539
1545
  self.is_shared_dirty = true
@@ -1551,6 +1557,7 @@ end
1551
1557
 
1552
1558
  function client.set_preprocessor(self: Client, component: Component, preprocessor: types.Preprocessor)
1553
1559
  ensure_component_serialization(self, component).preprocessor = preprocessor
1560
+ self.is_shared_dirty = true
1554
1561
  end
1555
1562
 
1556
1563
  function client.remove_preprocessor(self: Client, component: Component)
@@ -1560,6 +1567,7 @@ function client.remove_preprocessor(self: Client, component: Component)
1560
1567
  end
1561
1568
  options.preprocessor = nil
1562
1569
  prune_component_serialization(self, component, options)
1570
+ self.is_shared_dirty = true
1563
1571
  end
1564
1572
 
1565
1573
  function client.set_delta_serializer(self: Client, component: Component, delta_serializer: types.DeltaSerializer)
@@ -1575,21 +1583,25 @@ function client.remove_delta_serializer(self: Client, component: Component)
1575
1583
  prune_component_serialization(self, component, options)
1576
1584
  end
1577
1585
 
1578
- -- rebuilds the shared lookups (component ids, custom ids, serdes) when
1579
- -- flagged; cheap no-op otherwise, so every deserialize entry point calls it
1586
+ -- rebuilds the shared lookups (component ids, custom ids, serdes, preprocessors)
1587
+ -- when flagged; cheap no-op otherwise, so every deserialize entry point calls it
1580
1588
  function client.resolve_dirty(self: Client)
1581
1589
  if not self.is_shared_dirty then
1582
1590
  return
1583
1591
  end
1584
1592
 
1585
1593
  local serdes: { [Component]: types.Serdes } = {}
1594
+ local preprocessors: { [Component]: true } = {}
1586
1595
  for component, options in self.component_serialization do
1587
1596
  if options.serdes then
1588
1597
  serdes[component] = options.serdes
1589
1598
  end
1599
+ if options.preprocessor then
1600
+ preprocessors[component] = true
1601
+ end
1590
1602
  end
1591
1603
 
1592
- self.shared = shared.resolve_shared(self.world, self.components, self.registered_custom_ids, serdes)
1604
+ self.shared = shared.resolve_shared(self.world, self.components, self.registered_custom_ids, serdes, preprocessors)
1593
1605
  self.is_shared_dirty = false
1594
1606
  end
1595
1607
 
@@ -1752,6 +1764,8 @@ return {
1752
1764
 
1753
1765
  process_entity_pair = process_entity_pair,
1754
1766
  process_entity_pair_value = process_entity_pair_value,
1767
+ process_entity_pair_removal = process_entity_pair_removal,
1768
+ process_entity_relation_removals = process_entity_relation_removals,
1755
1769
  process_entity_relations = process_entity_relations,
1756
1770
  process_entity_relation_values = process_entity_relation_values,
1757
1771
  process_full_entity = process_full_entity,
@@ -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