@dcl/ecs 7.26.1-31715266070.commit-0ddb8e7 → 7.26.1-32157237851.commit-e712ef7

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.
@@ -1,13 +1,5 @@
1
1
  import _m0 from "protobufjs/minimal";
2
2
  import { Color3 } from "../../common/colors.gen";
3
- /** Mask for which bones an animation applies to. */
4
- /**
5
- * @public
6
- */
7
- export declare const enum AvatarEmoteMask {
8
- AEM_FULL_BODY = 0,
9
- AEM_UPPER_BODY = 1
10
- }
11
3
  /**
12
4
  * The AvatarShape component contains the information required to draw and animate avatar, acting as
13
5
  * a simplified GLTF container for this specific case.
@@ -3,15 +3,6 @@ import Long from "long";
3
3
  import _m0 from "protobufjs/minimal";
4
4
  import { Color3 } from "../../common/colors.gen";
5
5
  const protobufPackageSarasa = "decentraland.sdk.components";
6
- /** Mask for which bones an animation applies to. */
7
- /**
8
- * @public
9
- */
10
- export var AvatarEmoteMask;
11
- (function (AvatarEmoteMask) {
12
- AvatarEmoteMask[AvatarEmoteMask["AEM_FULL_BODY"] = 0] = "AEM_FULL_BODY";
13
- AvatarEmoteMask[AvatarEmoteMask["AEM_UPPER_BODY"] = 1] = "AEM_UPPER_BODY";
14
- })(AvatarEmoteMask || (AvatarEmoteMask = {}));
15
6
  function createBasePBAvatarShape() {
16
7
  return {
17
8
  id: "",
@@ -56,6 +56,16 @@ export var EntityState;
56
56
  */
57
57
  EntityState[EntityState["Reserved"] = 3] = "Reserved";
58
58
  })(EntityState || (EntityState = {}));
59
+ /**
60
+ * True when `entity`'s NUMBER falls in the renderer-reserved range, at any version.
61
+ *
62
+ * Masks rather than calling `EntityUtils.fromEntityId`, which allocates a tuple per call to
63
+ * read one number; this runs on every inbound CRDT message. `entity & MAX_U16` already lands
64
+ * in [0, 65535], so the `>>> 0` fromEntityId applies is a no-op here.
65
+ */
66
+ function isReservedEntity(entity, reservedStaticEntities) {
67
+ return (entity & MAX_U16) < reservedStaticEntities;
68
+ }
59
69
  /**
60
70
  * @public
61
71
  */
@@ -90,7 +100,11 @@ export function createEntityContainer(opts) {
90
100
  return generateNewEntity();
91
101
  }
