@@ -225,6 +225,15 @@ fn handle_body_monitor_event(
225225// TriggerAreaInstance and TriggerAreaState
226226// ============================================================================
227227
228+ /// Cached info about an avatar inside a trigger area
229+ #[ derive( Debug , Clone ) ]
230+ pub struct AvatarTriggerInfo {
231+ /// Godot instance_id of the avatar's TriggerDetector node
232+ pub instance_id : i64 ,
233+ /// Last known scene ID for this avatar (cached to avoid per-frame metadata queries)
234+ pub last_known_scene : Option < i32 > ,
235+ }
236+
228237/// State for a single trigger area instance
229238#[ derive( Debug ) ]
230239pub struct TriggerAreaInstance {
@@ -235,9 +244,8 @@ pub struct TriggerAreaInstance {
235244 /// Set of entities for which we've sent ENTER event (logical state)
236245 /// An entity can be in entities_inside but not entities_entered if it's not active in the scene
237246 pub entities_entered : HashSet < SceneEntityId > ,
238- /// Instance IDs for avatars inside this trigger area (used to query current scene metadata)
239- /// Maps entity_id -> Godot instance_id of their TriggerDetector node
240- pub avatar_instance_ids : HashMap < SceneEntityId , i64 > ,
247+ /// Info for avatars inside this trigger area (used to track scene changes)
248+ pub avatar_info : HashMap < SceneEntityId , AvatarTriggerInfo > ,
241249 pub mesh_type : TriggerAreaMeshType ,
242250 pub collision_mask : u32 ,
243251}
@@ -353,8 +361,7 @@ fn get_avatar_current_scene(instance_id: i64) -> Option<i32> {
353361}
354362
355363/// Sync entity states: handle entities becoming active/inactive
356- /// This generates synthetic ENTER events for entities that entered physically before becoming active
357- /// and EXIT events for entities that became inactive while inside
364+ /// Uses cached scene info and batches metadata queries for efficiency
358365fn sync_entity_states ( scene : & mut Scene , current_parcel_scene_id : & SceneId ) {
359366 // Only parcel scenes need syncing - global scenes always receive events
360367 if !matches ! ( scene. scene_type, SceneType :: Parcel ) {
@@ -364,147 +371,183 @@ fn sync_entity_states(scene: &mut Scene, current_parcel_scene_id: &SceneId) {
364371 let tick_number = scene. tick_number ;
365372 let scene_pos = scene. godot_dcl_scene . root_node_3d . get_global_position ( ) ;
366373 let this_scene_id = scene. scene_id . 0 ;
367-
368- // Check if local player is active in this scene
369374 let player_active = scene. scene_id == * current_parcel_scene_id;
370375
371- // Collect entities that need state transitions
372- let mut enter_events = Vec :: new ( ) ;
373- let mut exit_events = Vec :: new ( ) ;
376+ // Step 1: Collect all unique avatars that need metadata queries (batch to avoid duplicate queries)
377+ let mut avatars_to_query: HashMap < SceneEntityId , i64 > = HashMap :: new ( ) ;
378+
379+ for ( _trigger_entity, instance) in scene. trigger_areas . instances . iter ( ) {
380+ if ( instance. collision_mask & CL_PLAYER ) == 0 {
381+ continue ;
382+ }
383+
384+ // Avatars inside but not entered - need query if cache says not in this scene
385+ for collider_entity in instance. entities_inside . iter ( ) {
386+ if instance. entities_entered . contains ( collider_entity) {
387+ continue ;
388+ }
389+ if * collider_entity == SceneEntityId :: PLAYER {
390+ continue ;
391+ }
392+ if let Some ( info) = instance. avatar_info . get ( collider_entity) {
393+ if info. last_known_scene != Some ( this_scene_id) {
394+ avatars_to_query. insert ( * collider_entity, info. instance_id ) ;
395+ }
396+ }
397+ }
398+
399+ // Avatars entered - need query if cache says in this scene (to verify still true)
400+ for collider_entity in instance. entities_entered . iter ( ) {
401+ if * collider_entity == SceneEntityId :: PLAYER {
402+ continue ;
403+ }
404+ if let Some ( info) = instance. avatar_info . get ( collider_entity) {
405+ if info. last_known_scene == Some ( this_scene_id) {
406+ avatars_to_query. insert ( * collider_entity, info. instance_id ) ;
407+ }
408+ }
409+ }
410+ }
411+
412+ // Step 2: Batch query all avatar scenes at once
413+ let queried_scenes: HashMap < SceneEntityId , Option < i32 > > = avatars_to_query
414+ . into_iter ( )
415+ . map ( |( entity, instance_id) | ( entity, get_avatar_current_scene ( instance_id) ) )
416+ . collect ( ) ;
417+
418+ // Step 3: Determine state transitions using cached + queried values
419+ struct StateTransition {
420+ trigger_entity : SceneEntityId ,
421+ collider_entity : SceneEntityId ,
422+ is_enter : bool ,
423+ new_scene : Option < i32 > ,
424+ }
425+
426+ let mut transitions = Vec :: new ( ) ;
374427
375428 for ( trigger_entity, instance) in scene. trigger_areas . instances . iter ( ) {
376- // Only check trigger areas that can detect players/avatars
377429 if ( instance. collision_mask & CL_PLAYER ) == 0 {
378430 continue ;
379431 }
380432
381- // Check for entities that need synthetic ENTER ( physically inside but not entered)
433+ // Check avatars physically inside but not yet " entered"
382434 for collider_entity in instance. entities_inside . iter ( ) {
383435 if instance. entities_entered . contains ( collider_entity) {
384- continue ; // Already entered, skip
436+ continue ;
385437 }
386438
387- // Entity is physically inside but hasn't had ENTER sent
388- let is_now_active = if * collider_entity == SceneEntityId :: PLAYER {
389- // Local player: check current_parcel_scene_id
390- player_active
391- } else if let Some ( & instance_id) = instance. avatar_instance_ids . get ( collider_entity) {
392- // Remote avatar: query their current scene from metadata
393- get_avatar_current_scene ( instance_id)
394- . map ( |scene| scene == this_scene_id)
395- . unwrap_or ( false )
439+ let Some ( info) = instance. avatar_info . get ( collider_entity) else {
440+ continue ;
441+ } ;
442+
443+ let ( is_now_active, current_scene) = if * collider_entity == SceneEntityId :: PLAYER {
444+ ( player_active, Some ( current_parcel_scene_id. 0 ) )
396445 } else {
397- false
446+ let cached_in_scene = info. last_known_scene == Some ( this_scene_id) ;
447+ if cached_in_scene {
448+ ( true , info. last_known_scene )
449+ } else {
450+ // Use batched query result
451+ let current = queried_scenes. get ( collider_entity) . copied ( ) . flatten ( ) ;
452+ ( current == Some ( this_scene_id) , current)
453+ }
398454 } ;
399455
400456 if is_now_active {
401- let trigger_transform = scene
402- . godot_dcl_scene
403- . get_node_or_null_3d ( trigger_entity)
404- . map ( |n| n. get_global_transform ( ) )
405- . unwrap_or ( Transform3D :: IDENTITY ) ;
406-
407- let collider_transform = scene
408- . godot_dcl_scene
409- . get_node_or_null_3d ( collider_entity)
410- . map ( |n| n. get_global_transform ( ) )
411- . unwrap_or ( Transform3D :: IDENTITY ) ;
412-
413- enter_events. push ( (
414- * trigger_entity,
415- * collider_entity,
416- trigger_transform,
417- collider_transform,
418- ) ) ;
457+ transitions. push ( StateTransition {
458+ trigger_entity : * trigger_entity,
459+ collider_entity : * collider_entity,
460+ is_enter : true ,
461+ new_scene : current_scene,
462+ } ) ;
419463 }
420464 }
421465
422- // Check for entities that need EXIT ( entered but no longer active)
466+ // Check avatars that " entered" but may no longer be active
423467 for collider_entity in instance. entities_entered . iter ( ) {
424- let is_still_active = if * collider_entity == SceneEntityId :: PLAYER {
425- // Local player: check if they're still in this scene
426- player_active
427- } else if let Some ( & instance_id) = instance. avatar_instance_ids . get ( collider_entity) {
428- // Remote avatar: query their current scene from metadata
429- get_avatar_current_scene ( instance_id)
430- . map ( |scene| scene == this_scene_id)
431- . unwrap_or ( false )
468+ let Some ( info) = instance. avatar_info . get ( collider_entity) else {
469+ transitions. push ( StateTransition {
470+ trigger_entity : * trigger_entity,
471+ collider_entity : * collider_entity,
472+ is_enter : false ,
473+ new_scene : None ,
474+ } ) ;
475+ continue ;
476+ } ;
477+
478+ let ( is_still_active, current_scene) = if * collider_entity == SceneEntityId :: PLAYER {
479+ ( player_active, Some ( current_parcel_scene_id. 0 ) )
432480 } else {
433- // No instance_id tracked - entity might have been removed, treat as inactive
434- false
481+ let cached_in_scene = info. last_known_scene == Some ( this_scene_id) ;
482+ if cached_in_scene {
483+ // Use batched query result
484+ let current = queried_scenes. get ( collider_entity) . copied ( ) . flatten ( ) ;
485+ ( current == Some ( this_scene_id) , current)
486+ } else {
487+ ( false , info. last_known_scene )
488+ }
435489 } ;
436490
437491 if !is_still_active {
438- let trigger_transform = scene
439- . godot_dcl_scene
440- . get_node_or_null_3d ( trigger_entity)
441- . map ( |n| n. get_global_transform ( ) )
442- . unwrap_or ( Transform3D :: IDENTITY ) ;
443-
444- let collider_transform = scene
445- . godot_dcl_scene
446- . get_node_or_null_3d ( collider_entity)
447- . map ( |n| n. get_global_transform ( ) )
448- . unwrap_or ( Transform3D :: IDENTITY ) ;
449-
450- exit_events. push ( (
451- * trigger_entity,
452- * collider_entity,
453- trigger_transform,
454- collider_transform,
455- ) ) ;
492+ transitions. push ( StateTransition {
493+ trigger_entity : * trigger_entity,
494+ collider_entity : * collider_entity,
495+ is_enter : false ,
496+ new_scene : current_scene,
497+ } ) ;
456498 }
457499 }
458500 }
459501
460- // Process synthetic ENTER events
461- for ( trigger_entity, collider_entity, trigger_transform, collider_transform) in enter_events {
462- if let Some ( instance) = scene. trigger_areas . instances . get_mut ( & trigger_entity) {
463- instance. entities_entered . insert ( collider_entity) ;
464-
465- let result = build_trigger_result (
466- & trigger_entity,
467- & collider_entity,
468- TriggerAreaEventType :: TaetEnter ,
469- tick_number,
470- collider_transform,
471- trigger_transform,
472- scene_pos,
473- CL_PLAYER ,
474- ) ;
475- scene. trigger_area_results . push ( ( trigger_entity, result) ) ;
502+ // Step 4: Apply transitions
503+ for t in transitions {
504+ let Some ( instance) = scene. trigger_areas . instances . get_mut ( & t. trigger_entity ) else {
505+ continue ;
506+ } ;
476507
477- tracing:: debug!(
478- "[TriggerArea] SYNC_ENTER trigger={:?}, collider={:?} (entity became active)" ,
479- trigger_entity,
480- collider_entity
481- ) ;
508+ // Update cached scene info
509+ if let Some ( info) = instance. avatar_info . get_mut ( & t. collider_entity ) {
510+ info. last_known_scene = t. new_scene ;
482511 }
483- }
484512
485- // Process EXIT events for entities that became inactive
486- for ( trigger_entity, collider_entity, trigger_transform, collider_transform) in exit_events {
487- if let Some ( instance) = scene. trigger_areas . instances . get_mut ( & trigger_entity) {
488- instance. entities_entered . remove ( & collider_entity) ;
513+ let trigger_transform = scene
514+ . godot_dcl_scene
515+ . get_node_or_null_3d ( & t. trigger_entity )
516+ . map ( |n| n. get_global_transform ( ) )
517+ . unwrap_or ( Transform3D :: IDENTITY ) ;
489518
490- let result = build_trigger_result (
491- & trigger_entity,
492- & collider_entity,
493- TriggerAreaEventType :: TaetExit ,
494- tick_number,
495- collider_transform,
496- trigger_transform,
497- scene_pos,
498- CL_PLAYER ,
499- ) ;
500- scene. trigger_area_results . push ( ( trigger_entity, result) ) ;
519+ let collider_transform = scene
520+ . godot_dcl_scene
521+ . get_node_or_null_3d ( & t. collider_entity )
522+ . map ( |n| n. get_global_transform ( ) )
523+ . unwrap_or ( Transform3D :: IDENTITY ) ;
501524
502- tracing:: debug!(
503- "[TriggerArea] SYNC_EXIT trigger={:?}, collider={:?} (entity became inactive)" ,
504- trigger_entity,
505- collider_entity
506- ) ;
507- }
525+ let event_type = if t. is_enter {
526+ instance. entities_entered . insert ( t. collider_entity ) ;
527+ TriggerAreaEventType :: TaetEnter
528+ } else {
529+ instance. entities_entered . remove ( & t. collider_entity ) ;
530+ TriggerAreaEventType :: TaetExit
531+ } ;
532+
533+ let result = build_trigger_result (
534+ & t. trigger_entity ,
535+ & t. collider_entity ,
536+ event_type,
537+ tick_number,
538+ collider_transform,
539+ trigger_transform,
540+ scene_pos,
541+ CL_PLAYER ,
542+ ) ;
543+ scene. trigger_area_results . push ( ( t. trigger_entity , result) ) ;
544+
545+ tracing:: debug!(
546+ "[TriggerArea] SYNC_{} trigger={:?}, collider={:?}" ,
547+ if t. is_enter { "ENTER" } else { "EXIT" } ,
548+ t. trigger_entity,
549+ t. collider_entity
550+ ) ;
508551 }
509552}
510553
@@ -602,17 +645,32 @@ fn process_callback_events(scene: &mut Scene, current_parcel_scene_id: &SceneId)
602645 // Always update physical state (entities_inside)
603646 if event. is_enter {
604647 instance. entities_inside . insert ( event. collider_entity ) ;
605- // Store instance_id for avatars so we can query their current scene later
648+ // Store avatar info with their current scene (already computed in first pass)
606649 if is_avatar {
607- instance
608- . avatar_instance_ids
609- . insert ( event. collider_entity , event. instance_id ) ;
650+ instance. avatar_info . insert (
651+ event. collider_entity ,
652+ AvatarTriggerInfo {
653+ instance_id : event. instance_id ,
654+ last_known_scene : if event. collider_entity == SceneEntityId :: PLAYER {
655+ // For local player, use current_parcel_scene_id
656+ Some ( current_parcel_scene_id. 0 )
657+ } else {
658+ // For remote avatars, we got this from metadata in first pass
659+ // Reconstruct from entity_active check
660+ if event. entity_active {
661+ Some ( this_scene_id. 0 )
662+ } else {
663+ None // Not in this scene
664+ }
665+ } ,
666+ } ,
667+ ) ;
610668 }
611669 } else {
612670 instance. entities_inside . remove ( & event. collider_entity ) ;
613- // Remove instance_id tracking for avatars
671+ // Remove avatar info tracking
614672 if is_avatar {
615- instance. avatar_instance_ids . remove ( & event. collider_entity ) ;
673+ instance. avatar_info . remove ( & event. collider_entity ) ;
616674 }
617675 // If entity exits physically, also remove from entered state
618676 if instance. entities_entered . remove ( & event. collider_entity ) {
@@ -832,7 +890,7 @@ fn create_or_update_trigger_area(
832890 shape_rid,
833891 entities_inside : HashSet :: new ( ) ,
834892 entities_entered : HashSet :: new ( ) ,
835- avatar_instance_ids : HashMap :: new ( ) ,
893+ avatar_info : HashMap :: new ( ) ,
836894 mesh_type,
837895 collision_mask,
838896 } ,
0 commit comments