Skip to content

Commit 0b97733

Browse files
authored
Merge pull request #1548 from decentraland/chore/sync-main-to-authserver
chore: sync main to authserver
2 parents 96e9a29 + d83690c commit 0b97733

17 files changed

Lines changed: 508 additions & 51 deletions

packages/@dcl/ecs/src/engine/entity.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,17 @@ export type IEntityContainer = {
101101
updateUsedEntity(entity: Entity): boolean
102102
}
103103

104+
/**
105+
* True when `entity`'s NUMBER falls in the renderer-reserved range, at any version.
106+
*
107+
* Masks rather than calling `EntityUtils.fromEntityId`, which allocates a tuple per call to
108+
* read one number; this runs on every inbound CRDT message. `entity & MAX_U16` already lands
109+
* in [0, 65535], so the `>>> 0` fromEntityId applies is a no-op here.
110+
*/
111+
function isReservedEntity(entity: Entity, reservedStaticEntities: number): boolean {
112+
return (entity & MAX_U16) < reservedStaticEntities
113+
}
114+
104115
/**
105116
* @public
106117
*/
@@ -143,7 +154,11 @@ export function createEntityContainer(opts?: { reservedStaticEntities: number })
143154
}
144155

145156
for (const [number, version] of removedEntities.getMap()) {
146-
if (version < MAX_U16) {
157+
// Never recycle a renderer-reserved number: the renderer reissues those slots as
158+
// toEntityId(number, version + 1) too, from the same stored version, so it would hand
159+
// the scene an id belonging to a live remote player. Belt-and-braces — every writer
160+
// into `removedEntities` refuses reserved numbers, so this cannot fire today.
161+
if (number >= reservedStaticEntities && version < MAX_U16) {
147162
const entity = EntityUtils.toEntityId(number, version + 1)
148163
// If the entity is not being used, we can re-use it
149164
// If the entity was removed in this tick, we're not counting for the usedEntities, but we have it in the toRemoveEntityArray
@@ -158,7 +173,7 @@ export function createEntityContainer(opts?: { reservedStaticEntities: number })
158173
}
159174

160175
function removeEntity(entity: Entity) {
161-
if (entity < reservedStaticEntities) return false
176+
if (isReservedEntity(entity, reservedStaticEntities)) return false
162177

163178
if (usedEntities.has(entity)) {
164179
usedEntities.delete(entity)
@@ -185,6 +200,12 @@ export function createEntityContainer(opts?: { reservedStaticEntities: number })
185200
}
186201

187202
function updateRemovedEntity(entity: Entity) {
203+
// Called for EVERY inbound DELETE_ENTITY, including the renderer's tombstones for
204+
// departed remote players — so this is the door reserved numbers would otherwise enter
205+
// the free list through. They need no tombstone: getEntityState reports them Reserved
206+
// before it consults `removedEntities`, so `Removed` is unreachable for them anyway.
207+
if (isReservedEntity(entity, reservedStaticEntities)) return false
208+
188209
const [n, v] = EntityUtils.fromEntityId(entity)
189210

190211
// Update the removed entities map
@@ -199,6 +220,11 @@ export function createEntityContainer(opts?: { reservedStaticEntities: number })
199220
}
200221

201222
function updateUsedEntity(entity: Entity) {
223+
// Same invariant as updateRemovedEntity. Unreachable from the CRDT path today
224+
// (getEntityState returns `Reserved`, never `Unknown`), but the `v > 0` branch below
225+
// would seed `removedEntities` with a reserved number.
226+
if (isReservedEntity(entity, reservedStaticEntities)) return false
227+
202228
const [n, v] = EntityUtils.fromEntityId(entity)
203229

204230
// if the entity was removed then abort fast
@@ -216,11 +242,14 @@ export function createEntityContainer(opts?: { reservedStaticEntities: number })
216242
}
217243

218244
function getEntityState(entity: Entity): EntityState {
219-
const [n, v] = EntityUtils.fromEntityId(entity)
220-
if (n < reservedStaticEntities) {
245+
// Same guard as the three above, so `Engine.removeEntity` — which classifies via
246+
// getEntityState — cannot disagree with whether this container releases the id.
247+
if (isReservedEntity(entity, reservedStaticEntities)) {
221248
return EntityState.Reserved
222249
}
223250

251+
const [n, v] = EntityUtils.fromEntityId(entity)
252+
224253
if (usedEntities.has(entity)) {
225254
return EntityState.UsedEntity
226255
}

packages/@dcl/ecs/src/engine/index.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { ByteBuffer } from '../serialization/ByteBuffer'
88
import { crdtSceneSystem, OnChangeFunction } from '../systems/crdt'
99
import { ComponentDefinition } from './component'
1010
import { createComponentDefinitionFromSchema } from './lww-element-set-component-definition'
11-
import { Entity, createEntityContainer } from './entity'
11+
import { Entity, EntityState, EntityUtils, createEntityContainer } from './entity'
1212
import { ReadonlyComponentSchema } from './readonly'
1313
import { SystemItem, SystemContainer, SystemFn, SYSTEMS_REGULAR_PRIORITY } from './systems'
1414
import type {
@@ -29,6 +29,9 @@ export * from './readonly'
2929
export * from './types'
3030
export { Entity, ByteBuffer, SystemItem, OnChangeFunction }
3131

32+
/** RootEntity 0, PlayerEntity 1, CameraEntity 2 — see the engineInstance fields below. */
33+
const NAMED_STATIC_ENTITIES = 3
34+
3235
function preEngine(options?: IEngineOptions): PreEngine {
3336
const entityContainer = options?.entityContainer ?? createEntityContainer()
3437
const componentsDefinition = new Map<number, ComponentDefinition<unknown>>()
@@ -49,6 +52,20 @@ function preEngine(options?: IEngineOptions): PreEngine {
4952
return entity
5053
}
5154
function removeEntity(entity: Entity) {
55+
// The renderer streams the avatar range and drops the scene's deletes there, so purging
56+
// locally is permanent: a one-shot component like PlayerIdentityData is never re-sent, and
57+
// the entity is left as a moving Transform with no identity. The three named static
58+
// entities are reserved too, but the renderer DOES apply scene deletes on them — that is
59+
// how InputModifier.deleteFrom(engine.PlayerEntity) clears an input lock — so they must
60+
// still be purged. Asking the container keeps this in step with whether it releases the
61+
// id, including for a custom container with a different reserved range.
62+
const [entityNumber] = EntityUtils.fromEntityId(entity)
63+
const isAvatarEntity =
64+
entityNumber >= NAMED_STATIC_ENTITIES && entityContainer.getEntityState(entity) === EntityState.Reserved
65+
66+
const released = entityContainer.removeEntity(entity)
67+
if (isAvatarEntity) return released
68+
5269
for (const [, component] of componentsDefinition) {
5370
// TODO: hack for the moment.
5471
// We still need the NetworkEntity to forward this message to the SyncTransport.
@@ -57,7 +74,7 @@ function preEngine(options?: IEngineOptions): PreEngine {
5774
component.entityDeleted(entity, true)
5875
}
5976

60-
return entityContainer.removeEntity(entity)
77+
return released
6178
}
6279

6380
function removeEntityWithChildren(entity: Entity) {
@@ -337,7 +354,12 @@ export function Engine(options?: IEngineOptions): IEngine {
337354
PlayerEntity: 1 as Entity,
338355
CameraEntity: 2 as Entity,
339356

340-
getEntityState: partialEngine.entityContainer.getEntityState,
357+
// Delegated, not aliased. A detached `entityContainer.getEntityState` reference binds
358+
// `this` to the engine, so a custom IEntityContainer that uses `this` silently reports the
359+
// wrong state through the public API while the engine itself, which calls the container
360+
// directly, reports the right one. Harmless for the built-in closure-based container, but
361+
// removeEntity now classifies through getEntityState, so the two must never diverge.
362+
getEntityState: (entity: Entity) => partialEngine.entityContainer.getEntityState(entity),
341363
addTransport: crdtSystem.addTransport,
342364

343365
entityContainer: partialEngine.entityContainer

packages/@dcl/ecs/src/engine/types.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,19 @@ export interface IEngine {
9292
* @public
9393
* Remove all components of an entity
9494
* @param entity - entity
95+
* @returns whether the entity id was released for reuse. Ids in the renderer-reserved range
96+
* are never released, at any version. Components are still purged for
97+
* RootEntity/PlayerEntity/CameraEntity, but not for the avatar range.
9598
*/
96-
removeEntity(entity: Entity): void
99+
removeEntity(entity: Entity): boolean
97100

98101
/**
99102
* Remove all components of each entity in the tree made with Transform parenting
100103
* @param entity - the root entity of the tree
104+
*
105+
* May complete only partially and does not report it: nodes in the renderer-reserved range
106+
* are refused by `removeEntity`, so a reserved node anywhere in the tree survives while its
107+
* descendants are removed — leaving its `Transform.parent` pointing at a removed entity.
101108
*/
102109
removeEntityWithChildren(entity: Entity): void
103110

packages/@dcl/playground-assets/etc/playground-assets.api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1512,7 +1512,7 @@ export interface IEngine {
15121512
registerComponentDefinition<T>(componentName: string, componentDefinition: ComponentDefinition<T>): ComponentDefinition<T>;
15131513
// (undocumented)
15141514
removeComponentDefinition(componentId: number | string): void;
1515-
removeEntity(entity: Entity): void;
1515+
removeEntity(entity: Entity): boolean;
15161516
removeEntityWithChildren(entity: Entity): void;
15171517
removeSystem(selector: string | SystemFn): boolean;
15181518
readonly RootEntity: Entity;

0 commit comments

Comments
 (0)