92
102
  for (const [number, version] of removedEntities.getMap()) {
93
- if (version < MAX_U16) {
103
+ // Never recycle a renderer-reserved number: the renderer reissues those slots as
104
+ // toEntityId(number, version + 1) too, from the same stored version, so it would hand
105
+ // the scene an id belonging to a live remote player. Belt-and-braces — every writer
106
+ // into `removedEntities` refuses reserved numbers, so this cannot fire today.
107
+ if (number >= reservedStaticEntities && version < MAX_U16) {
94
108
  const entity = EntityUtils.toEntityId(number, version + 1);
95
109
  // If the entity is not being used, we can re-use it
96
110
  // If the entity was removed in this tick, we're not counting for the usedEntities, but we have it in the toRemoveEntityArray
@@ -103,7 +117,7 @@ export function createEntityContainer(opts) {
103
117
  return generateNewEntity();
104
118
  }
105
119
  function removeEntity(entity) {
106
- if (entity < reservedStaticEntities)
120
+ if (isReservedEntity(entity, reservedStaticEntities))
107
121
  return false;
108
122
  if (usedEntities.has(entity)) {
109
123
  usedEntities.delete(entity);
@@ -126,6 +140,12 @@ export function createEntityContainer(opts) {
126
140
  return arr;
127
141
  }
128
142
  function updateRemovedEntity(entity) {
143
+ // Called for EVERY inbound DELETE_ENTITY, including the renderer's tombstones for
144
+ // departed remote players — so this is the door reserved numbers would otherwise enter
145
+ // the free list through. They need no tombstone: getEntityState reports them Reserved
146
+ // before it consults `removedEntities`, so `Removed` is unreachable for them anyway.
147
+ if (isReservedEntity(entity, reservedStaticEntities))
148
+ return false;
129
149
  const [n, v] = EntityUtils.fromEntityId(entity);
130
150
  // Update the removed entities map
131
151
  removedEntities.addTo(n, v);
@@ -136,6 +156,11 @@ export function createEntityContainer(opts) {
136
156
  return true;
137
157
  }
138
158
  function updateUsedEntity(entity) {
159
+ // Same invariant as updateRemovedEntity. Unreachable from the CRDT path today
160
+ // (getEntityState returns `Reserved`, never `Unknown`), but the `v > 0` branch below
161
+ // would seed `removedEntities` with a reserved number.
162
+ if (isReservedEntity(entity, reservedStaticEntities))
163
+ return false;
139
164
  const [n, v] = EntityUtils.fromEntityId(entity);
140
165
  // if the entity was removed then abort fast
141
166
  if (removedEntities.has(n, v))
@@ -151,10 +176,12 @@ export function createEntityContainer(opts) {
151
176
  return true;
152
177
  }
153
178
  function getEntityState(entity) {
154
- const [n, v] = EntityUtils.fromEntityId(entity);
155
- if (n < reservedStaticEntities) {
179
+ // Same guard as the three above, so `Engine.removeEntity` — which classifies via
180
+ // getEntityState cannot disagree with whether this container releases the id.
181
+ if (isReservedEntity(entity, reservedStaticEntities)) {
156
182
  return EntityState.Reserved;
157
183
  }
184
+ const [n, v] = EntityUtils.fromEntityId(entity);
158
185
  if (usedEntities.has(entity)) {
159
186
  return EntityState.UsedEntity;
160
187
  }
@@ -4,7 +4,7 @@ import { checkNotThenable } from '../runtime/invariant';
4
4
  import { Schemas } from '../schemas';
5
5
  import { crdtSceneSystem } from '../systems/crdt';
6
6
  import { createComponentDefinitionFromSchema } from './lww-element-set-component-definition';
7
- import { createEntityContainer } from './entity';
7
+ import { EntityState, EntityUtils, createEntityContainer } from './entity';
8
8
  import { SystemContainer, SYSTEMS_REGULAR_PRIORITY } from './systems';
9
9
  import { createValueSetComponentDefinitionFromSchema } from './grow-only-value-set-component-definition';
10
10
  import { removeEntityWithChildren as removeEntityWithChildrenEngine } from '../runtime/helpers/tree';
@@ -12,6 +12,8 @@ import { CrdtMessageType } from '../serialization/crdt';
12
12
  export * from './input';
13
13
  export * from './readonly';
14
14
  export * from './types';
15
+ /** RootEntity 0, PlayerEntity 1, CameraEntity 2 — see the engineInstance fields below. */
16
+ const NAMED_STATIC_ENTITIES = 3;
15
17
  function preEngine(options) {
16
18
  const entityContainer = options?.entityContainer ?? createEntityContainer();
17
19
  const componentsDefinition = new Map();
@@ -28,6 +30,18 @@ function preEngine(options) {
28
30
  return entity;
29
31
  }
30
32
  function removeEntity(entity) {
33
+ // The renderer streams the avatar range and drops the scene's deletes there, so purging
34
+ // locally is permanent: a one-shot component like PlayerIdentityData is never re-sent, and
35
+ // the entity is left as a moving Transform with no identity. The three named static
36
+ // entities are reserved too, but the renderer DOES apply scene deletes on them — that is
37
+ // how InputModifier.deleteFrom(engine.PlayerEntity) clears an input lock — so they must
38
+ // still be purged. Asking the container keeps this in step with whether it releases the
39
+ // id, including for a custom container with a different reserved range.
40
+ const [entityNumber] = EntityUtils.fromEntityId(entity);
41
+ const isAvatarEntity = entityNumber >= NAMED_STATIC_ENTITIES && entityContainer.getEntityState(entity) === EntityState.Reserved;
42
+ const released = entityContainer.removeEntity(entity);
43
+ if (isAvatarEntity)
44
+ return released;
31
45
  for (const [, component] of componentsDefinition) {
32
46
  // TODO: hack for the moment.
33
47
  // We still need the NetworkEntity to forward this message to the SyncTransport.
@@ -36,7 +50,7 @@ function preEngine(options) {
36
50
  continue;
37
51
  component.entityDeleted(entity, true);
38
52
  }
39
- return entityContainer.removeEntity(entity);
53
+ return released;
40
54
  }
41
55
  function removeEntityWithChildren(entity) {
42
56
  return removeEntityWithChildrenEngine({ removeEntity, defineComponentFromSchema, getEntitiesWith, defineComponent }, entity);
@@ -260,7 +274,12 @@ export function Engine(options) {
260
274
  RootEntity: 0,
261
275
  PlayerEntity: 1,
262
276
  CameraEntity: 2,
263
- getEntityState: partialEngine.entityContainer.getEntityState,
277
+ // Delegated, not aliased. A detached `entityContainer.getEntityState` reference binds
278
+ // `this` to the engine, so a custom IEntityContainer that uses `this` silently reports the
279
+ // wrong state through the public API while the engine itself, which calls the container
280
+ // directly, reports the right one. Harmless for the built-in closure-based container, but
281
+ // removeEntity now classifies through getEntityState, so the two must never diverge.
282
+ getEntityState: (entity) => partialEngine.entityContainer.getEntityState(entity),
264
283
  addTransport: crdtSystem.addTransport,
265
284
  entityContainer: partialEngine.entityContainer
266
285
  };
@@ -55,11 +55,18 @@ export interface IEngine {
55
55
  * @public
56
56
  * Remove all components of an entity
57
57
  * @param entity - entity
58
+ * @returns whether the entity id was released for reuse. Ids in the renderer-reserved range
59
+ * are never released, at any version. Components are still purged for
60
+ * RootEntity/PlayerEntity/CameraEntity, but not for the avatar range.
58
61
  */
59
- removeEntity(entity: Entity): void;
62
+ removeEntity(entity: Entity): boolean;
60
63
  /**
61
64
  * Remove all components of each entity in the tree made with Transform parenting
62
65
  * @param entity - the root entity of the tree
66
+ *
67
+ * May complete only partially and does not report it: nodes in the renderer-reserved range
68
+ * are refused by `removeEntity`, so a reserved node anywhere in the tree survives while its
69
+ * descendants are removed — leaving its `Transform.parent` pointing at a removed entity.
63
70
  */
64
71
  removeEntityWithChildren(entity: Entity): void;
65
72
  /**
@@ -177,7 +177,8 @@ export function createTriggerAreaEventsSystem(engine) {
177
177
  // Pass 2: synthesize per-tick onStay callbacks
178
178
  // -----------------------------------------------------------------------
179
179
  // Only run if an onStay callback is registered and there are tracked triggerers.
180
- if (data.triggerCallbackMap.has(1 /* TriggerAreaEventType.TAET_STAY */) && data.insideTriggerers.size > 0) {
180
+ if (data.triggerCallbackMap.has(1 /* TriggerAreaEventType.TAET_STAY */) &&
181
+ data.insideTriggerers.size > 0) {
181
182
  const onStay = data.triggerCallbackMap.get(1 /* TriggerAreaEventType.TAET_STAY */);
182
183
  const currentTimestamp = Date.now();
183
184
  for (const [triggererEntity, cachedResult] of data.insideTriggerers) {
@@ -1,13 +1,5 @@
1
1
  import _m0 from "protobufjs/minimal";
2
2
  import { Color3 } from "../../common/colors.gen";
3
- /** Mask for which bones an animation applies to. */
4
- /**
5
- * @public
6
- */
7
- export declare const enum AvatarEmoteMask {
8
- AEM_FULL_BODY = 0,
9
- AEM_UPPER_BODY = 1
10
- }
11
3
  /**
12
4
  * The AvatarShape component contains the information required to draw and animate avatar, acting as
13
5
  * a simplified GLTF container for this specific case.
@@ -3,21 +3,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.PBAvatarShape = exports.AvatarEmoteMask = void 0;
6
+ exports.PBAvatarShape = void 0;
7
7
  /* eslint-disable */
8
8
  const long_1 = __importDefault(require("long"));
9
9
  const minimal_1 = __importDefault(require("protobufjs/minimal"));
10
10
  const colors_gen_1 = require("../../common/colors.gen");
11
11
  const protobufPackageSarasa = "decentraland.sdk.components";
12
- /** Mask for which bones an animation applies to. */
13
- /**
14
- * @public
15
- */
16
- var AvatarEmoteMask;
17
- (function (AvatarEmoteMask) {
18
- AvatarEmoteMask[AvatarEmoteMask["AEM_FULL_BODY"] = 0] = "AEM_FULL_BODY";
19
- AvatarEmoteMask[AvatarEmoteMask["AEM_UPPER_BODY"] = 1] = "AEM_UPPER_BODY";
20
- })(AvatarEmoteMask = exports.AvatarEmoteMask || (exports.AvatarEmoteMask = {}));
21
12
  function createBasePBAvatarShape() {
22
13
  return {
23
14
  id: "",
@@ -59,6 +59,16 @@ var EntityState;
59
59
  */
60
60
  EntityState[EntityState["Reserved"] = 3] = "Reserved";
61
61
  })(EntityState = exports.EntityState || (exports.EntityState = {}));
62
+ /**
63
+ * True when `entity`'s NUMBER falls in the renderer-reserved range, at any version.
64
+ *
65
+ * Masks rather than calling `EntityUtils.fromEntityId`, which allocates a tuple per call to
66
+ * read one number; this runs on every inbound CRDT message. `entity & MAX_U16` already lands
67
+ * in [0, 65535], so the `>>> 0` fromEntityId applies is a no-op here.
68
+ */
69
+ function isReservedEntity(entity, reservedStaticEntities) {
70
+ return (entity & exports.MAX_U16) < reservedStaticEntities;
71
+ }
62
72
  /**
63
73
  * @public
64
74
  */
@@ -93,7 +103,11 @@ function createEntityContainer(opts) {
93
103
  return generateNewEntity();
94
104
  }
95
105
  for (const [number, version] of removedEntities.getMap()) {
96
- if (version < exports.MAX_U16) {
106
+ // Never recycle a renderer-reserved number: the renderer reissues those slots as
107
+ // toEntityId(number, version + 1) too, from the same stored version, so it would hand
108
+ // the scene an id belonging to a live remote player. Belt-and-braces — every writer
109
+ // into `removedEntities` refuses reserved numbers, so this cannot fire today.
110
+ if (number >= reservedStaticEntities && version < exports.MAX_U16) {
97
111
  const entity = EntityUtils.toEntityId(number, version + 1);
98
112
  // If the entity is not being used, we can re-use it
99
113
  // If the entity was removed in this tick, we're not counting for the usedEntities, but we have it in the toRemoveEntityArray
@@ -106,7 +120,7 @@ function createEntityContainer(opts) {
106
120
  return generateNewEntity();
107
121
  }
108
122
  function removeEntity(entity) {
109
- if (entity < reservedStaticEntities)
123
+ if (isReservedEntity(entity, reservedStaticEntities))
110
124
  return false;
111
125
  if (usedEntities.has(entity)) {
112
126
  usedEntities.delete(entity);
@@ -129,6 +143,12 @@ function createEntityContainer(opts) {
129
143
  return arr;
130
144
  }
131
145
  function updateRemovedEntity(entity) {
146
+ // Called for EVERY inbound DELETE_ENTITY, including the renderer's tombstones for
147
+ // departed remote players — so this is the door reserved numbers would otherwise enter
148
+ // the free list through. They need no tombstone: getEntityState reports them Reserved
149
+ // before it consults `removedEntities`, so `Removed` is unreachable for them anyway.
150
+ if (isReservedEntity(entity, reservedStaticEntities))
151
+ return false;
132
152
  const [n, v] = EntityUtils.fromEntityId(entity);
133
153
  // Update the removed entities map
134
154
  removedEntities.addTo(n, v);
@@ -139,6 +159,11 @@ function createEntityContainer(opts) {
139
159
  return true;
140
160
  }
141
161
  function updateUsedEntity(entity) {
162
+ // Same invariant as updateRemovedEntity. Unreachable from the CRDT path today
163
+ // (getEntityState returns `Reserved`, never `Unknown`), but the `v > 0` branch below
164
+ // would seed `removedEntities` with a reserved number.
165
+ if (isReservedEntity(entity, reservedStaticEntities))
166
+ return false;
142
167
  const [n, v] = EntityUtils.fromEntityId(entity);
143
168
  // if the entity was removed then abort fast
144
169
  if (removedEntities.has(n, v))
@@ -154,10 +179,12 @@ function createEntityContainer(opts) {
154
179
  return true;
155
180
  }
156
181
  function getEntityState(entity) {
157
- const [n, v] = EntityUtils.fromEntityId(entity);
158
- if (n < reservedStaticEntities) {
182
+ // Same guard as the three above, so `Engine.removeEntity` — which classifies via
183
+ // getEntityState cannot disagree with whether this container releases the id.
184
+ if (isReservedEntity(entity, reservedStaticEntities)) {
159
185
  return EntityState.Reserved;
160
186
  }
187
+ const [n, v] = EntityUtils.fromEntityId(entity);
161
188
  if (usedEntities.has(entity)) {
162
189
  return EntityState.UsedEntity;
163
190
  }
@@ -41,6 +41,8 @@ const crdt_2 = require("../serialization/crdt");
41
41
  __exportStar(require("./input"), exports);
42
42
  __exportStar(require("./readonly"), exports);
43
43
  __exportStar(require("./types"), exports);
44
+ /** RootEntity 0, PlayerEntity 1, CameraEntity 2 — see the engineInstance fields below. */
45
+ const NAMED_STATIC_ENTITIES = 3;
44
46
  function preEngine(options) {
45
47
  const entityContainer = options?.entityContainer ?? (0, entity_1.createEntityContainer)();
46
48
  const componentsDefinition = new Map();
@@ -57,6 +59,18 @@ function preEngine(options) {
57
59
  return entity;
58
60
  }
59
61
  function removeEntity(entity) {
62
+ // The renderer streams the avatar range and drops the scene's deletes there, so purging
63
+ // locally is permanent: a one-shot component like PlayerIdentityData is never re-sent, and
64
+ // the entity is left as a moving Transform with no identity. The three named static
65
+ // entities are reserved too, but the renderer DOES apply scene deletes on them — that is
66
+ // how InputModifier.deleteFrom(engine.PlayerEntity) clears an input lock — so they must
67
+ // still be purged. Asking the container keeps this in step with whether it releases the
68
+ // id, including for a custom container with a different reserved range.
69
+ const [entityNumber] = entity_1.EntityUtils.fromEntityId(entity);
70
+ const isAvatarEntity = entityNumber >= NAMED_STATIC_ENTITIES && entityContainer.getEntityState(entity) === entity_1.EntityState.Reserved;
71
+ const released = entityContainer.removeEntity(entity);
72
+ if (isAvatarEntity)
73
+ return released;
60
74
  for (const [, component] of componentsDefinition) {
61
75
  // TODO: hack for the moment.
62
76
  // We still need the NetworkEntity to forward this message to the SyncTransport.
@@ -65,7 +79,7 @@ function preEngine(options) {
65
79
  continue;
66
80
  component.entityDeleted(entity, true);
67
81
  }
68
- return entityContainer.removeEntity(entity);
82
+ return released;
69
83
  }
70
84
  function removeEntityWithChildren(entity) {
71
85
  return (0, tree_1.removeEntityWithChildren)({ removeEntity, defineComponentFromSchema, getEntitiesWith, defineComponent }, entity);
@@ -289,7 +303,12 @@ function Engine(options) {
289
303
  RootEntity: 0,
290
304
  PlayerEntity: 1,
291
305
  CameraEntity: 2,
292
- getEntityState: partialEngine.entityContainer.getEntityState,
306
+ // Delegated, not aliased. A detached `entityContainer.getEntityState` reference binds
307
+ // `this` to the engine, so a custom IEntityContainer that uses `this` silently reports the
308
+ // wrong state through the public API while the engine itself, which calls the container
309
+ // directly, reports the right one. Harmless for the built-in closure-based container, but
310
+ // removeEntity now classifies through getEntityState, so the two must never diverge.
311
+ getEntityState: (entity) => partialEngine.entityContainer.getEntityState(entity),
293
312
  addTransport: crdtSystem.addTransport,
294
313
  entityContainer: partialEngine.entityContainer
295
314
  };
@@ -55,11 +55,18 @@ export interface IEngine {
55
55
  * @public
56
56
  * Remove all components of an entity
57
57
  * @param entity - entity
58
+ * @returns whether the entity id was released for reuse. Ids in the renderer-reserved range
59
+ * are never released, at any version. Components are still purged for
60
+ * RootEntity/PlayerEntity/CameraEntity, but not for the avatar range.
58
61
  */
59
- removeEntity(entity: Entity): void;
62
+ removeEntity(entity: Entity): boolean;
60
63
  /**
61
64
  * Remove all components of each entity in the tree made with Transform parenting
62
65
  * @param entity - the root entity of the tree
66
+ *
67
+ * May complete only partially and does not report it: nodes in the renderer-reserved range
68
+ * are refused by `removeEntity`, so a reserved node anywhere in the tree survives while its
69
+ * descendants are removed — leaving its `Transform.parent` pointing at a removed entity.
63
70
  */
64
71
  removeEntityWithChildren(entity: Entity): void;
65
72
  /**
@@ -203,7 +203,8 @@ function createTriggerAreaEventsSystem(engine) {
203
203
  // Pass 2: synthesize per-tick onStay callbacks
204
204
  // -----------------------------------------------------------------------
205
205
  // Only run if an onStay callback is registered and there are tracked triggerers.
206
- if (data.triggerCallbackMap.has(1 /* TriggerAreaEventType.TAET_STAY */) && data.insideTriggerers.size > 0) {
206
+ if (data.triggerCallbackMap.has(1 /* TriggerAreaEventType.TAET_STAY */) &&
207
+ data.insideTriggerers.size > 0) {
207
208
  const onStay = data.triggerCallbackMap.get(1 /* TriggerAreaEventType.TAET_STAY */);
208
209
  const currentTimestamp = Date.now();
209
210
  for (const [triggererEntity, cachedResult] of data.insideTriggerers) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dcl/ecs",
3
3
  "description": "Decentraland ECS",
4
- "version": "7.26.1-31715266070.commit-0ddb8e7",
4
+ "version": "7.26.1-32157237851.commit-e712ef7",
5
5
  "author": "DCL",
6
6
  "bugs": "https://github.com/decentraland/ecs/issues",
7
7
  "files": [
@@ -34,5 +34,5 @@
34
34
  "dependencies": {},
35
35
  "types": "./dist/index.d.ts",
36
36
  "typings": "./dist/index.d.ts",
37
- "commit": "0ddb8e747bcf2d28e5bd201fe4e898d4ec08036d"
37
+ "commit": "e712ef71c81b53f1125e7b9658f3751ab455b63e"
38
38
  }