@rbxts/replecs 0.1.0-alpha8 → 0.1.0-alpha9-test5

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.
@@ -9,8 +9,43 @@ local cursor = utils.cursor
9
9
  type Cursor = utils.Cursor
10
10
 
11
11
  type Entity<T = any> = jecs.Entity<T>
12
+ type Id<T = any> = jecs.Id<T>
12
13
  type World = jecs.World
13
14
 
15
+ type Array<T> = { T }
16
+ type Map<K, V> = { [K]: V }
17
+ type Disconnect = () -> ()
18
+
19
+ type HookMethod =
20
+ & ((
21
+ self: Client,
22
+ entity: Entity,
23
+ action: "change",
24
+ relation: Entity,
25
+ callback: (entity: Entity, id: Id, value: any) -> ()
26
+ ) -> Disconnect)
27
+ & ((
28
+ self: Client,
29
+ entity: Entity,
30
+ action: "removed",
31
+ relation: Entity,
32
+ callback: (entity: Entity, id: Id) -> ()
33
+ ) -> Disconnect)
34
+ & (self: Client, entity: Entity, action: "deleted", callback: (entity: Entity) -> ()) -> Disconnect
35
+
36
+ type ChangedHooksEntry = {
37
+ overrides: boolean,
38
+ callbacks: Array<(entity: Entity, id: Id, value: any) -> ()>,
39
+ }
40
+ type RemovedHooksEntry = {
41
+ overrides: boolean,
42
+ callbacks: Array<(entity: Entity, id: Id) -> ()>,
43
+ }
44
+ type DeletedHookEntry = {
45
+ overrides: boolean,
46
+ callbacks: Array<(entity: Entity) -> ()>,
47
+ }
48
+
14
49
  export type Client = {
15
50
  world: World,
16
51
  inited: boolean?,
@@ -18,6 +53,8 @@ export type Client = {
18
53
  is_replicating: boolean,
19
54
  after_replication_callbacks: { () -> () },
20
55
 
56
+ is_dirty: boolean,
57
+ requires_shared_lookup: { Entity },
21
58
  components: common.Components,
22
59
  shared: common.Shared,
23
60
  global_handler: ((id: number) -> Entity)?,
@@ -28,11 +65,26 @@ export type Client = {
28
65
 
29
66
  init: (self: Client, world: World?) -> (),
30
67
  destroy: (self: Client) -> (),
31
- after_replication: (self: Client, callback: () -> ()) -> (),
32
68
  handle_global: (self: Client, handler: (id: number) -> Entity) -> (),
33
69
  get_server_entity: (self: Client, client_entity: Entity) -> number?,
34
70
  get_client_entity: (self: Client, server_entity: number) -> Entity?,
35
71
 
72
+ hooks: common.WorldHooks,
73
+ hooked: Array<() -> ()>,
74
+
75
+ after_replication: (self: Client, callback: () -> ()) -> (),
76
+ added: (self: Client, callback: (entity: Entity) -> ()) -> Disconnect,
77
+ hook: HookMethod,
78
+ override: HookMethod,
79
+ addition_hooks: Array<(entity: Entity) -> ()>,
80
+ entity_hooks: {
81
+ [Entity]: {
82
+ changed: Map<Entity, ChangedHooksEntry>,
83
+ removed: Map<Entity, RemovedHooksEntry>,
84
+ deleted: DeletedHookEntry?,
85
+ },
86
+ },
87
+
36
88
  encode_component: (self: Client, component: Entity) -> number,
37
89
  decode_component: (self: Client, component: number) -> Entity,
38
90
 
@@ -46,7 +98,6 @@ local ENTITY_ID_TYPES = {
46
98
  entity = 1,
47
99
  custom = 2,
48
100
  shared = 3,
49
- global = 4,
50
101
  }
51
102
  local PACKET_TYPES = {
52
103
  full = 1,
@@ -55,6 +106,7 @@ local PACKET_TYPES = {
55
106
  }
56
107
 
57
108
  local ECS_NAME = jecs.Name
109
+ local WILDCARD = jecs.Wildcard
58
110
 
59
111
  local function PAIR(first: Entity, second: Entity): Entity
60
112
  return jecs.pair(first, second)
@@ -66,12 +118,106 @@ local function get_or_create_entity(client: Client, server_id: number): Entity
66
118
  return server_entity
67
119
  else
68
120
  local entity = client.world:entity()
121
+ client.world:add(entity, client.components.__alive_tracking__)
122
+
69
123
  client.server_ids[server_id] = entity
70
124
  client.client_ids[entity] = server_id
125
+
126
+ for _, callback in client.addition_hooks do
127
+ callback(entity)
128
+ end
129
+
71
130
  return entity
72
131
  end
73
132
  end
74
133
 
134
+ local function network_entity_set(client: Client, entity: Entity, id: Id, value: any, changed_hooks: ChangedHooksEntry?)
135
+ if changed_hooks then
136
+ if not changed_hooks.overrides then
137
+ client.world:set(entity, id, value)
138
+ end
139
+ for _, callback in changed_hooks.callbacks do
140
+ callback(entity, id, value)
141
+ end
142
+ else
143
+ client.world:set(entity, id, value)
144
+ end
145
+ end
146
+
147
+ local function network_entity_add(client: Client, entity: Entity, component: Entity, changed_hooks: ChangedHooksEntry?)
148
+ if changed_hooks then
149
+ if not changed_hooks.overrides then
150
+ client.world:add(entity, component)
151
+ end
152
+ for _, callback in changed_hooks.callbacks do
153
+ callback(entity, component)
154
+ end
155
+ else
156
+ client.world:add(entity, component)
157
+ end
158
+ end
159
+
160
+ local function network_entity_remove(client: Client, entity: Entity, id: Entity, removed_hooks: RemovedHooksEntry?)
161
+ if removed_hooks then
162
+ for _, callback in removed_hooks.callbacks do
163
+ callback(entity, id)
164
+ end
165
+ if not removed_hooks.overrides then
166
+ client.world:remove(entity, id)
167
+ end
168
+ else
169
+ client.world:remove(entity, id)
170
+ end
171
+ end
172
+
173
+ local function network_entity_remove_relations(
174
+ client: Client,
175
+ entity: Entity,
176
+ relation: Entity,
177
+ removed_hooks: RemovedHooksEntry?
178
+ )
179
+ local world = client.world
180
+ local entity_index = jecs.entity_index_try_get_fast(world.entity_index :: any, entity :: any)
181
+ if not entity_index then
182
+ return
183
+ end
184
+ local archetype = entity_index.archetype
185
+
186
+ local wildcard = PAIR(relation, WILDCARD) :: any
187
+ local idr = world.component_index[wildcard]
188
+ if not idr then
189
+ return
190
+ end
191
+
192
+ local archetype_id = archetype.id
193
+ local count = idr.counts[archetype_id]
194
+ if not count then
195
+ return
196
+ end
197
+
198
+ local start = idr.records[archetype_id]
199
+ if not start then
200
+ return
201
+ end
202
+
203
+ if removed_hooks then
204
+ for i = start, start + count - 1 do
205
+ local pair = archetype.types[i]
206
+ for _, callback in removed_hooks.callbacks do
207
+ callback(entity, pair)
208
+ end
209
+ if not removed_hooks.overrides then
210
+ world:remove(entity, pair :: any)
211
+ end
212
+ end
213
+ else
214
+ for i = start, start + count - 1 do
215
+ local pair = archetype.types[i]
216
+ world:remove(entity, pair :: any)
217
+ end
218
+ end
219
+ end
220
+
75
221
  local function encode_component(client: Client, component: Entity): number
76
222
  local encoded = client.shared.ids[component]
77
223
  if not encoded then
@@ -89,7 +235,7 @@ local function decode_component(client: Client, encoded: number): Entity
89
235
  return component
90
236
  end
91
237
 
92
- local function read_component_id(client: Client, c: Cursor)
238
+ local function read_component_id(client: Client, c: Cursor): Entity
93
239
  local shared_id = cursor.readu8(c)
94
240
  local component = client.shared.components[shared_id]
95
241
  if not component then
@@ -99,8 +245,7 @@ local function read_component_id(client: Client, c: Cursor)
99
245
  return component
100
246
  end
101
247
 
102
- local function read_component_value(client: Client, c: Cursor, variants: { any }?): (Entity, any)
103
- local component = read_component_id(client, c)
248
+ local function read_component_value(client: Client, component: Entity, c: Cursor, variants: { any }?): any
104
249
  local serdes = client.shared.serdes[component]
105
250
 
106
251
  if serdes then
@@ -119,17 +264,23 @@ local function read_component_value(client: Client, c: Cursor, variants: { any }
119
264
  end
120
265
 
121
266
  local output = serdes.deserialize(appended, serdes_variants)
122
- return component, output
267
+ return output
123
268
  else
124
269
  local variant_id = cursor.read_vlq(c)
125
270
  if variant_id == 0 then
126
- return component, nil
271
+ return nil
127
272
  end
128
273
  local value = (variants :: { any })[variant_id]
129
- return component, value
274
+ return value
130
275
  end
131
276
  end
132
277
 
278
+ local function read_component(client: Client, c: Cursor, variants: { any }?): (Entity, any)
279
+ local component = read_component_id(client, c)
280
+ local value = read_component_value(client, component, c, variants)
281
+ return component, value
282
+ end
283
+
133
284
  local function resolve_global(client: Client, global_id: number): Entity
134
285
  if not client.global_handler then
135
286
  error "global id parser not set, consider using client:handle_global()"
@@ -139,14 +290,14 @@ local function resolve_global(client: Client, global_id: number): Entity
139
290
  end
140
291
 
141
292
  local function read_entity_id(client: Client, c: Cursor, variants: { any }?): (Entity?, number?)
142
- local id_type = cursor.readi8(c)
293
+ local id_type = cursor.readu8(c)
143
294
 
144
- if id_type <= 10 then
295
+ if id_type <= GLOBAL_ID_OFFSET then
145
296
  if id_type == ENTITY_ID_TYPES.entity then
146
297
  local server_id = cursor.readu40(c)
147
298
  return client.server_ids[server_id], server_id
148
299
  elseif id_type == ENTITY_ID_TYPES.custom then
149
- local component, value = read_component_value(client, c, variants)
300
+ local component, value = read_component(client, c, variants)
150
301
  local custom_getter = client.custom_ids[component]
151
302
  if not custom_getter then
152
303
  error(
@@ -159,9 +310,6 @@ local function read_entity_id(client: Client, c: Cursor, variants: { any }?): (E
159
310
  return custom_getter(value), nil
160
311
  elseif id_type == ENTITY_ID_TYPES.shared then
161
312
  return read_component_id(client, c), nil
162
- elseif id_type == ENTITY_ID_TYPES.global then
163
- local global_id = cursor.readu8(c) - GLOBAL_ID_OFFSET
164
- return resolve_global(client, global_id), nil
165
313
  end
166
314
  error(`malformed entity id {id_type} ` .. cursor.readu32(c))
167
315
  else
@@ -178,34 +326,86 @@ local function process_entity_id(client: Client, c: Cursor, variants: { any }?)
178
326
  end
179
327
  end
180
328
 
181
- local function process_entity_relation(client: Client, entity: jecs.Entity, c: Cursor, variants: { any }?)
329
+ local function process_entity_relations(
330
+ client: Client,
331
+ entity: jecs.Entity,
332
+ changed_hooks: Map<Entity, ChangedHooksEntry>?,
333
+ c: Cursor,
334
+ variants: { any }?
335
+ )
336
+ local components = client.components
182
337
  local relation = read_component_id(client, c)
183
338
  local total_targets = cursor.read_vlq(c)
339
+ local pair_hooks = changed_hooks and changed_hooks[PAIR(components.pair, relation)]
184
340
 
185
341
  for _ = 1, total_targets do
186
342
  local target = process_entity_id(client, c, variants)
187
- client.world:add(entity, PAIR(relation, target))
343
+ network_entity_add(client, entity, PAIR(relation, target), pair_hooks)
344
+ end
345
+ end
346
+
347
+ local function process_entity_relation_values(
348
+ client: Client,
349
+ entity: jecs.Entity,
350
+ changed_hooks: Map<Entity, ChangedHooksEntry>?,
351
+ c: Cursor,
352
+ variants: { any }?
353
+ )
354
+ local relation = read_component_id(client, c)
355
+ local total_targets = cursor.read_vlq(c)
356
+ local components = client.components
357
+ local pair_hooks = changed_hooks and changed_hooks[PAIR(components.pair, relation)]
358
+
359
+ for _ = 1, total_targets do
360
+ local value = read_component_value(client, relation, c, variants)
361
+ local target = process_entity_id(client, c, variants)
362
+ network_entity_set(client, entity, PAIR(relation, target), value, pair_hooks)
188
363
  end
189
364
  end
190
365
 
191
366
  local function process_entity(client: Client, c: Cursor, variants: { any }?)
192
367
  local entity = process_entity_id(client, c, variants)
368
+ local entity_hooks = client.entity_hooks[entity]
369
+ local changed_hooks = entity_hooks and entity_hooks.changed
370
+ local components = client.components
193
371
 
194
372
  local total_tags = cursor.read_vlq(c)
195
373
  for _ = 1, total_tags do
196
374
  local tag = read_component_id(client, c)
197
- client.world:add(entity, tag)
375
+ local tag_hooks = changed_hooks and changed_hooks[PAIR(components.reliable, tag)]
376
+ network_entity_add(client, entity, tag, tag_hooks)
198
377
  end
199
378
 
200
379
  local total_components = cursor.read_vlq(c)
201
380
  for _ = 1, total_components do
202
- local component, value = read_component_value(client, c, variants)
203
- client.world:set(entity, component, value)
381
+ local component, value = read_component(client, c, variants)
382
+ local component_hooks = changed_hooks and changed_hooks[PAIR(components.reliable, component)]
383
+ network_entity_set(client, entity, component, value, component_hooks)
204
384
  end
205
385
 
206
386
  local total_pairs = cursor.read_vlq(c)
207
387
  for _ = 1, total_pairs do
208
- process_entity_relation(client, entity, c, variants)
388
+ process_entity_relations(client, entity, changed_hooks, c, variants)
389
+ end
390
+
391
+ local total_pairs_values = cursor.read_vlq(c)
392
+ for _ = 1, total_pairs_values do
393
+ process_entity_relation_values(client, entity, changed_hooks, c, variants)
394
+ end
395
+ end
396
+
397
+ local function finish_replication(client: Client)
398
+ client.is_replicating = false
399
+ for _, callback in client.after_replication_callbacks do
400
+ callback()
401
+ end
402
+ table.clear(client.after_replication_callbacks)
403
+ end
404
+
405
+ local function resolve_dirty(client: Client)
406
+ if client.is_dirty then
407
+ client.shared = utils.create_shared_lookup(client.world, client.components)
408
+ client.is_dirty = false :: true -- wtf
209
409
  end
210
410
  end
211
411
 
@@ -236,7 +436,10 @@ end
236
436
  local function apply_updates(client: Client, buf: buffer, all_variants: { { any } }?)
237
437
  local c = cursor.from(buf)
238
438
  check_packet_type(c, "reliable")
439
+ resolve_dirty(client)
440
+ client.is_replicating = true
239
441
 
442
+ local components = client.components
240
443
  local total_packets = cursor.read_vlq(c)
241
444
  local variant_start = (all_variants and #all_variants + 1) :: number
242
445
 
@@ -254,21 +457,31 @@ local function apply_updates(client: Client, buf: buffer, all_variants: { { any
254
457
  local entity = process_entity_id(client, c, variants)
255
458
  local added_mask = cursor.readu8(c)
256
459
 
460
+ local entity_hooks = client.entity_hooks[entity]
461
+ local changed_hooks = entity_hooks and entity_hooks.changed
462
+
257
463
  local total_tags = read_vlq_bitmask(c, added_mask, 0)
258
464
  for _ = 1, total_tags do
259
465
  local tag = read_component_id(client, c)
260
- client.world:add(entity, tag)
466
+ local tag_hooks = changed_hooks and changed_hooks[PAIR(components.reliable, tag)]
467
+ network_entity_add(client, entity, tag, tag_hooks)
261
468
  end
262
469
 
263
470
  local total_components = read_vlq_bitmask(c, added_mask, 1)
264
471
  for _ = 1, total_components do
265
- local component, value = read_component_value(client, c, variants)
266
- client.world:set(entity, component, value)
472
+ local component, value = read_component(client, c, variants)
473
+ local component_hooks = changed_hooks and changed_hooks[PAIR(components.reliable, component)]
474
+ network_entity_set(client, entity, component, value, component_hooks)
267
475
  end
268
476
 
269
477
  local total_pairs = read_vlq_bitmask(c, added_mask, 2)
270
478
  for _ = 1, total_pairs do
271
- process_entity_relation(client, entity, c, variants)
479
+ process_entity_relations(client, entity, changed_hooks, c, variants)
480
+ end
481
+
482
+ local total_pairs_values = read_vlq_bitmask(c, added_mask, 3)
483
+ for _ = 1, total_pairs_values do
484
+ process_entity_relation_values(client, entity, changed_hooks, c, variants)
272
485
  end
273
486
  end
274
487
 
@@ -277,68 +490,99 @@ local function apply_updates(client: Client, buf: buffer, all_variants: { { any
277
490
  local entity = process_entity_id(client, c, variants)
278
491
  local changed_mask = cursor.readu8(c)
279
492
 
493
+ local entity_hooks = client.entity_hooks[entity]
494
+ local changed_hooks = entity_hooks and entity_hooks.changed
495
+ local removed_hooks = entity_hooks and entity_hooks.removed
496
+
280
497
  local total_tagged = read_vlq_bitmask(c, changed_mask, 0)
281
498
  for _ = 1, total_tagged do
282
499
  local tag = read_component_id(client, c)
283
- client.world:add(entity, tag)
500
+ local tag_hooks = changed_hooks and changed_hooks[PAIR(components.reliable, tag)]
501
+ network_entity_add(client, entity, tag, tag_hooks)
284
502
  end
285
503
 
286
504
  local total_components = read_vlq_bitmask(c, changed_mask, 1)
287
505
  for _ = 1, total_components do
288
- local component, value = read_component_value(client, c, variants)
289
- client.world:set(entity, component, value)
506
+ local component, value = read_component(client, c, variants)
507
+ local component_hooks = changed_hooks and changed_hooks[PAIR(components.reliable, component)]
508
+ network_entity_set(client, entity, component, value, component_hooks)
290
509
  end
291
510
 
292
- local total_pairs = read_vlq_bitmask(c, changed_mask, 2)
511
+ local total_removed = read_vlq_bitmask(c, changed_mask, 2)
512
+ for _ = 1, total_removed do
513
+ local component = read_component_id(client, c)
514
+ local component_hooks = removed_hooks and removed_hooks[PAIR(components.reliable, component)]
515
+ network_entity_remove(client, entity, component, component_hooks)
516
+ end
517
+
518
+ local total_pairs = read_vlq_bitmask(c, changed_mask, 3)
293
519
  for _ = 1, total_pairs do
294
520
  local relation = read_component_id(client, c)
521
+ local pair_changed_hooks = changed_hooks and changed_hooks[PAIR(components.pair, relation)]
522
+ local pair_removed_hooks = removed_hooks and removed_hooks[PAIR(components.pair, relation)]
295
523
  local total_targets = cursor.read_vlq(c)
296
524
 
297
525
  for _ = 1, total_targets do
298
- local id_type = cursor.readi8(c)
299
- -- here I use negative numbers to indicate removals
300
- -- but "process_entity_id" expects a positive value
301
- -- so I just peek at it and put it back but positive
302
- -- (surely there is a better way to do this)
303
- cursor.writei8(c, math.abs(id_type))
304
- local target = process_entity_id(client, c, variants)
526
+ local action_type = cursor.readi8(c)
527
+
528
+ if action_type == 1 then -- add pair
529
+ local target = process_entity_id(client, c, variants)
530
+ network_entity_add(client, entity, PAIR(relation, target), pair_changed_hooks)
531
+ elseif action_type == -1 then -- remove pair
532
+ local target = read_entity_id(client, c, variants)
533
+ if target then
534
+ network_entity_remove(client, entity, PAIR(relation, target), pair_removed_hooks)
535
+ end
536
+ end
537
+ end
538
+ end
305
539
 
306
- if id_type > 0 then
307
- client.world:add(entity, PAIR(relation, target))
308
- elseif id_type < 0 then
309
- client.world:remove(entity, PAIR(relation, target))
540
+ local total_pairs_values_removed = read_vlq_bitmask(c, changed_mask, 4)
541
+ for _ = 1, total_pairs_values_removed do
542
+ local relation = read_component_id(client, c)
543
+ local pair_hooks = removed_hooks and removed_hooks[PAIR(components.pair, relation)]
544
+ local total_targets = cursor.read_vlq(c)
545
+
546
+ for _ = 1, total_targets do
547
+ local target = read_entity_id(client, c, variants)
548
+ if target then
549
+ network_entity_remove(client, entity, PAIR(relation, target), pair_hooks)
310
550
  end
311
551
  end
312
552
  end
313
553
 
314
- local total_removed = read_vlq_bitmask(c, changed_mask, 3)
315
- for _ = 1, total_removed do
316
- local component = read_component_id(client, c)
317
- client.world:remove(entity, component)
554
+ local total_pairs_values = read_vlq_bitmask(c, changed_mask, 5)
555
+ for _ = 1, total_pairs_values do
556
+ local relation = read_component_id(client, c)
557
+ local pair_hooks = changed_hooks and changed_hooks[PAIR(components.pair, relation)]
558
+ local total_targets = cursor.read_vlq(c)
559
+
560
+ for _ = 1, total_targets do
561
+ local value = read_component_value(client, relation, c, variants)
562
+ local target = process_entity_id(client, c, variants)
563
+ network_entity_set(client, entity, PAIR(relation, target), value, pair_hooks)
564
+ end
318
565
  end
319
566
  end
320
567
 
321
- local total_component_deleted = read_vlq_bitmask(c, storage_mask, 3)
322
- for _ = 1, total_component_deleted do
568
+ local total_component_deletions = read_vlq_bitmask(c, storage_mask, 3)
569
+ for _ = 1, total_component_deletions do
323
570
  local entity = process_entity_id(client, c, variants)
324
- local deleted_mask = cursor.readu8(c)
325
-
326
- local total_tags = read_vlq_bitmask(c, deleted_mask, 0)
327
- for _ = 1, total_tags do
328
- local tag = read_component_id(client, c)
329
- client.world:remove(entity, tag)
330
- end
571
+ local entity_hooks = client.entity_hooks[entity]
572
+ local removed_hooks = entity_hooks and entity_hooks.removed
331
573
 
332
- local total_components = read_vlq_bitmask(c, deleted_mask, 1)
333
- for _ = 1, total_components do
574
+ local total_removed = cursor.read_vlq(c)
575
+ for _ = 1, total_removed do
334
576
  local component = read_component_id(client, c)
335
- client.world:remove(entity, component)
577
+ local component_hooks = removed_hooks and removed_hooks[PAIR(components.reliable, component)]
578
+ network_entity_remove(client, entity, component, component_hooks)
336
579
  end
337
580
 
338
- local total_pairs = read_vlq_bitmask(c, deleted_mask, 2)
581
+ local total_pairs = cursor.read_vlq(c)
339
582
  for _ = 1, total_pairs do
340
583
  local relation = read_component_id(client, c)
341
- utils.remove_all_relations(client.world, entity, relation)
584
+ local pair_hooks = removed_hooks and removed_hooks[PAIR(components.pair, relation)]
585
+ network_entity_remove_relations(client, entity, relation, pair_hooks)
342
586
  end
343
587
  end
344
588
 
@@ -346,19 +590,35 @@ local function apply_updates(client: Client, buf: buffer, all_variants: { { any
346
590
  for _ = 1, total_deleted do
347
591
  local entity, server_id = read_entity_id(client, c, variants)
348
592
  if entity then
349
- client.world:delete(entity)
593
+ local entity_hooks = client.entity_hooks[entity]
594
+ local deleted_hooks = entity_hooks and entity_hooks.deleted
595
+ if deleted_hooks then
596
+ for _, callback in deleted_hooks.callbacks do
597
+ callback(entity)
598
+ end
599
+ if not deleted_hooks.overrides then
600
+ client.world:delete(entity)
601
+ end
602
+ else
603
+ client.world:delete(entity)
604
+ end
350
605
  end
351
606
  if server_id then
352
607
  client.server_ids[server_id] = nil
353
608
  end
354
609
  end
355
610
  end
611
+
612
+ finish_replication(client)
356
613
  end
357
614
 
358
615
  local function apply_unreliable(client: Client, buf: buffer, all_variants: { { any } }?)
359
616
  local c = cursor.from(buf)
360
617
  check_packet_type(c, "unreliable")
618
+ resolve_dirty(client)
619
+ client.is_replicating = true
361
620
 
621
+ local components = client.components
362
622
  local total_packets = cursor.read_vlq(c)
363
623
  local variant_start = (all_variants and #all_variants + 1) :: number
364
624
 
@@ -368,25 +628,36 @@ local function apply_unreliable(client: Client, buf: buffer, all_variants: { { a
368
628
 
369
629
  for _ = 1, total_entities do
370
630
  local entity = read_entity_id(client, c, variants)
631
+
371
632
  local total_unreliable = cursor.read_vlq(c)
372
633
 
373
634
  if entity then
374
- local component_id, value = read_component_value(client, c, variants)
375
- client.world:set(entity, component_id, value)
635
+ local entity_hooks = client.entity_hooks[entity]
636
+ local changed_hooks = entity_hooks and entity_hooks.changed
637
+
638
+ for _ = 1, total_unreliable do
639
+ local component_id, value = read_component(client, c, variants)
640
+ local unreliable_hooks = changed_hooks and changed_hooks[PAIR(components.unreliable, component_id)]
641
+ network_entity_set(client, entity, component_id, value, unreliable_hooks)
642
+ end
376
643
  else
377
644
  -- dont apply unreliable updates to entities that dont exist in the client yet
378
645
  -- but we still need to process the values to keep the cursor in the right place
379
646
  for _ = 1, total_unreliable do
380
- read_component_value(client, c, variants)
647
+ read_component(client, c, variants)
381
648
  end
382
649
  end
383
650
  end
384
651
  end
652
+
653
+ finish_replication(client)
385
654
  end
386
655
 
387
656
  local function apply_full(client: Client, buf: buffer, all_variants: { { any } }?)
388
657
  local c = cursor.from(buf)
389
658
  check_packet_type(c, "full")
659
+ resolve_dirty(client)
660
+ client.is_replicating = true
390
661
 
391
662
  local total_packets = cursor.read_vlq(c)
392
663
  local variant_start = (all_variants and #all_variants + 1) :: number
@@ -399,6 +670,8 @@ local function apply_full(client: Client, buf: buffer, all_variants: { { any } }
399
670
  process_entity(client, c, variants)
400
671
  end
401
672
  end
673
+
674
+ finish_replication(client)
402
675
  end
403
676
 
404
677
  local function init(client: Client, _world: World?)
@@ -417,6 +690,13 @@ local function init(client: Client, _world: World?)
417
690
  error "Providing a world is required to start replecs"
418
691
  end
419
692
 
693
+ local hooks = {
694
+ added = (world :: any).added,
695
+ changed = (world :: any).changed,
696
+ removed = (world :: any).removed,
697
+ }
698
+ client.hooks = hooks
699
+
420
700
  local components = client.components
421
701
 
422
702
  client.shared = utils.create_shared_lookup(world, components)
@@ -424,6 +704,27 @@ local function init(client: Client, _world: World?)
424
704
  for component, custom_get in world:query(components.custom_handler):with(components.shared, ECS_NAME) do
425
705
  client.custom_ids[component] = custom_get
426
706
  end
707
+
708
+ local alive_unhook = hooks.removed(world, components.__alive_tracking__, function(entity)
709
+ local server_id = client.client_ids[entity]
710
+ if server_id then
711
+ client.server_ids[server_id] = nil
712
+ client.client_ids[entity] = nil
713
+ end
714
+ client.entity_hooks[entity] = nil
715
+ end)
716
+ table.insert(client.hooked, alive_unhook)
717
+
718
+ for _, component in client.requires_shared_lookup do
719
+ local added_hook = hooks.added(world, component, function()
720
+ client.is_dirty = true
721
+ end)
722
+ local removed_hook = hooks.removed(world, component, function()
723
+ client.is_dirty = true
724
+ end)
725
+ table.insert(client.hooked, added_hook)
726
+ table.insert(client.hooked, removed_hook)
727
+ end
427
728
  end
428
729
 
429
730
  local function after_replication(client: Client, callback: () -> ())
@@ -434,6 +735,97 @@ local function after_replication(client: Client, callback: () -> ())
434
735
  end
435
736
  end
436
737
 
738
+ local function added(client: Client, callback: (entity: Entity) -> ())
739
+ table.insert(client.addition_hooks, callback)
740
+ return function()
741
+ local index = table.find(client.addition_hooks, callback)
742
+ if index then
743
+ table.remove(client.addition_hooks, index)
744
+ end
745
+ end
746
+ end
747
+
748
+ local function get_or_create_entity_hooks(client: Client, entity: Entity)
749
+ local entity_hooks = client.entity_hooks[entity]
750
+ if not entity_hooks then
751
+ entity_hooks = {
752
+ changed = {},
753
+ removed = {},
754
+ deleted = nil :: DeletedHookEntry?,
755
+ }
756
+ client.entity_hooks[entity] = entity_hooks
757
+ end
758
+ return entity_hooks
759
+ end
760
+
761
+ local function add_hook_entry(client: Client, entity: Entity, action: string, ...): ({ overrides: boolean }, () -> ())
762
+ if action == "changed" or action == "removed" then
763
+ local relation = select(1, ...) :: Entity
764
+ local callback = select(2, ...) :: (entity: Entity, id: number, value: any) -> ()
765
+ local hooks = get_or_create_entity_hooks(client, entity)[action]
766
+ local entries = hooks[relation] :: ChangedHooksEntry
767
+
768
+ if not entries then
769
+ entries = {
770
+ overrides = false,
771
+ callbacks = {},
772
+ }
773
+ hooks[relation] = entries
774
+ end
775
+ table.insert(entries.callbacks, callback)
776
+
777
+ local function disconnect()
778
+ local callbacks = entries.callbacks
779
+ local index = table.find(callbacks, callback)
780
+ if index then
781
+ table.remove(callbacks, index)
782
+ if #callbacks == 0 then
783
+ hooks[relation] = nil
784
+ end
785
+ end
786
+ end
787
+ return entries, disconnect
788
+ elseif action == "deleted" then
789
+ local callback = select(1, ...) :: (entity: Entity) -> ()
790
+ local hooks = get_or_create_entity_hooks(client, entity)
791
+ local entries = hooks.deleted :: DeletedHookEntry
792
+
793
+ if not entries then
794
+ entries = {
795
+ overrides = false,
796
+ callbacks = {},
797
+ }
798
+ hooks.deleted = entries
799
+ end
800
+ table.insert(entries.callbacks, callback)
801
+
802
+ local function disconnect()
803
+ local callbacks = entries.callbacks
804
+ local index = table.find(callbacks, callback)
805
+ if index then
806
+ table.remove(callbacks, index)
807
+ if #callbacks == 0 then
808
+ hooks.deleted = nil
809
+ end
810
+ end
811
+ end
812
+ return entries, disconnect
813
+ else
814
+ error("invalid hook action: " .. action)
815
+ end
816
+ end
817
+
818
+ local function hook(client: Client, entity: Entity, action: string, ...)
819
+ local entry, disconnect = add_hook_entry(client, entity, action, ...)
820
+ entry.overrides = false
821
+ return disconnect
822
+ end
823
+ local function override(client: Client, entity: Entity, action: string, ...)
824
+ local entry, disconnect = add_hook_entry(client, entity, action, ...)
825
+ entry.overrides = true
826
+ return disconnect
827
+ end
828
+
437
829
  local function get_server_entity(client: Client, client_entity: Entity): number?
438
830
  return client.client_ids[client_entity]
439
831
  end
@@ -447,8 +839,9 @@ local function destroy(client: Client)
447
839
  return warn "attempted to destroy a client twice"
448
840
  end
449
841
  client.inited = nil :: any
450
- -- I'll add any logic here if I need to clean up something
451
- -- right now this is kind of useless
842
+ for _, unhook in client.hooked do
843
+ unhook()
844
+ end
452
845
  end
453
846
 
454
847
  local function handle_global(client: Client, handler: (id: number) -> Entity)
@@ -459,7 +852,6 @@ local client = {}
459
852
  client.__index = client
460
853
  client.init = init
461
854
  client.destroy = destroy
462
- client.after_replication = after_replication
463
855
  client.apply_updates = apply_updates
464
856
  client.apply_unreliable = apply_unreliable
465
857
  client.apply_full = apply_full
@@ -468,6 +860,10 @@ client.decode_component = decode_component
468
860
  client.get_server_entity = get_server_entity
469
861
  client.get_client_entity = get_client_entity
470
862
  client.handle_global = handle_global
863
+ client.after_replication = after_replication
864
+ client.hook = hook
865
+ client.override = override
866
+ client.added = added
471
867
 
472
868
  local function create(world: World?, components: common.Components): Client
473
869
  local self = {} :: Client
@@ -477,10 +873,21 @@ local function create(world: World?, components: common.Components): Client
477
873
  self.custom_ids = {}
478
874
  self.server_ids = {}
479
875
  self.client_ids = {}
876
+ self.is_dirty = false
877
+ self.requires_shared_lookup = {
878
+ self.components.shared,
879
+ self.components.serdes,
880
+ self.components.bytespan,
881
+ jecs.Name,
882
+ }
480
883
  self.global_handler = nil
481
884
  self.world = world :: any
482
885
  self.inited = false
483
886
  self.is_replicating = false
887
+ self.hooked = {}
888
+
889
+ self.addition_hooks = {}
890
+ self.entity_hooks = {}
484
891
  self.after_replication_callbacks = {}
485
892
 
486
893
  return setmetatable(self, client) :: any