@rbxts/replecs 0.1.0-alpha8 → 0.1.0-alpha9

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