< Summary

Class:DCL.Controllers.SceneBoundsChecker
Assembly:DCL.Runtime
File(s):/tmp/workspace/unity-renderer/unity-renderer/Assets/Scripts/MainScripts/DCL/WorldRuntime/SceneBoundariesController/SceneBoundsChecker.cs
Covered lines:165
Uncovered lines:22
Coverable lines:187
Total lines:413
Line coverage:88.2% (165 of 187)
Covered branches:0
Total branches:0
Covered methods:32
Total methods:34
Method coverage:94.1% (32 of 34)

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity NPath complexity Sequence coverage
SceneBoundsChecker(...)0%220100%
Initialize()0%110100%
SetFeedbackStyle(...)0%110100%
GetFeedbackStyle()0%110100%
GetOriginalMaterials(...)0%2100%
CheckEntitiesAsync()0%10.0110095%
IsTimeBudgetDepleted(...)0%330100%
Restart()0%110100%
Start()0%2.012085.71%
Stop()0%220100%
Dispose()0%110100%
AddEntityToBeChecked(...)0%770100%
RunPreliminaryEvaluationAsync()0%550100%
AddEntity(...)0%220100%
RemoveEntity(...)0%550100%
WasAddedAsPersistent(...)0%110100%
RunEntityEvaluation(...)0%9.049092.31%
EvaluateMeshBounds(...)0%550100%
EvaluateEntityPosition(...)0%330100%
EvaluateAvatarMeshBounds(...)0%12300%
SetEntityInsideBoundariesState(...)0%220100%
HasMesh(...)0%440100%
IsEntityMeshInsideSceneBoundaries(...)0%5.125083.33%
AreSubMeshesInsideBoundaries(...)0%4.034087.5%
AreCollidersInsideBoundaries(...)0%4.034087.5%
SetMeshesAndComponentsInsideBoundariesState(...)0%110100%
SetEntityMeshesInsideBoundariesState(...)0%110100%
SetEntityCollidersInsideBoundariesState(...)0%770100%
SetComponentsInsideBoundariesValidState(...)0%440100%

File(s)

/tmp/workspace/unity-renderer/unity-renderer/Assets/Scripts/MainScripts/DCL/WorldRuntime/SceneBoundariesController/SceneBoundsChecker.cs

