@dcl/ecs 7.7.3-13090257149.commit-df175f2 → 7.7.3-13092675740.commit-ba2dd71

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/README.md CHANGED
@@ -1,10 +1,118 @@
1
- # ECS 7
1
+ # @dcl/ecs
2
2
 
3
- ## Installing dependencies
4
- Run `make install`, this will run the `npm install` and other dependencies
3
+ Core Entity Component System (ECS) package for Decentraland scenes. Implements a CRDT-based ECS architecture for networked scene state.
5
4
 
6
- ## Building
7
- Run `make build`
5
+ ## Installation
8
6
 
9
- ## Testing
10
- Run `make test`, you can also debug the test in VS code, selecting the launch `Jest current file` or just `Jest` (this will run all test)
7
+ ```bash
8
+ npm install @dcl/ecs
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { engine } from '@dcl/ecs'
15
+
16
+ // Create entity
17
+ const entity = engine.addEntity()
18
+
19
+ // Define and add component
20
+ const Health = engine.defineComponent(1, {
21
+ current: Number,
22
+ max: Number,
23
+ regeneration: Number
24
+ })
25
+
26
+ Health.create(entity, {
27
+ current: 100,
28
+ max: 100,
29
+ regeneration: 1
30
+ })
31
+
32
+ // Create system
33
+ engine.addSystem((dt: number) => {
34
+ for (const [entity, health] of engine.mutableGroupOf(Health)) {
35
+ if (health.current < health.max) {
36
+ health.current = Math.min(health.max, health.current + health.regeneration * dt)
37
+ }
38
+ }
39
+ })
40
+ ```
41
+
42
+ ## Technical Overview
43
+
44
+ ### Component Definition
45
+
46
+ Components are defined with a unique ID and a schema. The schema is used to:
47
+
48
+ - Generate TypeScript types
49
+ - Create binary serializers/deserializers
50
+ - Set up CRDT operations
51
+
52
+ ### CRDT Implementation
53
+
54
+ The ECS uses CRDTs (Conflict-free Replicated Data Types) to enable deterministic state updates across multiple engine instances:
55
+
56
+ - Component updates are CRDT operations with logical timestamps
57
+ - Multiple engine instances can be synced by exchanging CRDT operations
58
+ - Conflict resolution uses timestamps and entity IDs to ensure consistency
59
+ - Binary transport format minimizes network overhead
60
+
61
+ ### Network Entities
62
+
63
+ For multiplayer scenes, the `syncEntity` method marks entities that should be synchronized across peers.
64
+ In the background it creates a NetworkEntity and a SyncComponents components with all the info necessary to synchronise the entity through the network.
65
+
66
+ ```typescript
67
+ import { engine, NetworkEntity } from '@dcl/ecs'
68
+
69
+ // Create a networked entity
70
+ const foe = engine.addEntity()
71
+ NetworkEntity.create(foe)
72
+
73
+ // Components on this entity will be synced across peers
74
+ Health.create(foe, { current: 100, max: 100, regeneration: 1 })
75
+ ```
76
+
77
+ Each peer maintains its own engine instance. When using NetworkEntity:
78
+
79
+ - The owner peer can modify the entity's components
80
+ - Other peers receive read-only replicas
81
+ - Updates are propagated through the network transport layer using CRDT operations
82
+
83
+ Example transport message:
84
+
85
+ ```typescript
86
+ {
87
+ entityId: number
88
+ componentId: number
89
+ timestamp: number
90
+ data: Uint8Array // Serialized component data
91
+ }
92
+ ```
93
+
94
+ ### Performance Features
95
+
96
+ - Zero-allocation component iteration
97
+ - Dirty state tracking for efficient updates
98
+ - Binary serialization for network transport
99
+ - Batched component operations
100
+
101
+ ## Development
102
+
103
+ ```bash
104
+ # Build
105
+ make build
106
+
107
+ # Test
108
+ make test
109
+
110
+ # Clean and reinstall
111
+ make clean && make install
112
+ ```
113
+
114
+ ## Documentation
115
+
116
+ - [ECS Guide](https://docs.decentraland.org/creator/development-guide/sdk7/entities-components/)
117
+ - [Component Reference](https://docs.decentraland.org/creator/development-guide/sdk7/components/)
118
+ - [ADR-117: CRDT Protocol](https://adr.decentraland.org/adr/ADR-117)
@@ -206,14 +206,22 @@ export function crdtSceneSystem(engine, onProcessEntityComponentChange) {
206
206
  // Send CRDT messages to transports
207
207
  const transportBuffer = new ReadWriteByteBuffer();
208
208
  for (const index in transports) {
209
+ // NetworkMessages can only have a MAX_SIZE of 13kb. So we need to send it in chunks.
210
+ const LIVEKIT_MAX_SIZE = 13;
211
+ const __NetworkMessagesBuffer = [];
209
212
  const transportIndex = Number(index);
210
213
  const transport = transports[transportIndex];
211
214
  const isRendererTransport = transport.type === 'renderer';
212
215
  const isNetworkTransport = transport.type === 'network';
216
+ // Reset Buffer for each Transport
213
217
  transportBuffer.resetBuffer();
214
218
  const buffer = new ReadWriteByteBuffer();
215
219
  // Then we send all the new crdtMessages that the transport needs to process
216
220
  for (const message of crdtMessages) {
221
+ if (isNetworkTransport && transportBuffer.toBinary().byteLength / 1024 > LIVEKIT_MAX_SIZE) {
222
+ __NetworkMessagesBuffer.push(transportBuffer.toBinary());
223
+ transportBuffer.resetBuffer();
224
+ }
217
225
  // Avoid echo messages
218
226
  if (message.transportId === transportIndex)
219
227
  continue;
@@ -261,7 +269,10 @@ export function crdtSceneSystem(engine, onProcessEntityComponentChange) {
261
269
  // Common message
262
270
  transportBuffer.writeBuffer(message.messageBuffer, false);
263
271
  }
264
- const message = transportBuffer.currentWriteOffset() ? transportBuffer.toBinary() : new Uint8Array([]);
272
+ if (isNetworkTransport && transportBuffer.currentWriteOffset()) {
273
+ __NetworkMessagesBuffer.push(transportBuffer.toBinary());
274
+ }
275
+ const message = isNetworkTransport ? __NetworkMessagesBuffer : transportBuffer.toBinary();
265
276
  await transport.send(message);
266
277
  }
267
278
  }
@@ -21,7 +21,11 @@ export type TransportMessage = Omit<ReceiveMessage, 'data'>;
21
21
  * @public
22
22
  */
23
23
  export type Transport = {
24
- send(message: Uint8Array): Promise<void>;
24
+ /**
25
+ * For Network messages its an Uint8Array[]. Due too the LiveKit MAX_SIZE = 13kb
26
+ * For Renderer & Other transports we send a single Uint8Array
27
+ */
28
+ send(message: Uint8Array | Uint8Array[]): Promise<void>;
25
29
  onmessage?(message: Uint8Array): void;
26
30
  filter(message: Omit<TransportMessage, 'messageBuffer'>): boolean;
27
31
  type?: string;
@@ -232,14 +232,22 @@ function crdtSceneSystem(engine, onProcessEntityComponentChange) {
232
232
  // Send CRDT messages to transports
233
233
  const transportBuffer = new ByteBuffer_1.ReadWriteByteBuffer();
234
234
  for (const index in transports) {
235
+ // NetworkMessages can only have a MAX_SIZE of 13kb. So we need to send it in chunks.
236
+ const LIVEKIT_MAX_SIZE = 13;
237
+ const __NetworkMessagesBuffer = [];
235
238
  const transportIndex = Number(index);
236
239
  const transport = transports[transportIndex];
237
240
  const isRendererTransport = transport.type === 'renderer';
238
241
  const isNetworkTransport = transport.type === 'network';
242
+ // Reset Buffer for each Transport
239
243
  transportBuffer.resetBuffer();
240
244
  const buffer = new ByteBuffer_1.ReadWriteByteBuffer();
241
245
  // Then we send all the new crdtMessages that the transport needs to process
242
246
  for (const message of crdtMessages) {
247
+ if (isNetworkTransport && transportBuffer.toBinary().byteLength / 1024 > LIVEKIT_MAX_SIZE) {
248
+ __NetworkMessagesBuffer.push(transportBuffer.toBinary());
249
+ transportBuffer.resetBuffer();
250
+ }
243
251
  // Avoid echo messages
244
252
  if (message.transportId === transportIndex)
245
253
  continue;
@@ -287,7 +295,10 @@ function crdtSceneSystem(engine, onProcessEntityComponentChange) {
287
295
  // Common message
288
296
  transportBuffer.writeBuffer(message.messageBuffer, false);
289
297
  }
290
- const message = transportBuffer.currentWriteOffset() ? transportBuffer.toBinary() : new Uint8Array([]);
298
+ if (isNetworkTransport && transportBuffer.currentWriteOffset()) {
299
+ __NetworkMessagesBuffer.push(transportBuffer.toBinary());
300
+ }
301
+ const message = isNetworkTransport ? __NetworkMessagesBuffer : transportBuffer.toBinary();
291
302
  await transport.send(message);
292
303
  }
293
304
  }
@@ -21,7 +21,11 @@ export type TransportMessage = Omit<ReceiveMessage, 'data'>;
21
21
  * @public
22
22
  */
23
23
  export type Transport = {
24
- send(message: Uint8Array): Promise<void>;
24
+ /**
25
+ * For Network messages its an Uint8Array[]. Due too the LiveKit MAX_SIZE = 13kb
26
+ * For Renderer & Other transports we send a single Uint8Array
27
+ */
28
+ send(message: Uint8Array | Uint8Array[]): Promise<void>;
25
29
  onmessage?(message: Uint8Array): void;
26
30
  filter(message: Omit<TransportMessage, 'messageBuffer'>): boolean;
27
31
  type?: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dcl/ecs",
3
3
  "description": "Decentraland ECS",
4
- "version": "7.7.3-13090257149.commit-df175f2",
4
+ "version": "7.7.3-13092675740.commit-ba2dd71",
5
5
  "author": "DCL",
6
6
  "bugs": "https://github.com/decentraland/ecs/issues",
7
7
  "files": [
@@ -33,5 +33,5 @@
33
33
  },
34
34
  "types": "./dist/index.d.ts",
35
35
  "typings": "./dist/index.d.ts",
36
- "commit": "df175f269c65f9b03e9f43af8d306839a7480361"
36
+ "commit": "ba2dd71f5c68f8e432074a9fdca6fed2b1b1b863"
37
37
  }