@rbxts/replecs 0.4.0-rc.2.ts.1 → 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.
@@ -891,20 +891,11 @@ local function process_entity_updates(
891
891
  end
892
892
  end
893
893
 
894
- -- one update group: [u16 group mask, deleted ids, added ids, per entity
895
- -- blocks] in read order. deletions come first so a server id reused by an
896
- -- addition in the same group starts from a fresh entity
894
+ -- one update group: [u16 group mask, added ids, per entity blocks, deleted
895
+ -- ids] in read order. creations, then updates, then deletions
897
896
  local function process_update_group(self: Client, c: Cursor, custom_ids: CustomIdMap, variants: Array<any>?)
898
897
  local mask = cursor.readu16(c)
899
898
 
900
- local total_deleted = read_vlq_bitmask(c, mask, UPDATE_GROUP_BITS.deleted_entities)
901
- for _ = 1, total_deleted do
902
- local entity, server_id = find_entity(self, c)
903
- if entity then
904
- delete_entity(self, entity, server_id)
905
- end
906
- end
907
-
908
899
  local total_added = read_vlq_bitmask(c, mask, UPDATE_GROUP_BITS.added_entities)
909
900
  for _ = 1, total_added do
910
901
  process_entity(self, c, custom_ids)
@@ -916,6 +907,14 @@ local function process_update_group(self: Client, c: Cursor, custom_ids: CustomI
916
907
  process_entity_updates(self, entity, c, custom_ids, variants)
917
908
  end)
918
909
  end
910
+
911
+ local total_deleted = read_vlq_bitmask(c, mask, UPDATE_GROUP_BITS.deleted_entities)
912
+ for _ = 1, total_deleted do
913
+ local entity, server_id = find_entity(self, c)
914
+ if entity then
915
+ delete_entity(self, entity, server_id)
916
+ end
917
+ end
919
918
  end
920
919
 
921
920
  local function apply_command_buffer(
@@ -1533,8 +1532,8 @@ local function prune_component_serialization(self: Client, component: Component,
1533
1532
  end
1534
1533
  end
1535
1534
 
1536
- -- serdes take part in the handshake, preprocessors and delta serializers are
1537
- -- 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
1538
1537
  function client.set_serdes(self: Client, component: Component, serdes: types.Serdes)
1539
1538
  ensure_component_serialization(self, component).serdes = serdes
1540
1539
  self.is_shared_dirty = true
@@ -1552,6 +1551,7 @@ end
1552
1551
 
1553
1552
  function client.set_preprocessor(self: Client, component: Component, preprocessor: types.Preprocessor)
1554
1553
  ensure_component_serialization(self, component).preprocessor = preprocessor
1554
+ self.is_shared_dirty = true
1555
1555
  end
1556
1556
 
1557
1557
  function client.remove_preprocessor(self: Client, component: Component)
@@ -1561,6 +1561,7 @@ function client.remove_preprocessor(self: Client, component: Component)
1561
1561
  end
1562
1562
  options.preprocessor = nil
1563
1563
  prune_component_serialization(self, component, options)
1564
+ self.is_shared_dirty = true
1564
1565
  end
1565
1566
 
1566
1567
  function client.set_delta_serializer(self: Client, component: Component, delta_serializer: types.DeltaSerializer)
@@ -1576,21 +1577,26 @@ function client.remove_delta_serializer(self: Client, component: Component)
1576
1577
  prune_component_serialization(self, component, options)
1577
1578
  end
1578
1579
 
1579
- -- rebuilds the shared lookups (component ids, custom ids, serdes) when
1580
- -- 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
1581
1582
  function client.resolve_dirty(self: Client)
1582
1583
  if not self.is_shared_dirty then
1583
1584
  return
1584
1585
  end
1585
1586
 
1586
1587
  local serdes: { [Component]: types.Serdes } = {}
1588
+ local preprocessors: { [Component]: true } = {}
1587
1589
  for component, options in self.component_serialization do
1588
1590
  if options.serdes then
1589
1591
  serdes[component] = options.serdes
1590
1592
  end
1593
+ if options.preprocessor then
1594
+ preprocessors[component] = true
1595
+ end
1591
1596
  end
1592
1597
 
1593
- 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)
1594
1600
  self.is_shared_dirty = false
1595
1601
  end
1596
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