@dcl/ecs 7.23.2-25670347846.commit-ae6c2f7 → 7.23.2-25802088407.commit-15e8697

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.
@@ -26,6 +26,11 @@ export interface TriggerAreaEventsSystem {
26
26
  * Execute callback when an entity stays in the Trigger Area
27
27
  * @param entity - The entity that already has the TriggerArea component
28
28
  * @param cb - Function to execute the 'Stay' type of result is detected
29
+ *
30
+ * Note: stay callbacks are synthesized by the SDK on every tick between a wire ENTER and a wire EXIT.
31
+ * Wire-level TAET_STAY events (still emitted by legacy Explorers) are ignored entirely — they neither
32
+ * fire callbacks nor mutate state. The SDK is the sole source of onTriggerStay dispatches, driven
33
+ * from the ENTER/EXIT state machine.
29
34
  */
30
35
  onTriggerStay(entity: Entity, cb: TriggerAreaEventSystemCallback): void;
31
36
  /**
@@ -1,10 +1,57 @@
1
1
  import * as components from '../components';
2
2
  import { EntityState } from '../engine/entity';
3
+ /**
4
+ * Builds a synthetic PBTriggerAreaResult for a per-tick onStay callback.
5
+ *
6
+ * Transform components are resolved at call time so that scene-owned entities report
7
+ * up-to-date position/rotation/scale. For player-avatar triggerers (reserved entities
8
+ * without a scene-side Transform), there is no scene Transform component, so the cached
9
+ * values from the last ENTER or wire-STAY event are used as-is. These cached values may
10
+ * be slightly stale for the current frame — this is expected and acceptable for the
11
+ * avatar case.
12
+ */
13
+ function buildSyntheticStayResult(cached, triggerAreaEntity, triggererEntity, currentTimestamp, Transform) {
14
+ // Shallow-clone the trigger sub-object so we can mutate it.
15
+ const trigger = cached.trigger
16
+ ? {
17
+ entity: cached.trigger.entity,
18
+ layers: cached.trigger.layers,
19
+ position: cached.trigger.position ? { ...cached.trigger.position } : undefined,
20
+ rotation: cached.trigger.rotation ? { ...cached.trigger.rotation } : undefined,
21
+ scale: cached.trigger.scale ? { ...cached.trigger.scale } : undefined
22
+ }
23
+ : undefined;
24
+ // Build the cloned result with a forced TAET_STAY eventType and refreshed timestamp.
25
+ const result = {
26
+ triggeredEntity: cached.triggeredEntity,
27
+ triggeredEntityPosition: cached.triggeredEntityPosition ? { ...cached.triggeredEntityPosition } : undefined,
28
+ triggeredEntityRotation: cached.triggeredEntityRotation ? { ...cached.triggeredEntityRotation } : undefined,
29
+ eventType: 1 /* TriggerAreaEventType.TAET_STAY */,
30
+ timestamp: currentTimestamp,
31
+ trigger
32
+ };
33
+ // Refresh trigger-area entity transform when it is scene-owned.
34
+ const triggerAreaTransform = Transform.getOrNull(triggerAreaEntity);
35
+ if (triggerAreaTransform !== null) {
36
+ result.triggeredEntityPosition = { ...triggerAreaTransform.position };
37
+ result.triggeredEntityRotation = { ...triggerAreaTransform.rotation };
38
+ }
39
+ // Refresh triggerer transform when it is scene-owned.
40
+ // For player-avatar entities (reserved, no scene-side Transform) the cached values are kept.
41
+ const triggererTransform = Transform.getOrNull(triggererEntity);
42
+ if (triggererTransform !== null && result.trigger) {
43
+ result.trigger.position = { ...triggererTransform.position };
44
+ result.trigger.rotation = { ...triggererTransform.rotation };
45
+ result.trigger.scale = { ...triggererTransform.scale };
46
+ }
47
+ return result;
48
+ }
3
49
  /**
4
50
  * @internal
5
51
  */
6
52
  export function createTriggerAreaEventsSystem(engine) {
7
53
  const triggerAreaResultComponent = components.TriggerAreaResult(engine);
54
+ const Transform = components.Transform(engine);
8
55
  const entitiesMap = new Map();
9
56
  function hasCallbacksMap(entity) {
10
57
  return entitiesMap.has(entity) && entitiesMap.get(entity) !== undefined;
@@ -16,7 +63,8 @@ export function createTriggerAreaEventsSystem(engine) {
16
63
  else {
17
64
  entitiesMap.set(entity, {
18
65
  triggerCallbackMap: new Map([[triggerType, callback]]),
19
- lastConsumedTimestamp: -1
66
+ lastConsumedTimestamp: -1,
67
+ insideTriggerers: new Map()
20
68
  });
21
69
  }
22
70
  }
@@ -25,7 +73,9 @@ export function createTriggerAreaEventsSystem(engine) {
25
73
  return;
26
74
  const triggerCallbackMap = entitiesMap.get(entity).triggerCallbackMap;
27
75
  triggerCallbackMap.delete(triggerType);
28
- // Remove entity if no more trigger callbacks are registered
76
+ // Remove entity if no more trigger callbacks are registered.
77
+ // insideTriggerers is intentionally left populated so that re-subscription picks up
78
+ // in-flight sessions without missing the first synthesized onStay.
29
79
  if (triggerCallbackMap.size === 0)
30
80
  entitiesMap.delete(entity);
31
81
  }
@@ -55,51 +105,88 @@ export function createTriggerAreaEventsSystem(engine) {
55
105
  continue;
56
106
  }
57
107
  const result = triggerAreaResultComponent.get(entity);
58
- // The Explorer may be taking time before the result component is put
59
- if (result.size === 0)
60
- continue;
61
- const values = Array.from(result.values());
62
- // determine starting index for new values (more than one could be added between System updates)
63
- // search backwards to find the anchor at lastConsumedTimestamp
64
- let startIndex = 0;
65
- if (data.lastConsumedTimestamp >= 0) {
66
- const newestTimestamp = values[values.length - 1].timestamp;
67
- // if nothing new, skip processing
68
- if (newestTimestamp <= data.lastConsumedTimestamp) {
69
- continue;
108
+ // -----------------------------------------------------------------------
109
+ // Pass 1: drain new GOVS events
110
+ // -----------------------------------------------------------------------
111
+ // The Explorer may be taking time before the result component is put.
112
+ if (result.size > 0) {
113
+ const values = Array.from(result.values());
114
+ // Determine starting index for new values (more than one could be added between System updates).
115
+ // Search backwards to find the anchor at lastConsumedTimestamp.
116
+ let startIndex = 0;
117
+ if (data.lastConsumedTimestamp >= 0) {
118
+ const newestTimestamp = values[values.length - 1].timestamp;
119
+ // If nothing new, skip processing.
120
+ if (newestTimestamp > data.lastConsumedTimestamp) {
121
+ // Find index of value with the lastConsumedTimestamp.
122
+ let i = values.length - 2;
123
+ while (i >= 0 && values[i].timestamp > data.lastConsumedTimestamp)
124
+ i--;
125
+ // Mark the following value index as the starting point to trigger all the new value callbacks.
126
+ startIndex = i + 1;
127
+ }
128
+ else {
129
+ // No new events — skip to Pass 2.
130
+ startIndex = values.length;
131
+ }
132
+ }
133
+ if (startIndex < values.length) {
134
+ // Process new wire events in chronological order.
135
+ for (let i = startIndex; i < values.length; i++) {
136
+ const event = values[i];
137
+ switch (event.eventType) {
138
+ case 0 /* TriggerAreaEventType.TAET_ENTER */:
139
+ // Update in-flight tracking before firing the callback.
140
+ data.insideTriggerers.set(event.trigger.entity, {
141
+ triggeredEntity: event.triggeredEntity,
142
+ triggeredEntityPosition: event.triggeredEntityPosition
143
+ ? { ...event.triggeredEntityPosition }
144
+ : undefined,
145
+ triggeredEntityRotation: event.triggeredEntityRotation
146
+ ? { ...event.triggeredEntityRotation }
147
+ : undefined,
148
+ eventType: event.eventType,
149
+ timestamp: event.timestamp,
150
+ trigger: event.trigger
151
+ ? {
152
+ entity: event.trigger.entity,
153
+ layers: event.trigger.layers,
154
+ position: event.trigger.position ? { ...event.trigger.position } : undefined,
155
+ rotation: event.trigger.rotation ? { ...event.trigger.rotation } : undefined,
156
+ scale: event.trigger.scale ? { ...event.trigger.scale } : undefined
157
+ }
158
+ : undefined
159
+ });
160
+ if (data.triggerCallbackMap.has(0 /* TriggerAreaEventType.TAET_ENTER */)) {
161
+ data.triggerCallbackMap.get(0 /* TriggerAreaEventType.TAET_ENTER */)(event);
162
+ }
163
+ break;
164
+ case 2 /* TriggerAreaEventType.TAET_EXIT */:
165
+ data.insideTriggerers.delete(event.trigger.entity);
166
+ if (data.triggerCallbackMap.has(2 /* TriggerAreaEventType.TAET_EXIT */)) {
167
+ data.triggerCallbackMap.get(2 /* TriggerAreaEventType.TAET_EXIT */)(event);
168
+ }
169
+ break;
170
+ // Wire-level TAET_STAY and any unknown event types are ignored — no callback, no state mutation.
171
+ }
172
+ }
173
+ data.lastConsumedTimestamp = values[values.length - 1].timestamp;
70
174
  }
71
- // Find index of value with the lastConsumedTimestamp
72
- let i = values.length - 2;
73
- while (i >= 0 && values[i].timestamp > data.lastConsumedTimestamp)
74
- i--;
75
- // Mark the following value index as the starting point to trigger all the new value callbacks
76
- startIndex = i + 1;
77
175
  }
78
- if (startIndex >= values.length)
79
- continue;
80
- // Trigger callbacks for all the new values
81
- for (let i = startIndex; i < values.length; i++) {
82
- switch (values[i].eventType) {
83
- case 0 /* TriggerAreaEventType.TAET_ENTER */:
84
- if (!data.triggerCallbackMap.has(0 /* TriggerAreaEventType.TAET_ENTER */))
85
- continue;
86
- data.triggerCallbackMap.get(0 /* TriggerAreaEventType.TAET_ENTER */)(values[i]);
87
- break;
88
- case 1 /* TriggerAreaEventType.TAET_STAY */:
89
- if (!data.triggerCallbackMap.has(1 /* TriggerAreaEventType.TAET_STAY */))
90
- continue;
91
- data.triggerCallbackMap.get(1 /* TriggerAreaEventType.TAET_STAY */)(values[i]);
92
- break;
93
- case 2 /* TriggerAreaEventType.TAET_EXIT */:
94
- if (!data.triggerCallbackMap.has(2 /* TriggerAreaEventType.TAET_EXIT */))
95
- continue;
96
- data.triggerCallbackMap.get(2 /* TriggerAreaEventType.TAET_EXIT */)(values[i]);
97
- break;
176
+ // -----------------------------------------------------------------------
177
+ // Pass 2: synthesize per-tick onStay callbacks
178
+ // -----------------------------------------------------------------------
179
+ // Only run if an onStay callback is registered and there are tracked triggerers.
180
+ if (data.triggerCallbackMap.has(1 /* TriggerAreaEventType.TAET_STAY */) &&
181
+ data.insideTriggerers.size > 0) {
182
+ const onStay = data.triggerCallbackMap.get(1 /* TriggerAreaEventType.TAET_STAY */);
183
+ const currentTimestamp = Date.now();
184
+ for (const [triggererEntity, cachedResult] of data.insideTriggerers) {
185
+ onStay(buildSyntheticStayResult(cachedResult, entity, triggererEntity, currentTimestamp, Transform));
98
186
  }
99
187
  }
100
- data.lastConsumedTimestamp = values[values.length - 1].timestamp;
101
188
  }
102
- // Clean up garbage entries
189
+ // Clean up garbage entries.
103
190
  garbageEntries.forEach((garbageEntity) => entitiesMap.delete(garbageEntity));
104
191
  });
105
192
  return {
@@ -26,6 +26,11 @@ export interface TriggerAreaEventsSystem {
26
26
  * Execute callback when an entity stays in the Trigger Area
27
27
  * @param entity - The entity that already has the TriggerArea component
28
28
  * @param cb - Function to execute the 'Stay' type of result is detected
29
+ *
30
+ * Note: stay callbacks are synthesized by the SDK on every tick between a wire ENTER and a wire EXIT.
31
+ * Wire-level TAET_STAY events (still emitted by legacy Explorers) are ignored entirely — they neither
32
+ * fire callbacks nor mutate state. The SDK is the sole source of onTriggerStay dispatches, driven
33
+ * from the ENTER/EXIT state machine.
29
34
  */
30
35
  onTriggerStay(entity: Entity, cb: TriggerAreaEventSystemCallback): void;
31
36
  /**
@@ -26,11 +26,58 @@ Object.defineProperty(exports, "__esModule", { value: true });
26
26
  exports.createTriggerAreaEventsSystem = void 0;
27
27
  const components = __importStar(require("../components"));
28
28
  const entity_1 = require("../engine/entity");
29
+ /**
30
+ * Builds a synthetic PBTriggerAreaResult for a per-tick onStay callback.
31
+ *
32
+ * Transform components are resolved at call time so that scene-owned entities report
33
+ * up-to-date position/rotation/scale. For player-avatar triggerers (reserved entities
34
+ * without a scene-side Transform), there is no scene Transform component, so the cached
35
+ * values from the last ENTER or wire-STAY event are used as-is. These cached values may
36
+ * be slightly stale for the current frame — this is expected and acceptable for the
37
+ * avatar case.
38
+ */
39
+ function buildSyntheticStayResult(cached, triggerAreaEntity, triggererEntity, currentTimestamp, Transform) {
40
+ // Shallow-clone the trigger sub-object so we can mutate it.
41
+ const trigger = cached.trigger
42
+ ? {
43
+ entity: cached.trigger.entity,
44
+ layers: cached.trigger.layers,
45
+ position: cached.trigger.position ? { ...cached.trigger.position } : undefined,
46
+ rotation: cached.trigger.rotation ? { ...cached.trigger.rotation } : undefined,
47
+ scale: cached.trigger.scale ? { ...cached.trigger.scale } : undefined
48
+ }
49
+ : undefined;
50
+ // Build the cloned result with a forced TAET_STAY eventType and refreshed timestamp.
51
+ const result = {
52
+ triggeredEntity: cached.triggeredEntity,
53
+ triggeredEntityPosition: cached.triggeredEntityPosition ? { ...cached.triggeredEntityPosition } : undefined,
54
+ triggeredEntityRotation: cached.triggeredEntityRotation ? { ...cached.triggeredEntityRotation } : undefined,
55
+ eventType: 1 /* TriggerAreaEventType.TAET_STAY */,
56
+ timestamp: currentTimestamp,
57
+ trigger
58
+ };
59
+ // Refresh trigger-area entity transform when it is scene-owned.
60
+ const triggerAreaTransform = Transform.getOrNull(triggerAreaEntity);
61
+ if (triggerAreaTransform !== null) {
62
+ result.triggeredEntityPosition = { ...triggerAreaTransform.position };
63
+ result.triggeredEntityRotation = { ...triggerAreaTransform.rotation };
64
+ }
65
+ // Refresh triggerer transform when it is scene-owned.
66
+ // For player-avatar entities (reserved, no scene-side Transform) the cached values are kept.
67
+ const triggererTransform = Transform.getOrNull(triggererEntity);
68
+ if (triggererTransform !== null && result.trigger) {
69
+ result.trigger.position = { ...triggererTransform.position };
70
+ result.trigger.rotation = { ...triggererTransform.rotation };
71
+ result.trigger.scale = { ...triggererTransform.scale };
72
+ }
73
+ return result;
74
+ }
29
75
  /**
30
76
  * @internal
31
77
  */
32
78
  function createTriggerAreaEventsSystem(engine) {
33
79
  const triggerAreaResultComponent = components.TriggerAreaResult(engine);
80
+ const Transform = components.Transform(engine);
34
81
  const entitiesMap = new Map();
35
82
  function hasCallbacksMap(entity) {
36
83
  return entitiesMap.has(entity) && entitiesMap.get(entity) !== undefined;
@@ -42,7 +89,8 @@ function createTriggerAreaEventsSystem(engine) {
42
89
  else {
43
90
  entitiesMap.set(entity, {
44
91
  triggerCallbackMap: new Map([[triggerType, callback]]),
45
- lastConsumedTimestamp: -1
92
+ lastConsumedTimestamp: -1,
93
+ insideTriggerers: new Map()
46
94
  });
47
95
  }
48
96
  }
@@ -51,7 +99,9 @@ function createTriggerAreaEventsSystem(engine) {
51
99
  return;
52
100
  const triggerCallbackMap = entitiesMap.get(entity).triggerCallbackMap;
53
101
  triggerCallbackMap.delete(triggerType);
54
- // Remove entity if no more trigger callbacks are registered
102
+ // Remove entity if no more trigger callbacks are registered.
103
+ // insideTriggerers is intentionally left populated so that re-subscription picks up
104
+ // in-flight sessions without missing the first synthesized onStay.
55
105
  if (triggerCallbackMap.size === 0)
56
106
  entitiesMap.delete(entity);
57
107
  }
@@ -81,51 +131,88 @@ function createTriggerAreaEventsSystem(engine) {
81
131
  continue;
82
132
  }
83
133
  const result = triggerAreaResultComponent.get(entity);
84
- // The Explorer may be taking time before the result component is put
85
- if (result.size === 0)
86
- continue;
87
- const values = Array.from(result.values());
88
- // determine starting index for new values (more than one could be added between System updates)
89
- // search backwards to find the anchor at lastConsumedTimestamp
90
- let startIndex = 0;
91
- if (data.lastConsumedTimestamp >= 0) {
92
- const newestTimestamp = values[values.length - 1].timestamp;
93
- // if nothing new, skip processing
94
- if (newestTimestamp <= data.lastConsumedTimestamp) {
95
- continue;
134
+ // -----------------------------------------------------------------------
135
+ // Pass 1: drain new GOVS events
136
+ // -----------------------------------------------------------------------
137
+ // The Explorer may be taking time before the result component is put.
138
+ if (result.size > 0) {
139
+ const values = Array.from(result.values());
140
+ // Determine starting index for new values (more than one could be added between System updates).
141
+ // Search backwards to find the anchor at lastConsumedTimestamp.
142
+ let startIndex = 0;
143
+ if (data.lastConsumedTimestamp >= 0) {
144
+ const newestTimestamp = values[values.length - 1].timestamp;
145
+ // If nothing new, skip processing.
146
+ if (newestTimestamp > data.lastConsumedTimestamp) {
147
+ // Find index of value with the lastConsumedTimestamp.
148
+ let i = values.length - 2;
149
+ while (i >= 0 && values[i].timestamp > data.lastConsumedTimestamp)
150
+ i--;
151
+ // Mark the following value index as the starting point to trigger all the new value callbacks.
152
+ startIndex = i + 1;
153
+ }
154
+ else {
155
+ // No new events — skip to Pass 2.
156
+ startIndex = values.length;
157
+ }
158
+ }
159
+ if (startIndex < values.length) {
160
+ // Process new wire events in chronological order.
161
+ for (let i = startIndex; i < values.length; i++) {
162
+ const event = values[i];
163
+ switch (event.eventType) {
164
+ case 0 /* TriggerAreaEventType.TAET_ENTER */:
165
+ // Update in-flight tracking before firing the callback.
166
+ data.insideTriggerers.set(event.trigger.entity, {
167
+ triggeredEntity: event.triggeredEntity,
168
+ triggeredEntityPosition: event.triggeredEntityPosition
169
+ ? { ...event.triggeredEntityPosition }
170
+ : undefined,
171
+ triggeredEntityRotation: event.triggeredEntityRotation
172
+ ? { ...event.triggeredEntityRotation }
173
+ : undefined,
174
+ eventType: event.eventType,
175
+ timestamp: event.timestamp,
176
+ trigger: event.trigger
177
+ ? {
178
+ entity: event.trigger.entity,
179
+ layers: event.trigger.layers,
180
+ position: event.trigger.position ? { ...event.trigger.position } : undefined,
181
+ rotation: event.trigger.rotation ? { ...event.trigger.rotation } : undefined,
182
+ scale: event.trigger.scale ? { ...event.trigger.scale } : undefined
183
+ }
184
+ : undefined
185
+ });
186
+ if (data.triggerCallbackMap.has(0 /* TriggerAreaEventType.TAET_ENTER */)) {
187
+ data.triggerCallbackMap.get(0 /* TriggerAreaEventType.TAET_ENTER */)(event);
188
+ }
189
+ break;
190
+ case 2 /* TriggerAreaEventType.TAET_EXIT */:
191
+ data.insideTriggerers.delete(event.trigger.entity);
192
+ if (data.triggerCallbackMap.has(2 /* TriggerAreaEventType.TAET_EXIT */)) {
193
+ data.triggerCallbackMap.get(2 /* TriggerAreaEventType.TAET_EXIT */)(event);
194
+ }
195
+ break;
196
+ // Wire-level TAET_STAY and any unknown event types are ignored — no callback, no state mutation.
197
+ }
198
+ }
199
+ data.lastConsumedTimestamp = values[values.length - 1].timestamp;
96
200
  }
97
- // Find index of value with the lastConsumedTimestamp
98
- let i = values.length - 2;
99
- while (i >= 0 && values[i].timestamp > data.lastConsumedTimestamp)
100
- i--;
101
- // Mark the following value index as the starting point to trigger all the new value callbacks
102
- startIndex = i + 1;
103
201
  }
104
- if (startIndex >= values.length)
105
- continue;
106
- // Trigger callbacks for all the new values
107
- for (let i = startIndex; i < values.length; i++) {
108
- switch (values[i].eventType) {
109
- case 0 /* TriggerAreaEventType.TAET_ENTER */:
110
- if (!data.triggerCallbackMap.has(0 /* TriggerAreaEventType.TAET_ENTER */))
111
- continue;
112
- data.triggerCallbackMap.get(0 /* TriggerAreaEventType.TAET_ENTER */)(values[i]);
113
- break;
114
- case 1 /* TriggerAreaEventType.TAET_STAY */:
115
- if (!data.triggerCallbackMap.has(1 /* TriggerAreaEventType.TAET_STAY */))
116
- continue;
117
- data.triggerCallbackMap.get(1 /* TriggerAreaEventType.TAET_STAY */)(values[i]);
118
- break;
119
- case 2 /* TriggerAreaEventType.TAET_EXIT */:
120
- if (!data.triggerCallbackMap.has(2 /* TriggerAreaEventType.TAET_EXIT */))
121
- continue;
122
- data.triggerCallbackMap.get(2 /* TriggerAreaEventType.TAET_EXIT */)(values[i]);
123
- break;
202
+ // -----------------------------------------------------------------------
203
+ // Pass 2: synthesize per-tick onStay callbacks
204
+ // -----------------------------------------------------------------------
205
+ // Only run if an onStay callback is registered and there are tracked triggerers.
206
+ if (data.triggerCallbackMap.has(1 /* TriggerAreaEventType.TAET_STAY */) &&
207
+ data.insideTriggerers.size > 0) {
208
+ const onStay = data.triggerCallbackMap.get(1 /* TriggerAreaEventType.TAET_STAY */);
209
+ const currentTimestamp = Date.now();
210
+ for (const [triggererEntity, cachedResult] of data.insideTriggerers) {
211
+ onStay(buildSyntheticStayResult(cachedResult, entity, triggererEntity, currentTimestamp, Transform));
124
212
  }
125
213
  }
126
- data.lastConsumedTimestamp = values[values.length - 1].timestamp;
127
214
  }
128
- // Clean up garbage entries
215
+ // Clean up garbage entries.
129
216
  garbageEntries.forEach((garbageEntity) => entitiesMap.delete(garbageEntity));
130
217
  });
131
218
  return {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dcl/ecs",
3
3
  "description": "Decentraland ECS",
4
- "version": "7.23.2-25670347846.commit-ae6c2f7",
4
+ "version": "7.23.2-25802088407.commit-15e8697",
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": "ae6c2f752c70332beca70e0e90095a1f70a46d2a"
37
+ "commit": "15e86977205ad099c6c1b81f6c14e1acb7b52bd7"
38
38
  }