@rbxts/replecs 0.3.1 → 0.4.0-rc.2

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.
@@ -109,7 +109,7 @@ local function WORLD_GET<T>(world: World, entity: Entity, id: Component<T>): T
109
109
  return world:get(entity, id) :: T
110
110
  end
111
111
 
112
- local function WORLD_TARGET(world: World, entity: Entity, rel: Component, index: number?): jecs.Entity?
112
+ local function WORLD_TARGET(world: World, entity: Entity, rel: Component, index: number?): Entity?
113
113
  return world:target(entity, rel, index)
114
114
  end
115
115
 
@@ -131,8 +131,22 @@ end
131
131
  local PREREGISTER_COMPONENT = jecs.component
132
132
  local PREREGISTER_TAG = jecs.tag
133
133
 
134
- local function IS_COMPONENT(world: World, id: Component)
135
- return not jecs.is_tag(world, id)
134
+ local ECS_COMPONENT = jecs.Component
135
+
136
+ -- jecs.is_tag misreports pairs that have not been used in the world yet, so
137
+ -- component-ness is resolved by hand: a plain id is a component when it has
138
+ -- jecs.Component; a pair takes its data from the relation, or from the
139
+ -- target when the relation is a tag
140
+ local function IS_COMPONENT(world: World, id: Component): boolean
141
+ if IS_PAIR(id) then
142
+ local relation = jecs.pair_first(world, id)
143
+ if world:has(relation, ECS_COMPONENT) then
144
+ return true
145
+ end
146
+ local target = jecs.pair_second(world, id)
147
+ return world:has(target, ECS_COMPONENT)
148
+ end
149
+ return world:has(id, ECS_COMPONENT)
136
150
  end
137
151
 
138
152
  local function HOOK_ADDED(world: World, id: Component, callback: (e: Entity, id: Component, value: any) -> ()): () -> ()
