@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.
- package/out/replecs/client/init.luau +12 -5
- package/out/replecs/handshake.luau +35 -0
- package/out/replecs/index.d.ts +43 -0
- package/out/replecs/init.luau +8 -0
- package/out/replecs/server/filter_group.luau +297 -123
- package/out/replecs/server/init.luau +169 -132
- package/out/replecs/server/registry.luau +30 -1
- package/out/replecs/shared.luau +3 -1
- package/out/replecs/types.luau +3 -0
- package/out/replecs/ver.luau +1 -1
- package/package.json +3 -2
|
@@ -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
|
|
1536
|
-
-- local to each side and never
|
|
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)
|
|
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 =
|
|
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
|
|
package/out/replecs/index.d.ts
CHANGED
|
@@ -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
|
|
package/out/replecs/init.luau
CHANGED
|
@@ -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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
25
|
+
export type FilterNodeClass = {
|
|
26
|
+
kind: number,
|
|
27
|
+
registry: Registry?,
|
|
23
28
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
value: Bitmask?,
|
|
35
|
+
scratch: Bitmask?,
|
|
36
|
+
version: number,
|
|
37
|
+
dirty: boolean,
|
|
38
|
+
resolved_epoch: number,
|
|
39
|
+
resolved_cycle: number,
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
-- source only
|
|
42
|
+
member_set: Set<Player>?,
|
|
43
|
+
inverted: boolean?,
|
|
44
|
+
predicator: FilterPredicator?,
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
local
|
|
45
|
-
|
|
47
|
+
local filter_node = {}
|
|
48
|
+
filter_node.__index = filter_node
|
|
46
49
|
|
|
47
|
-
export type
|
|
50
|
+
export type FilterNode = FilterNodeClass & typeof(filter_node)
|
|
51
|
+
export type FilterOperand = FilterNode | FilterSimple
|
|
48
52
|
|
|
49
|
-
function
|
|
50
|
-
return typeof(value) == "table" and getmetatable(value) ==
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
60
|
-
local
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
67
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
|
82
|
-
if
|
|
83
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
local
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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
|
-
|
|
133
|
-
|
|
214
|
+
function filter_node.set(self: FilterNode, filter: FilterSimple)
|
|
215
|
+
assert_source(self, "set")
|
|
134
216
|
|
|
135
|
-
local
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
228
|
+
members[filter] = true
|
|
140
229
|
end
|
|
141
230
|
|
|
142
|
-
|
|
231
|
+
self.member_set = members
|
|
232
|
+
self.inverted = inverted
|
|
233
|
+
mark_dirty(self)
|
|
143
234
|
end
|
|
144
235
|
|
|
145
|
-
function
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|
172
|
-
local
|
|
173
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
185
|
-
|
|
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
|
-
|
|
193
|
-
|
|
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
|
|
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
|
|
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 |
|
|
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
|
|
80
|
-
--
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
139
|
-
type
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
453
|
-
storage.
|
|
454
|
-
|
|
466
|
+
local bindings = self.filter_bindings[node]
|
|
467
|
+
local index = storage.binding_index :: number
|
|
468
|
+
local entries = bindings.entries
|
|
455
469
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
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:
|
|
546
|
-
|
|
547
|
-
)
|
|
548
|
-
|
|
549
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
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
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
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)
|
|
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 =
|
|
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.
|
|
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
|
|
package/out/replecs/shared.luau
CHANGED
|
@@ -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
|
|
package/out/replecs/types.luau
CHANGED
|
@@ -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
|
package/out/replecs/ver.luau
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
return "v0.4.0-rc.
|
|
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
|
+
"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",
|