@@ -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 }
0 commit comments