@@ -0,0 +1,268 @@
1
+ export type Cursor = {
2
+ offset: number,
3
+ buffer: buffer,
4
+ }
5
+
6
+ local cursor = {}
7
+
8
+ function cursor.new(): Cursor
9
+ return {
10
+ buffer = buffer.create(100),
11
+ offset = 0,
12
+ }
13
+ end
14
+
15
+ function cursor.from(buf: buffer): Cursor
16
+ return {
17
+ buffer = buf,
18
+ offset = buffer.len(buf),
19
+ }
20
+ end
21
+
22
+ function cursor.from_offset(buf: buffer, offset: number): Cursor
23
+ return {
24
+ buffer = buf,
25
+ offset = offset,
26
+ }
27
+ end
28
+
29
+ function cursor.tryalloc(c: Cursor, bytes: number)
30
+ local buf = c.buffer
31
+ local offset = c.offset
32
+ local len = buffer.len(buf)
33
+
34
+ if len < offset + bytes then
35
+ local exponent = math.ceil(math.log((bytes + offset) / len, 1.5))
36
+ local new_len = math.floor(len * 1.5 ^ exponent)
37
+ if new_len < offset + bytes then
38
+ new_len = offset + bytes
39
+ end
40
+ local new = buffer.create(new_len)
41
+ buffer.copy(new, 0, buf, 0)
42
+ c.buffer = new
43
+ end
44
+ end
45
+
46
+ function cursor.write_buffer(c: Cursor, buf: buffer, start: number?, count: number?)
47
+ if start == nil and count == nil then
48
+ cursor.tryalloc(c, buffer.len(buf))
49
+ buffer.copy(c.buffer, c.offset, buf, 0, buffer.len(buf))
50
+ c.offset += buffer.len(buf)
51
+ else
52
+ local len = buffer.len(buf)
53
+ local available = len - (start or 0)
54
+ local write_count = count and math.min(available, count) or available
55
+
56
+ cursor.tryalloc(c, write_count)
57
+ buffer.copy(c.buffer, c.offset, buf, start or 0, write_count)
58
+ c.offset += write_count
59
+ end
60
+ end
61
+
62
+ function cursor.print(c: Cursor)
63
+ local bytes: { any } = { string.byte(buffer.tostring(c.buffer), 1, -1) }
64
+ local offset = 7
65
+ for i = 1, c.offset do
66
+ local byte = bytes[i] :: number
67
+ local digits = if byte == 0 then 1 else math.ceil(math.log10(1 + byte))
68
+ offset += digits + 1
69
+ end
70
+
71
+ local last_byte = bytes[c.offset + 1]
72
+ if last_byte then
73
+ local lastDigits = if last_byte == 0 then 1 else math.ceil(math.log10(1 + last_byte))
74
+ offset += lastDigits // 2
75
+ end
76
+ bytes[c.offset + 1] = `[{bytes[c.offset + 1] :: number}]` :: any
77
+
78
+ if #bytes == 0 or c.offset == buffer.len(c.buffer) then
79
+ table.insert(bytes, " ")
80
+ end
81
+
82
+ return `Pos: {c.offset} / {buffer.len(c.buffer)}\nBuf: \{ {table.concat(bytes, " ")} \}`
83
+ end
84
+
85
+ function cursor.writeu8(c: Cursor, value: number)
86
+ cursor.tryalloc(c, 1)
87
+ buffer.writeu8(c.buffer, c.offset, value)
88
+ c.offset += 1
89
+ end
90
+
91
+ function cursor.writeu16(c: Cursor, value: number)
92
+ cursor.tryalloc(c, 2)
93
+ buffer.writeu16(c.buffer, c.offset, value)
94
+ c.offset += 2
95
+ end
96
+ function cursor.writeu24(c: Cursor, value: number)
97
+ cursor.tryalloc(c, 3)
98
+ buffer.writeu8(c.buffer, c.offset, value)
99
+ buffer.writeu16(c.buffer, c.offset + 1, value // 256)
100
+ c.offset += 3
101
+ end
102
+
103
+ function cursor.writeu32(c: Cursor, value: number)
104
+ cursor.tryalloc(c, 4)
105
+ buffer.writeu32(c.buffer, c.offset, value :: any)
106
+ c.offset += 4
107
+ end
108
+
109
+ function cursor.writeu40(c: Cursor, value: number)
110
+ cursor.tryalloc(c, 5)
111
+ buffer.writeu8(c.buffer, c.offset, value)
112
+ buffer.writeu32(c.buffer, c.offset + 1, value // 256)
113
+ c.offset += 5
114
+ end
115
+
116
+ function cursor.writei8(c: Cursor, value: number)
117
+ cursor.tryalloc(c, 1)
118
+ buffer.writei8(c.buffer, c.offset, value)
119
+ c.offset += 1
120
+ end
121
+
122
+ function cursor.writei16(c: Cursor, value: number)
123
+ cursor.tryalloc(c, 2)
124
+ buffer.writei16(c.buffer, c.offset, value)
125
+ c.offset += 2
126
+ end
127
+
128
+ function cursor.writei32(c: Cursor, value: number)
129
+ cursor.tryalloc(c, 4)
130
+ buffer.writeu32(c.buffer, c.offset, value :: any)
131
+ c.offset += 4
132
+ end
133
+
134
+ function cursor.write_span(c: Cursor, value: number, span: number)
135
+ if span <= 0xFF then
136
+ cursor.writeu8(c, value)
137
+ elseif span <= 0xFFFF then
138
+ cursor.writeu16(c, value)
139
+ elseif span <= 0xFFFFFF then
140
+ cursor.writeu24(c, value)
141
+ elseif span <= 0xFFFFFFFF then
142
+ cursor.writeu32(c, value)
143
+ else
144
+ error "span too large"
145
+ end
146
+ end
147
+
148
+ function cursor.read_buffer(c: Cursor, len: number)
149
+ local retrieved = buffer.create(len)
150
+ buffer.copy(retrieved, 0, c.buffer, c.offset - len, len)
151
+ c.offset -= len
152
+ return retrieved
153
+ end
154
+
155
+ function cursor.readu8(c: Cursor): number
156
+ c.offset -= 1
157
+ return buffer.readu8(c.buffer, c.offset)
158
+ end
159
+
160
+ function cursor.readu16(c: Cursor): number
161
+ c.offset -= 2
162
+ return buffer.readu16(c.buffer, c.offset)
163
+ end
164
+
165
+ function cursor.readu24(c: Cursor): number
166
+ c.offset -= 3
167
+ local one = buffer.readu8(c.buffer, c.offset)
168
+ local two = buffer.readu16(c.buffer, c.offset + 1)
169
+ return one + two * 256
170
+ end
171
+
172
+ function cursor.readu32(c: Cursor): number
173
+ c.offset -= 4
174
+ return buffer.readu32(c.buffer, c.offset)
175
+ end
176
+
177
+ function cursor.readu40(c: Cursor): number
178
+ c.offset -= 5
179
+ local one = buffer.readu8(c.buffer, c.offset)
180
+ local two = buffer.readu32(c.buffer, c.offset + 1)
181
+ return one + two * 256
182
+ end
183
+
184
+ function cursor.read_span(c: Cursor, span: number): number
185
+ if span <= 0xFF then
186
+ return cursor.readu8(c)
187
+ elseif span <= 0xFFFF then
188
+ return cursor.readu16(c)
189
+ elseif span <= 0xFFFFFF then
190
+ return cursor.readu24(c)
191
+ elseif span <= 0xFFFFFFFF then
192
+ return cursor.readu32(c)
193
+ else
194
+ error "span too large"
195
+ end
196
+ end
197
+
198
+ function cursor.readi8(c: Cursor): number
199
+ c.offset -= 1
200
+ return buffer.readi8(c.buffer, c.offset)
201
+ end
202
+
203
+ function cursor.readi16(c: Cursor): number
204
+ c.offset -= 2
205
+ return buffer.readi16(c.buffer, c.offset)
206
+ end
207
+ function cursor.readi32(c: Cursor): number
208
+ c.offset -= 4
209
+ return buffer.readi32(c.buffer, c.offset)
210
+ end
211
+
212
+ function cursor.write_vlq(c: Cursor, value: number)
213
+ local x0 = value // 1% 128
214
+ local x1 = value // 128% 128
215
+ local x2 = value // 16384% 128
216
+ local x3 = value // 2097152% 128
217
+
218
+ if x3 ~= 0 then
219
+ cursor.tryalloc(c, 4)
220
+ cursor.writeu32(c, x0 * 16777216+ x1 * 65536+ x2 * 256 + x3 + 128)
221
+ elseif x2 ~= 0 then
222
+ cursor.tryalloc(c, 3)
223
+ cursor.writeu24(c, x0 * 65536+ x1 * 256 + x2 + 128)
224
+ elseif x1 ~= 0 then
225
+ cursor.tryalloc(c, 2)
226
+ cursor.writeu16(c, x0 * 256 + x1 + 128)
227
+ else
228
+ cursor.tryalloc(c, 1)
229
+ cursor.writeu8(c, x0 + 128)
230
+ end
231
+ end
232
+
233
+ function cursor.read_vlq(c: Cursor): number
234
+ local b = cursor.readu8(c)
235
+ if b >= 128 then
236
+ return b - 128
237
+ end
238
+ local x = b
239
+
240
+ b = cursor.readu8(c)
241
+ if b >= 128 then
242
+ return x + (b - 128) * 128
243
+ end
244
+ x += b * 128
245
+
246
+ b = cursor.readu8(c)
247
+ if b >= 128 then
248
+ return x + (b - 128) * 16384
249
+ end
250
+ x += b * 16384
251
+
252
+ b = cursor.readu8(c)
253
+ if b >= 128 then
254
+ return x + (b - 128) * 2097152
255
+ end
256
+ x += b * 2097152
257
+
258
+ error "vlq length too large"
259
+ end
260
+
261
+ function cursor.close(c: Cursor): buffer
262
+ local final = buffer.create(c.offset)
263
+ buffer.copy(final, 0, c.buffer, 0, c.offset)
264
+ c.buffer = final
265
+ return final
266
+ end
267
+
268
+ return cursor
@@ -0,0 +1,108 @@
1
+ local types = require (script.Parent:WaitForChild('types'))
2
+ local common = require (script.Parent:WaitForChild('common'))
3
+
4
+ type Component = common.Component
5
+ type Entity = common.Entity
6
+ type World = common.World
7
+
8
+ local handshake = {}
9
+
10
+ function handshake.generate_handshake(shared: types.Shared): types.HandshakeInfo
11
+ local component_to_name: { [Entity]: string } = {}
12
+ local handshake: types.HandshakeInfo = {
13
+ components = {},
14
+ custom_ids = {},
15
+ serdes = {},
16
+ }
17
+
18
+ for component_name, component in shared.components.lookup do
19
+ handshake.components[component_name] = true
20
+ component_to_name[component] = component_name
21
+ end
22
+
23
+ for custom_id in shared.custom_ids.lookup do
24
+ handshake.custom_ids[custom_id] = true
25
+ end
26
+
27
+ for component, serdes in shared.serdes do
28
+ local name = component_to_name[component]
29
+ if not name then
30
+ continue
31
+ end
32
+
33
+ handshake.serdes[name] = {
34
+ includes_variants = serdes.includes_variants,
35
+ bytespan = serdes.bytespan,
36
+ }
37
+ end
38
+
39
+ return handshake
40
+ end
41
+
42
+ function handshake.verify_handshake(
43
+ shared: types.Shared,
44
+ handshake: types.HandshakeInfo,
45
+ handshake_side: string,
46
+ verify_side: string
47
+ ): (boolean, string?)
48
+ local component_to_name: { [Entity]: string } = {}
49
+
50
+ for component_name in handshake.components do
51
+ if not shared.components.lookup[component_name] then
52
+ return false, `missing shared component "{component_name}" in {verify_side}`
53
+ end
54
+ end
55
+ for component_name, component in shared.components.lookup do
56
+ if not handshake.components[component_name] then
57
+ return false, `missing shared component "{component_name}" in {handshake_side}`
58
+ end
59
+ component_to_name[component] = component_name
60
+ end
61
+
62
+ for custom_id in handshake.custom_ids do
63
+ if not shared.custom_ids.lookup[custom_id] then
64
+ return false, `missing custom id "{custom_id}" in {verify_side}`
65
+ end
66
+ end
67
+ for custom_id in shared.custom_ids.lookup do
68
+ if not handshake.custom_ids[custom_id] then
69
+ return false, `missing custom id "{custom_id}" in {handshake_side}`
70
+ end
71
+ end
72
+
73
+ for component_name, serdes_info in handshake.serdes do
74
+ local component = shared.components.lookup[component_name]
75
+ if not component then
76
+ common.log_warn(`{handshake_side} registered a serdes for "{component_name}" that {verify_side} doesn't have`)
77
+ continue
78
+ end
79
+
80
+ local serdes = shared.serdes[component]
81
+ if not serdes then
82
+ return false, `missing serdes for component "{component_name}" in {verify_side}`
83
+ end
84
+
85
+ if (serdes_info.includes_variants or false) ~= (serdes.includes_variants or false) then
86
+ return false,
87
+ `mismatched includes_variants for component "{component_name}": ({serdes_info.includes_variants} ~= {serdes.includes_variants})`
88
+ end
89
+ if serdes_info.bytespan ~= serdes.bytespan then
90
+ return false,
91
+ `mismatched bytespan for component "{component_name}": ({serdes_info.bytespan} ~= {serdes.bytespan})`
92
+ end
93
+ end
94
+ for component in shared.serdes do
95
+ local component_name = component_to_name[component]
96
+ if not component_name then
97
+ continue
98
+ end
99
+
100
+ if not handshake.serdes[component_name] then
101
+ common.log_warn(`{verify_side} registered a serdes for "{component_name}" that {handshake_side} doesn't have`)
102
+ end
103
+ end
104
+
105
+ return true, nil
106
+ end
107
+
108
+ return handshake
@@ -30,11 +30,17 @@ export type ReplecsLib = {
30
30
 
31
31
  set_serdes: <T>(self: ReplecsLib, id: Component<T>, serdes: types.Serdes<T>) -> (),
32
32
  remove_serdes: (self: ReplecsLib, id: Component) -> (),
33
+ set_preprocessor: <B, A>(self: ReplecsLib, id: Component<B>, preprocessor: types.Preprocessor<B, A>) -> (),
34
+ remove_preprocessor: (self: ReplecsLib, id: Component) -> (),
35
+ set_delta_serializer: <T, D>(self: ReplecsLib, id: Component<T>, delta: types.DeltaSerializer<T, D>) -> (),
36
+ remove_delta_serializer: (self: ReplecsLib, id: Component) -> (),
33
37
  }
34
38
 
35
39
  type Replecs = types.Components & {
36
40
  __index: any,
37
41
  VERSION: string,
42
+ MAX_CLIENTS: number,
43
+
38
44
  create: (world: World?) -> ReplecsLib,
39
45
 
40
46
  create_server: (world: World?) -> Server,
@@ -44,6 +50,7 @@ type Replecs = types.Components & {
44
50
 
45
51
  local replecs = {}
46
52
  replecs.VERSION = VERSION
53
+ replecs.MAX_CLIENTS = 128
47
54
  replecs.__index = replecs
48
55
 
49
56
  local PREREGISTRATION = common.preregistration
@@ -54,13 +61,12 @@ if PREREGISTRATION then
54
61
  utils.add_component_names(components, common.add_preregistered_name)
55
62
 
56
63
  jecs.meta(components.custom, jecs.Exclusive)
57
- jecs.meta(components.global, jecs.Exclusive)
58
64
  end
59
65
 
60
- local COMPONENTS = if PREREGISTRATION then replecs :: Replecs else nil
66
+ local COMPONENTS: Replecs = if PREREGISTRATION then replecs else nil :: any
61
67
 
62
68
  function replecs.create_server(world: World?): Server
63
- return server.create(world, COMPONENTS)
69
+ return server.create(world, COMPONENTS, replecs.MAX_CLIENTS)
64
70
  end
65
71
 
66
72
  function replecs.create_client(world: World?): Client
@@ -68,25 +74,27 @@ function replecs.create_client(world: World?): Client
68
74
  end
69
75
 
70
76
  function replecs.create(world: World?): ReplecsLib
71
- assert(game, "This is a Roblox specific function")
77
+ assert(
78
+ game,
79
+ "replecs.create() is only for roblox, call create_client or create_server manually depending on your setup"
80
+ )
81
+
72
82
  local RunService = game:GetService "RunService"
73
83
  local self = {} :: ReplecsLib
74
84
 
75
- if COMPONENTS then
76
- self.components = COMPONENTS
77
- elseif world then
78
- self.components = utils.create_components(utils.tag_factory(world), utils.component_factory(world))
79
- utils.add_component_names(self.components, function(component, name)
80
- common.add_name(world, component, name)
81
- end)
82
- end
85
+ -- without preregistration the components come from the world: shared by
86
+ -- both sides when it is known here, else created by whichever inits first
87
+ local components = COMPONENTS or (if world then utils.create_world_components(world) else nil) :: any
83
88
 
84
89
  if RunService:IsServer() then
85
- self.server = server.create(world, self.components)
90
+ self.server = server.create(world, components, replecs.MAX_CLIENTS)
91
+ components = self.server.components
86
92
  end
87
93
  if RunService:IsClient() then
88
- self.client = client.create(world, self.components)
94
+ self.client = client.create(world, components)
95
+ components = self.client.components
89
96
  end
97
+ self.components = components
90
98
 
91
99
  return setmetatable(self, replecs) :: any
92
100
  end
@@ -131,4 +139,40 @@ function replecs.remove_serdes(lib: ReplecsLib, id: Component)
131
139
  end
132
140
  end
133
141
 
134
- return replecs :: Replecs
142
+ function replecs.set_preprocessor(lib: ReplecsLib, id: Component, preprocessor: types.Preprocessor)
143
+ if lib.server then
144
+ lib.server:set_preprocessor(id, preprocessor)
145
+ end
146
+ if lib.client then
147
+ lib.client:set_preprocessor(id, preprocessor)
148
+ end
149
+ end
150
+
151
+ function replecs.remove_preprocessor(lib: ReplecsLib, id: Component)
152
+ if lib.server then
153
+ lib.server:remove_preprocessor(id)
154
+ end
155
+ if lib.client then
156
+ lib.client:remove_preprocessor(id)
157
+ end
158
+ end
159
+
160
+ function replecs.set_delta_serializer(lib: ReplecsLib, id: Component, delta_serializer: types.DeltaSerializer)
161
+ if lib.server then
162
+ lib.server:set_delta_serializer(id, delta_serializer)
163
+ end
164
+ if lib.client then
165
+ lib.client:set_delta_serializer(id, delta_serializer)
166
+ end
167
+ end
168
+
169
+ function replecs.remove_delta_serializer(lib: ReplecsLib, id: Component)
170
+ if lib.server then
171
+ lib.server:remove_delta_serializer(id)
172
+ end
173
+ if lib.client then
174
+ lib.client:remove_delta_serializer(id)
175
+ end
176
+ end
177
+
178
+ return (replecs :: any) :: Replecs