#LineLine coverage
 1using Cysharp.Threading.Tasks;
 2using DCL.Models;
 3using System;
 4using System.Collections.Generic;
 5using System.Threading;
 6using UnityEngine;
 7
 8namespace DCL.Controllers
 9{
 10    public class SceneBoundsChecker : ISceneBoundsChecker
 11    {
 12        private const float NERFED_TIME_BUDGET = 0.5f / 1000f;
 13        private const float RECHECK_BUDGET = 0.5f;
 150114        public bool enabled => isEnabled;
 295815        public float timeBetweenChecks { get; set; } = RECHECK_BUDGET;
 216        public int entitiesToCheckCount => entitiesToCheck.Count;
 17
 18        private const bool VERBOSE = false;
 42619        private Logger logger = new ("SceneBoundsChecker") { verboseEnabled = VERBOSE };
 42620        private HashSet<IDCLEntity> entitiesToCheck = new ();
 42621        private HashSet<IDCLEntity> checkedEntities = new ();
 42622        private HashSet<IDCLEntity> persistentEntities = new ();
 23        private ISceneBoundsFeedbackStyle feedbackStyle;
 24        private float lastCheckTime;
 25
 26        private Service<IMessagingControllersManager> messagingManagerService;
 45527        private IMessagingControllersManager messagingManager => messagingManagerService.Ref;
 28
 29        private bool isNerfed;
 30        private bool isInsistent;
 31        private bool isEnabled;
 42632        private CancellationTokenSource cancellationTokenSource = new ();
 33
 34        public void Initialize()
 35        {
 42636            Start();
 42637        }
 38
 42639        public SceneBoundsChecker(ISceneBoundsFeedbackStyle feedbackStyle = null)
 40        {
 42641            this.feedbackStyle = feedbackStyle ?? new SceneBoundsFeedbackStyle_Simple();
 42642            FeatureFlag featureFlag = DataStore.i.featureFlags.flags.Get();
 42643            isNerfed = featureFlag.IsFeatureEnabled("NERF_SBC");
 42644            isInsistent = featureFlag.IsFeatureEnabled("INSISTENT_SBC");
 42645        }
 46
 47        public void SetFeedbackStyle(ISceneBoundsFeedbackStyle feedbackStyle)
 48        {
 4749            this.feedbackStyle.CleanFeedback();
 4750            this.feedbackStyle = feedbackStyle;
 4751            Restart();
 4752        }
 53
 54        public ISceneBoundsFeedbackStyle GetFeedbackStyle() =>
 7755            feedbackStyle;
 56
 57        public List<Material> GetOriginalMaterials(MeshesInfo meshesInfo) =>
 058            feedbackStyle.GetOriginalMaterials(meshesInfo);
 59
 60        private async UniTask CheckEntitiesAsync(CancellationToken cancellationToken)
 61        {
 62            try
 63            {
 7364                while (true)
 65                {
 296666                    cancellationToken.ThrowIfCancellationRequested();
 67
 747968                    await UniTask.WaitForFixedUpdate();
 69
 70                    // Kinerius: Since the nerf can skip the process a lot faster than before, we need faster rechecks
 249371                    var finalTimeBetweenChecks = isNerfed ? NERFED_TIME_BUDGET : timeBetweenChecks;
 72
 249373                    float elapsedTime = Time.realtimeSinceStartup - lastCheckTime;
 74
 249375                    if ((entitiesToCheck.Count > 0) && (finalTimeBetweenChecks <= 0f || elapsedTime >= finalTimeBetweenC
 76                    {
 7377                        var timeBudget = NERFED_TIME_BUDGET;
 78
 79                        void ProcessEntitiesList(HashSet<IDCLEntity> entities)
 80                        {
 7381                            if (IsTimeBudgetDepleted(timeBudget))
 82                            {
 83                                if (VERBOSE)
 84                                    logger.Verbose("Time budget reached, escaping entities processing until next iterati
 85
 086                                return;
 87                            }
 88
 7389                            using HashSet<IDCLEntity>.Enumerator iterator = entities.GetEnumerator();
 90
 15191                            while (iterator.MoveNext())
 92                            {
 7893                                if (IsTimeBudgetDepleted(timeBudget))
 94                                {
 095                                    if (VERBOSE)
 96                                        logger.Verbose("Time budget reached, escaping entities processing until next ite
 97
 98                                    return;
 99                                }
 100
 78101                                float startTime = Time.realtimeSinceStartup;
 102
 78103                                RunEntityEvaluation(iterator.Current, false);
 78104                                checkedEntities.Add(iterator.Current);
 105
 78106                                float finishTime = Time.realtimeSinceStartup;
 78107                                float usedTimeBudget = finishTime - startTime;
 108
 78109                                if (!isNerfed)
 110                                {
 78111                                    if (messagingManager != null)
 77112                                        messagingManager.timeBudgetCounter -= usedTimeBudget;
 113                                }
 114
 78115                                timeBudget -= usedTimeBudget;
 116                            }
 146117                        }
 118
 73119                        ProcessEntitiesList(entitiesToCheck);
 120
 121                        // As we can't modify the hashset while traversing it, we keep track of the entities that should
 73122                        using (var iterator = checkedEntities.GetEnumerator())
 123                        {
 229124                            while (iterator.MoveNext()) { RemoveEntity(iterator.Current, removeIfPersistent: false, rese
 73125                        }
 126
 127                        if (VERBOSE)
 128                            logger.Verbose($"Finished checking entities: checked entities {checkedEntities.Count}; entit
 129
 73130                        checkedEntities.Clear();
 131
 73132                        lastCheckTime = Time.realtimeSinceStartup;
 133                    }
 134                }
 135            }
 136            catch (Exception e)
 137            {
 473138                if (e is not OperationCanceledException)
 0139                    throw;
 473140            }
 473141        }
 142
 143        private bool IsTimeBudgetDepleted(float timeBudget) =>
 151144            isNerfed ? timeBudget <= 0f : messagingManager != null && messagingManager.timeBudgetCounter <= 0f;
 145
 146        private void Restart()
 147        {
 47148            Stop();
 47149            Start();
 47150        }
 151
 152        public void Start()
 153        {
 473154            if (isEnabled)
 0155                return;
 156
 473157            cancellationTokenSource = new CancellationTokenSource();
 473158            lastCheckTime = Time.realtimeSinceStartup;
 473159            isEnabled = true;
 473160            CheckEntitiesAsync(cancellationTokenSource.Token).Forget();
 473161        }
 162
 163        public void Stop()
 164        {
 494165            if (!isEnabled)
 21166                return;
 167
 473168            cancellationTokenSource.Cancel();
 473169            cancellationTokenSource.Dispose();
 170
 473171            isEnabled = false;
 473172        }
 173
 174        public void Dispose()
 175        {
 426176            Stop();
 426177        }
 178
 179        public void AddEntityToBeChecked(IDCLEntity entity, bool isPersistent = false, bool runPreliminaryEvaluation = f
 180        {
 857181            IParcelScene entityScene = entity.scene;
 182
 183            // Entities from global or sdk7 scenes should not be added to this boundaries checker system
 857184            bool isInvalidEntity = entityScene != null && (entityScene.isPersistent || entityScene.sceneData.sdk7);
 185
 857186            if (!enabled || isInvalidEntity)
 609187                return;
 188
 248189            if (runPreliminaryEvaluation)
 183190                RunPreliminaryEvaluationAsync(entity, isPersistent).Forget();
 191            else
 65192                AddEntity(entity, isPersistent);
 65193        }
 194
 195        private async UniTask RunPreliminaryEvaluationAsync(IDCLEntity entity, bool isPersistent)
 196        {
 549197            await UniTask.WaitForFixedUpdate();
 198
 199            // The outer bounds check is cheaper than the regular check
 183200            RunEntityEvaluation(entity, onlyOuterBoundsCheck: true);
 201
 202            // No need to add the entity to be checked later if we already found it outside scene outer boundaries.
 203            // When the correct events are triggered again, the entity will be checked again.
 183204            if (isInsistent || isPersistent || entity.isInsideSceneOuterBoundaries)
 90205                AddEntity(entity, isPersistent);
 183206        }
 207
 208        private void AddEntity(IDCLEntity entity, bool isPersistent)
 209        {
 155210            entitiesToCheck.Add(entity);
 211
 155212            if (isPersistent)
 2213                persistentEntities.Add(entity);
 155214        }
 215
 216        public void RemoveEntity(IDCLEntity entity, bool removeIfPersistent = false, bool resetState = false)
 217        {
 236218            if (!enabled || (!removeIfPersistent && persistentEntities.Contains(entity)))
 3219                return;
 220
 233221            entitiesToCheck.Remove(entity);
 233222            persistentEntities.Remove(entity);
 223
 233224            if (resetState)
 156225                SetMeshesAndComponentsInsideBoundariesState(entity, true);
 233226        }
 227
 228        public bool WasAddedAsPersistent(IDCLEntity entity) =>
 7229            persistentEntities.Contains(entity);
 230
 231        public void RunEntityEvaluation(IDCLEntity entity, bool onlyOuterBoundsCheck)
 232        {
 274233            if (entity == null || entity.gameObject == null || entity.scene == null || entity.scene.isPersistent)
 6234                return;
 235
 236            // Recursively evaluate entity children as well, we need to check this up front because this entity may not 
 268237            if (entity.children.Count > 0)
 238            {
 11239                using (var iterator = entity.children.GetEnumerator())
 240                {
 33241                    while (iterator.MoveNext()) { RunEntityEvaluation(iterator.Current.Value, onlyOuterBoundsCheck); }
 11242                }
 243            }
 244
 268245            if (HasMesh(entity)) // If it has a mesh we don't evaluate its position due to artists "pivot point sloppine
 205246                EvaluateMeshBounds(entity, onlyOuterBoundsCheck);
 63247            else if (entity.scene.componentsManagerLegacy.HasComponent(entity, CLASS_ID_COMPONENT.AVATAR_SHAPE)) // Avat
 0248                EvaluateAvatarMeshBounds(entity, onlyOuterBoundsCheck);
 249            else
 63250                EvaluateEntityPosition(entity, onlyOuterBoundsCheck);
 63251        }
 252
 253        private void EvaluateMeshBounds(IDCLEntity entity, bool onlyOuterBoundsCheck = false)
 254        {
 205255            var loadWrapper = Environment.i.world.state.GetLoaderForEntity(entity);
 256
 205257            if (loadWrapper != null && !loadWrapper.alreadyLoaded)
 6258                return;
 259
 199260            entity.UpdateOuterBoundariesStatus(entity.scene.IsInsideSceneOuterBoundaries(entity.meshesInfo.mergedBounds)
 261
 199262            if (!entity.isInsideSceneOuterBoundaries)
 94263                SetMeshesAndComponentsInsideBoundariesState(entity, false);
 264
 199265            if (onlyOuterBoundsCheck)
 127266                return;
 267
 72268            SetMeshesAndComponentsInsideBoundariesState(entity, IsEntityMeshInsideSceneBoundaries(entity));
 72269        }
 270
 271        private void EvaluateEntityPosition(IDCLEntity entity, bool onlyOuterBoundsCheck = false)
 272        {
 63273            Vector3 entityGOPosition = entity.gameObject.transform.position;
 63274            entity.UpdateOuterBoundariesStatus(entity.scene.IsInsideSceneOuterBoundaries(entityGOPosition));
 275
 63276            if (!entity.isInsideSceneOuterBoundaries)
 277            {
 31278                SetComponentsInsideBoundariesValidState(entity, false);
 31279                SetEntityInsideBoundariesState(entity, false);
 280            }
 281
 63282            if (onlyOuterBoundsCheck)
 57283                return;
 284
 6285            bool isInsideBoundaries = entity.scene.IsInsideSceneBoundaries(entityGOPosition + CommonScriptableObjects.wo
 6286            SetComponentsInsideBoundariesValidState(entity, isInsideBoundaries);
 6287            SetEntityInsideBoundariesState(entity, isInsideBoundaries);
 6288        }
 289
 290        private void EvaluateAvatarMeshBounds(IDCLEntity entity, bool onlyOuterBoundsCheck = false)
 291        {
 0292            Vector3 entityGOPosition = entity.gameObject.transform.position;
 293
 294            // Heuristic using the entity scale for the size of the avatar bounds, otherwise we should configure the
 295            // entity's meshRootGameObject, etc. after its GPU skinning runs and use the regular entity mesh evaluation
 0296            Bounds avatarBounds = new Bounds();
 0297            avatarBounds.center = entityGOPosition;
 0298            avatarBounds.size = entity.gameObject.transform.lossyScale;
 299
 0300            entity.UpdateOuterBoundariesStatus(entity.scene.IsInsideSceneOuterBoundaries(avatarBounds));
 301
 0302            if (!entity.isInsideSceneOuterBoundaries)
 303            {
 0304                SetComponentsInsideBoundariesValidState(entity, false);
 0305                SetEntityInsideBoundariesState(entity, false);
 306            }
 307
 0308            if (onlyOuterBoundsCheck)
 0309                return;
 310
 0311            bool isInsideBoundaries = entity.scene.IsInsideSceneBoundaries(avatarBounds);
 0312            SetComponentsInsideBoundariesValidState(entity, isInsideBoundaries);
 0313            SetEntityInsideBoundariesState(entity, isInsideBoundaries);
 0314        }
 315
 316        private void SetEntityInsideBoundariesState(IDCLEntity entity, bool isInsideBoundaries)
 317        {
 359318            if (entity.isInsideSceneBoundaries == isInsideBoundaries)
 261319                return;
 320
 98321            entity.UpdateInsideBoundariesStatus(isInsideBoundaries);
 98322        }
 323
 324        private bool HasMesh(IDCLEntity entity) =>
 268325            entity.meshRootGameObject != null
 326            && (entity.meshesInfo.colliders.Count > 0
 327                || (entity.meshesInfo.renderers != null
 328                    && entity.meshesInfo.renderers.Length > 0));
 329
 330        public bool IsEntityMeshInsideSceneBoundaries(IDCLEntity entity)
 331        {
 72332            if (entity.meshesInfo == null
 333                || entity.meshesInfo.meshRootGameObject == null
 334                || entity.meshesInfo.mergedBounds == null)
 0335                return false;
 336
 337            // 1st check (full mesh AABB)
 72338            bool isInsideBoundaries = entity.scene.IsInsideSceneBoundaries(entity.meshesInfo.mergedBounds);
 339
 340            // 2nd check (submeshes & colliders AABB)
 103341            if (!isInsideBoundaries) { isInsideBoundaries = AreSubMeshesInsideBoundaries(entity) && AreCollidersInsideBo
 342
 72343            return isInsideBoundaries;
 344        }
 345
 346        private static bool AreSubMeshesInsideBoundaries(IDCLEntity entity)
 347        {
 62348            for (int i = 0; i < entity.meshesInfo.renderers.Length; i++)
 349            {
 29350                Renderer renderer = entity.meshesInfo.renderers[i];
 351
 29352                if (renderer == null)
 353                    continue;
 354
 29355                if (!entity.scene.IsInsideSceneBoundaries(MeshesInfoUtils.GetSafeBounds(renderer.bounds, renderer.transf
 29356                    return false;
 357            }
 358
 2359            return true;
 360        }
 361
 362        private bool AreCollidersInsideBoundaries(IDCLEntity entity)
 363        {
 6364            foreach (Collider collider in entity.meshesInfo.colliders)
 365            {
 2366                if (collider == null)
 367                    continue;
 368
 2369                if (!entity.scene.IsInsideSceneBoundaries(MeshesInfoUtils.GetSafeBounds(collider.bounds, collider.transf
 2370                    return false;
 371            }
 372
 0373            return true;
 2374        }
 375
 376        private void SetMeshesAndComponentsInsideBoundariesState(IDCLEntity entity, bool isInsideBoundaries)
 377        {
 322378            SetEntityMeshesInsideBoundariesState(entity.meshesInfo, isInsideBoundaries);
 322379            SetEntityCollidersInsideBoundariesState(entity.meshesInfo, isInsideBoundaries);
 322380            SetComponentsInsideBoundariesValidState(entity, isInsideBoundaries);
 381
 382            // Should always be set last as entity.isInsideSceneBoundaries is checked to avoid re-running code unnecessa
 322383            SetEntityInsideBoundariesState(entity, isInsideBoundaries);
 322384        }
 385
 386        private void SetEntityMeshesInsideBoundariesState(MeshesInfo meshesInfo, bool isInsideBoundaries)
 387        {
 322388            feedbackStyle.ApplyFeedback(meshesInfo, isInsideBoundaries);
 322389        }
 390
 391        private void SetEntityCollidersInsideBoundariesState(MeshesInfo meshesInfo, bool isInsideBoundaries)
 392        {
 322393            if (meshesInfo == null || meshesInfo.colliders.Count == 0 || !meshesInfo.currentShape.HasCollisions())
 85394                return;
 395
 1050396            foreach (Collider collider in meshesInfo.colliders)
 397            {
 288398                if (collider == null) continue;
 399
 288400                if (collider.enabled != isInsideBoundaries)
 116401                    collider.enabled = isInsideBoundaries;
 402            }
 237403        }
 404
 405        private void SetComponentsInsideBoundariesValidState(IDCLEntity entity, bool isInsideBoundaries)
 406        {
 359407            if (entity.isInsideSceneBoundaries == isInsideBoundaries || !DataStore.i.sceneBoundariesChecker.componentsCh
 357408                return;
 409
 10410            foreach (IOutOfSceneBoundariesHandler component in DataStore.i.sceneBoundariesChecker.componentsCheckSceneBo
 2411        }
 412    }
 413}

Methods/Properties

enabled()
timeBetweenChecks()
timeBetweenChecks(System.Single)
SceneBoundsChecker(DCL.Controllers.ISceneBoundsFeedbackStyle)
entitiesToCheckCount()
messagingManager()
Initialize()
SetFeedbackStyle(DCL.Controllers.ISceneBoundsFeedbackStyle)
GetFeedbackStyle()
GetOriginalMaterials(DCL.Models.MeshesInfo)
CheckEntitiesAsync()
IsTimeBudgetDepleted(System.Single)
Restart()
Start()
Stop()
Dispose()
AddEntityToBeChecked(DCL.Models.IDCLEntity, System.Boolean, System.Boolean)
RunPreliminaryEvaluationAsync()
AddEntity(DCL.Models.IDCLEntity, System.Boolean)
RemoveEntity(DCL.Models.IDCLEntity, System.Boolean, System.Boolean)
WasAddedAsPersistent(DCL.Models.IDCLEntity)
RunEntityEvaluation(DCL.Models.IDCLEntity, System.Boolean)
EvaluateMeshBounds(DCL.Models.IDCLEntity, System.Boolean)
EvaluateEntityPosition(DCL.Models.IDCLEntity, System.Boolean)
EvaluateAvatarMeshBounds(DCL.Models.IDCLEntity, System.Boolean)
SetEntityInsideBoundariesState(DCL.Models.IDCLEntity, System.Boolean)
HasMesh(DCL.Models.IDCLEntity)
IsEntityMeshInsideSceneBoundaries(DCL.Models.IDCLEntity)
AreSubMeshesInsideBoundaries(DCL.Models.IDCLEntity)
AreCollidersInsideBoundaries(DCL.Models.IDCLEntity)
SetMeshesAndComponentsInsideBoundariesState(DCL.Models.IDCLEntity, System.Boolean)
SetEntityMeshesInsideBoundariesState(DCL.Models.MeshesInfo, System.Boolean)
SetEntityCollidersInsideBoundariesState(DCL.Models.MeshesInfo, System.Boolean)
SetComponentsInsideBoundariesValidState(DCL.Models.IDCLEntity, System.Boolean)