| | 1 | | using Cysharp.Threading.Tasks; |
| | 2 | | using DCL.Models; |
| | 3 | | using System; |
| | 4 | | using System.Collections.Generic; |
| | 5 | | using System.Threading; |
| | 6 | | using UnityEngine; |
| | 7 | |
|
| | 8 | | namespace 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; |
| 1501 | 14 | | public bool enabled => isEnabled; |
| 2958 | 15 | | public float timeBetweenChecks { get; set; } = RECHECK_BUDGET; |
| 2 | 16 | | public int entitiesToCheckCount => entitiesToCheck.Count; |
| | 17 | |
|
| | 18 | | private const bool VERBOSE = false; |
| 426 | 19 | | private Logger logger = new ("SceneBoundsChecker") { verboseEnabled = VERBOSE }; |
| 426 | 20 | | private HashSet<IDCLEntity> entitiesToCheck = new (); |
| 426 | 21 | | private HashSet<IDCLEntity> checkedEntities = new (); |
| 426 | 22 | | private HashSet<IDCLEntity> persistentEntities = new (); |
| | 23 | | private ISceneBoundsFeedbackStyle feedbackStyle; |
| | 24 | | private float lastCheckTime; |
| | 25 | |
|
| | 26 | | private Service<IMessagingControllersManager> messagingManagerService; |
| 455 | 27 | | private IMessagingControllersManager messagingManager => messagingManagerService.Ref; |
| | 28 | |
|
| | 29 | | private bool isNerfed; |
| | 30 | | private bool isInsistent; |
| | 31 | | private bool isEnabled; |
| 426 | 32 | | private CancellationTokenSource cancellationTokenSource = new (); |
| | 33 | |
|
| | 34 | | public void Initialize() |
| | 35 | | { |
| 426 | 36 | | Start(); |
| 426 | 37 | | } |
| | 38 | |
|
| 426 | 39 | | public SceneBoundsChecker(ISceneBoundsFeedbackStyle feedbackStyle = null) |
| | 40 | | { |
| 426 | 41 | | this.feedbackStyle = feedbackStyle ?? new SceneBoundsFeedbackStyle_Simple(); |
| 426 | 42 | | FeatureFlag featureFlag = DataStore.i.featureFlags.flags.Get(); |
| 426 | 43 | | isNerfed = featureFlag.IsFeatureEnabled("NERF_SBC"); |
| 426 | 44 | | isInsistent = featureFlag.IsFeatureEnabled("INSISTENT_SBC"); |
| 426 | 45 | | } |
| | 46 | |
|
| | 47 | | public void SetFeedbackStyle(ISceneBoundsFeedbackStyle feedbackStyle) |
| | 48 | | { |
| 47 | 49 | | this.feedbackStyle.CleanFeedback(); |
| 47 | 50 | | this.feedbackStyle = feedbackStyle; |
| 47 | 51 | | Restart(); |
| 47 | 52 | | } |
| | 53 | |
|
| | 54 | | public ISceneBoundsFeedbackStyle GetFeedbackStyle() => |
| 77 | 55 | | feedbackStyle; |
| | 56 | |
|
| | 57 | | public List<Material> GetOriginalMaterials(MeshesInfo meshesInfo) => |
| 0 | 58 | | feedbackStyle.GetOriginalMaterials(meshesInfo); |
| | 59 | |
|
| | 60 | | private async UniTask CheckEntitiesAsync(CancellationToken cancellationToken) |
| | 61 | | { |
| | 62 | | try |
| | 63 | | { |
| 73 | 64 | | while (true) |
| | 65 | | { |
| 2966 | 66 | | cancellationToken.ThrowIfCancellationRequested(); |
| | 67 | |
|
| 7479 | 68 | | await UniTask.WaitForFixedUpdate(); |
| | 69 | |
|
| | 70 | | // Kinerius: Since the nerf can skip the process a lot faster than before, we need faster rechecks |
| 2493 | 71 | | var finalTimeBetweenChecks = isNerfed ? NERFED_TIME_BUDGET : timeBetweenChecks; |
| | 72 | |
|
| 2493 | 73 | | float elapsedTime = Time.realtimeSinceStartup - lastCheckTime; |
| | 74 | |
|
| 2493 | 75 | | if ((entitiesToCheck.Count > 0) && (finalTimeBetweenChecks <= 0f || elapsedTime >= finalTimeBetweenC |
| | 76 | | { |
| 73 | 77 | | var timeBudget = NERFED_TIME_BUDGET; |
| | 78 | |
|
| | 79 | | void ProcessEntitiesList(HashSet<IDCLEntity> entities) |
| | 80 | | { |
| 73 | 81 | | if (IsTimeBudgetDepleted(timeBudget)) |
| | 82 | | { |
| | 83 | | if (VERBOSE) |
| | 84 | | logger.Verbose("Time budget reached, escaping entities processing until next iterati |
| | 85 | |
|
| 0 | 86 | | return; |
| | 87 | | } |
| | 88 | |
|
| 73 | 89 | | using HashSet<IDCLEntity>.Enumerator iterator = entities.GetEnumerator(); |
| | 90 | |
|
| 151 | 91 | | while (iterator.MoveNext()) |
| | 92 | | { |
| 78 | 93 | | if (IsTimeBudgetDepleted(timeBudget)) |
| | 94 | | { |
| 0 | 95 | | if (VERBOSE) |
| | 96 | | logger.Verbose("Time budget reached, escaping entities processing until next ite |
| | 97 | |
|
| | 98 | | return; |
| | 99 | | } |
| | 100 | |
|
| 78 | 101 | | float startTime = Time.realtimeSinceStartup; |
| | 102 | |
|
| 78 | 103 | | RunEntityEvaluation(iterator.Current, false); |
| 78 | 104 | | checkedEntities.Add(iterator.Current); |
| | 105 | |
|
| 78 | 106 | | float finishTime = Time.realtimeSinceStartup; |
| 78 | 107 | | float usedTimeBudget = finishTime - startTime; |
| | 108 | |
|
| 78 | 109 | | if (!isNerfed) |
| | 110 | | { |
| 78 | 111 | | if (messagingManager != null) |
| 77 | 112 | | messagingManager.timeBudgetCounter -= usedTimeBudget; |
| | 113 | | } |
| | 114 | |
|
| 78 | 115 | | timeBudget -= usedTimeBudget; |
| | 116 | | } |
| 146 | 117 | | } |
| | 118 | |
|
| 73 | 119 | | ProcessEntitiesList(entitiesToCheck); |
| | 120 | |
|
| | 121 | | // As we can't modify the hashset while traversing it, we keep track of the entities that should |
| 73 | 122 | | using (var iterator = checkedEntities.GetEnumerator()) |
| | 123 | | { |
| 229 | 124 | | while (iterator.MoveNext()) { RemoveEntity(iterator.Current, removeIfPersistent: false, rese |
| 73 | 125 | | } |
| | 126 | |
|
| | 127 | | if (VERBOSE) |
| | 128 | | logger.Verbose($"Finished checking entities: checked entities {checkedEntities.Count}; entit |
| | 129 | |
|
| 73 | 130 | | checkedEntities.Clear(); |
| | 131 | |
|
| 73 | 132 | | lastCheckTime = Time.realtimeSinceStartup; |
| | 133 | | } |
| | 134 | | } |
| | 135 | | } |
| | 136 | | catch (Exception e) |
| | 137 | | { |
| 473 | 138 | | if (e is not OperationCanceledException) |
| 0 | 139 | | throw; |
| 473 | 140 | | } |
| 473 | 141 | | } |
| | 142 | |
|
| | 143 | | private bool IsTimeBudgetDepleted(float timeBudget) => |
| 151 | 144 | | isNerfed ? timeBudget <= 0f : messagingManager != null && messagingManager.timeBudgetCounter <= 0f; |
| | 145 | |
|
| | 146 | | private void Restart() |
| | 147 | | { |
| 47 | 148 | | Stop(); |
| 47 | 149 | | Start(); |
| 47 | 150 | | } |
| | 151 | |
|
| | 152 | | public void Start() |
| | 153 | | { |
| 473 | 154 | | if (isEnabled) |
| 0 | 155 | | return; |
| | 156 | |
|
| 473 | 157 | | cancellationTokenSource = new CancellationTokenSource(); |
| 473 | 158 | | lastCheckTime = Time.realtimeSinceStartup; |
| 473 | 159 | | isEnabled = true; |
| 473 | 160 | | CheckEntitiesAsync(cancellationTokenSource.Token).Forget(); |
| 473 | 161 | | } |
| | 162 | |
|
| | 163 | | public void Stop() |
| | 164 | | { |
| 494 | 165 | | if (!isEnabled) |
| 21 | 166 | | return; |
| | 167 | |
|
| 473 | 168 | | cancellationTokenSource.Cancel(); |
| 473 | 169 | | cancellationTokenSource.Dispose(); |
| | 170 | |
|
| 473 | 171 | | isEnabled = false; |
| 473 | 172 | | } |
| | 173 | |
|
| | 174 | | public void Dispose() |
| | 175 | | { |
| 426 | 176 | | Stop(); |
| 426 | 177 | | } |
| | 178 | |
|
| | 179 | | public void AddEntityToBeChecked(IDCLEntity entity, bool isPersistent = false, bool runPreliminaryEvaluation = f |
| | 180 | | { |
| 857 | 181 | | IParcelScene entityScene = entity.scene; |
| | 182 | |
|
| | 183 | | // Entities from global or sdk7 scenes should not be added to this boundaries checker system |
| 857 | 184 | | bool isInvalidEntity = entityScene != null && (entityScene.isPersistent || entityScene.sceneData.sdk7); |
| | 185 | |
|
| 857 | 186 | | if (!enabled || isInvalidEntity) |
| 609 | 187 | | return; |
| | 188 | |
|
| 248 | 189 | | if (runPreliminaryEvaluation) |
| 183 | 190 | | RunPreliminaryEvaluationAsync(entity, isPersistent).Forget(); |
| | 191 | | else |
| 65 | 192 | | AddEntity(entity, isPersistent); |
| 65 | 193 | | } |
| | 194 | |
|
| | 195 | | private async UniTask RunPreliminaryEvaluationAsync(IDCLEntity entity, bool isPersistent) |
| | 196 | | { |
| 549 | 197 | | await UniTask.WaitForFixedUpdate(); |
| | 198 | |
|
| | 199 | | // The outer bounds check is cheaper than the regular check |
| 183 | 200 | | 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. |
| 183 | 204 | | if (isInsistent || isPersistent || entity.isInsideSceneOuterBoundaries) |
| 90 | 205 | | AddEntity(entity, isPersistent); |
| 183 | 206 | | } |
| | 207 | |
|
| | 208 | | private void AddEntity(IDCLEntity entity, bool isPersistent) |
| | 209 | | { |
| 155 | 210 | | entitiesToCheck.Add(entity); |
| | 211 | |
|
| 155 | 212 | | if (isPersistent) |
| 2 | 213 | | persistentEntities.Add(entity); |
| 155 | 214 | | } |
| | 215 | |
|
| | 216 | | public void RemoveEntity(IDCLEntity entity, bool removeIfPersistent = false, bool resetState = false) |
| | 217 | | { |
| 236 | 218 | | if (!enabled || (!removeIfPersistent && persistentEntities.Contains(entity))) |
| 3 | 219 | | return; |
| | 220 | |
|
| 233 | 221 | | entitiesToCheck.Remove(entity); |
| 233 | 222 | | persistentEntities.Remove(entity); |
| | 223 | |
|
| 233 | 224 | | if (resetState) |
| 156 | 225 | | SetMeshesAndComponentsInsideBoundariesState(entity, true); |
| 233 | 226 | | } |
| | 227 | |
|
| | 228 | | public bool WasAddedAsPersistent(IDCLEntity entity) => |
| 7 | 229 | | persistentEntities.Contains(entity); |
| | 230 | |
|
| | 231 | | public void RunEntityEvaluation(IDCLEntity entity, bool onlyOuterBoundsCheck) |
| | 232 | | { |
| 274 | 233 | | if (entity == null || entity.gameObject == null || entity.scene == null || entity.scene.isPersistent) |
| 6 | 234 | | return; |
| | 235 | |
|
| | 236 | | // Recursively evaluate entity children as well, we need to check this up front because this entity may not |
| 268 | 237 | | if (entity.children.Count > 0) |
| | 238 | | { |
| 11 | 239 | | using (var iterator = entity.children.GetEnumerator()) |
| | 240 | | { |
| 33 | 241 | | while (iterator.MoveNext()) { RunEntityEvaluation(iterator.Current.Value, onlyOuterBoundsCheck); } |
| 11 | 242 | | } |
| | 243 | | } |
| | 244 | |
|
| 268 | 245 | | if (HasMesh(entity)) // If it has a mesh we don't evaluate its position due to artists "pivot point sloppine |
| 205 | 246 | | EvaluateMeshBounds(entity, onlyOuterBoundsCheck); |
| 63 | 247 | | else if (entity.scene.componentsManagerLegacy.HasComponent(entity, CLASS_ID_COMPONENT.AVATAR_SHAPE)) // Avat |
| 0 | 248 | | EvaluateAvatarMeshBounds(entity, onlyOuterBoundsCheck); |
| | 249 | | else |
| 63 | 250 | | EvaluateEntityPosition(entity, onlyOuterBoundsCheck); |
| 63 | 251 | | } |
| | 252 | |
|
| | 253 | | private void EvaluateMeshBounds(IDCLEntity entity, bool onlyOuterBoundsCheck = false) |
| | 254 | | { |
| 205 | 255 | | var loadWrapper = Environment.i.world.state.GetLoaderForEntity(entity); |
| | 256 | |
|
| 205 | 257 | | if (loadWrapper != null && !loadWrapper.alreadyLoaded) |
| 6 | 258 | | return; |
| | 259 | |
|
| 199 | 260 | | entity.UpdateOuterBoundariesStatus(entity.scene.IsInsideSceneOuterBoundaries(entity.meshesInfo.mergedBounds) |
| | 261 | |
|
| 199 | 262 | | if (!entity.isInsideSceneOuterBoundaries) |
| 94 | 263 | | SetMeshesAndComponentsInsideBoundariesState(entity, false); |
| | 264 | |
|
| 199 | 265 | | if (onlyOuterBoundsCheck) |
| 127 | 266 | | return; |
| | 267 | |
|
| 72 | 268 | | SetMeshesAndComponentsInsideBoundariesState(entity, IsEntityMeshInsideSceneBoundaries(entity)); |
| 72 | 269 | | } |
| | 270 | |
|
| | 271 | | private void EvaluateEntityPosition(IDCLEntity entity, bool onlyOuterBoundsCheck = false) |
| | 272 | | { |
| 63 | 273 | | Vector3 entityGOPosition = entity.gameObject.transform.position; |
| 63 | 274 | | entity.UpdateOuterBoundariesStatus(entity.scene.IsInsideSceneOuterBoundaries(entityGOPosition)); |
| | 275 | |
|
| 63 | 276 | | if (!entity.isInsideSceneOuterBoundaries) |
| | 277 | | { |
| 31 | 278 | | SetComponentsInsideBoundariesValidState(entity, false); |
| 31 | 279 | | SetEntityInsideBoundariesState(entity, false); |
| | 280 | | } |
| | 281 | |
|
| 63 | 282 | | if (onlyOuterBoundsCheck) |
| 57 | 283 | | return; |
| | 284 | |
|
| 6 | 285 | | bool isInsideBoundaries = entity.scene.IsInsideSceneBoundaries(entityGOPosition + CommonScriptableObjects.wo |
| 6 | 286 | | SetComponentsInsideBoundariesValidState(entity, isInsideBoundaries); |
| 6 | 287 | | SetEntityInsideBoundariesState(entity, isInsideBoundaries); |
| 6 | 288 | | } |
| | 289 | |
|
| | 290 | | private void EvaluateAvatarMeshBounds(IDCLEntity entity, bool onlyOuterBoundsCheck = false) |
| | 291 | | { |
| 0 | 292 | | 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 |
| 0 | 296 | | Bounds avatarBounds = new Bounds(); |
| 0 | 297 | | avatarBounds.center = entityGOPosition; |
| 0 | 298 | | avatarBounds.size = entity.gameObject.transform.lossyScale; |
| | 299 | |
|
| 0 | 300 | | entity.UpdateOuterBoundariesStatus(entity.scene.IsInsideSceneOuterBoundaries(avatarBounds)); |
| | 301 | |
|
| 0 | 302 | | if (!entity.isInsideSceneOuterBoundaries) |
| | 303 | | { |
| 0 | 304 | | SetComponentsInsideBoundariesValidState(entity, false); |
| 0 | 305 | | SetEntityInsideBoundariesState(entity, false); |
| | 306 | | } |
| | 307 | |
|
| 0 | 308 | | if (onlyOuterBoundsCheck) |
| 0 | 309 | | return; |
| | 310 | |
|
| 0 | 311 | | bool isInsideBoundaries = entity.scene.IsInsideSceneBoundaries(avatarBounds); |
| 0 | 312 | | SetComponentsInsideBoundariesValidState(entity, isInsideBoundaries); |
| 0 | 313 | | SetEntityInsideBoundariesState(entity, isInsideBoundaries); |
| 0 | 314 | | } |
| | 315 | |
|
| | 316 | | private void SetEntityInsideBoundariesState(IDCLEntity entity, bool isInsideBoundaries) |
| | 317 | | { |
| 359 | 318 | | if (entity.isInsideSceneBoundaries == isInsideBoundaries) |
| 261 | 319 | | return; |
| | 320 | |
|
| 98 | 321 | | entity.UpdateInsideBoundariesStatus(isInsideBoundaries); |
| 98 | 322 | | } |
| | 323 | |
|
| | 324 | | private bool HasMesh(IDCLEntity entity) => |
| 268 | 325 | | 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 | | { |
| 72 | 332 | | if (entity.meshesInfo == null |
| | 333 | | || entity.meshesInfo.meshRootGameObject == null |
| | 334 | | || entity.meshesInfo.mergedBounds == null) |
| 0 | 335 | | return false; |
| | 336 | |
|
| | 337 | | // 1st check (full mesh AABB) |
| 72 | 338 | | bool isInsideBoundaries = entity.scene.IsInsideSceneBoundaries(entity.meshesInfo.mergedBounds); |
| | 339 | |
|
| | 340 | | // 2nd check (submeshes & colliders AABB) |
| 103 | 341 | | if (!isInsideBoundaries) { isInsideBoundaries = AreSubMeshesInsideBoundaries(entity) && AreCollidersInsideBo |
| | 342 | |
|
| 72 | 343 | | return isInsideBoundaries; |
| | 344 | | } |
| | 345 | |
|
| | 346 | | private static bool AreSubMeshesInsideBoundaries(IDCLEntity entity) |
| | 347 | | { |
| 62 | 348 | | for (int i = 0; i < entity.meshesInfo.renderers.Length; i++) |
| | 349 | | { |
| 29 | 350 | | Renderer renderer = entity.meshesInfo.renderers[i]; |
| | 351 | |
|
| 29 | 352 | | if (renderer == null) |
| | 353 | | continue; |
| | 354 | |
|
| 29 | 355 | | if (!entity.scene.IsInsideSceneBoundaries(MeshesInfoUtils.GetSafeBounds(renderer.bounds, renderer.transf |
| 29 | 356 | | return false; |
| | 357 | | } |
| | 358 | |
|
| 2 | 359 | | return true; |
| | 360 | | } |
| | 361 | |
|
| | 362 | | private bool AreCollidersInsideBoundaries(IDCLEntity entity) |
| | 363 | | { |
| 6 | 364 | | foreach (Collider collider in entity.meshesInfo.colliders) |
| | 365 | | { |
| 2 | 366 | | if (collider == null) |
| | 367 | | continue; |
| | 368 | |
|
| 2 | 369 | | if (!entity.scene.IsInsideSceneBoundaries(MeshesInfoUtils.GetSafeBounds(collider.bounds, collider.transf |
| 2 | 370 | | return false; |
| | 371 | | } |
| | 372 | |
|
| 0 | 373 | | return true; |
| 2 | 374 | | } |
| | 375 | |
|
| | 376 | | private void SetMeshesAndComponentsInsideBoundariesState(IDCLEntity entity, bool isInsideBoundaries) |
| | 377 | | { |
| 322 | 378 | | SetEntityMeshesInsideBoundariesState(entity.meshesInfo, isInsideBoundaries); |
| 322 | 379 | | SetEntityCollidersInsideBoundariesState(entity.meshesInfo, isInsideBoundaries); |
| 322 | 380 | | SetComponentsInsideBoundariesValidState(entity, isInsideBoundaries); |
| | 381 | |
|
| | 382 | | // Should always be set last as entity.isInsideSceneBoundaries is checked to avoid re-running code unnecessa |
| 322 | 383 | | SetEntityInsideBoundariesState(entity, isInsideBoundaries); |
| 322 | 384 | | } |
| | 385 | |
|
| | 386 | | private void SetEntityMeshesInsideBoundariesState(MeshesInfo meshesInfo, bool isInsideBoundaries) |
| | 387 | | { |
| 322 | 388 | | feedbackStyle.ApplyFeedback(meshesInfo, isInsideBoundaries); |
| 322 | 389 | | } |
| | 390 | |
|
| | 391 | | private void SetEntityCollidersInsideBoundariesState(MeshesInfo meshesInfo, bool isInsideBoundaries) |
| | 392 | | { |
| 322 | 393 | | if (meshesInfo == null || meshesInfo.colliders.Count == 0 || !meshesInfo.currentShape.HasCollisions()) |
| 85 | 394 | | return; |
| | 395 | |
|
| 1050 | 396 | | foreach (Collider collider in meshesInfo.colliders) |
| | 397 | | { |
| 288 | 398 | | if (collider == null) continue; |
| | 399 | |
|
| 288 | 400 | | if (collider.enabled != isInsideBoundaries) |
| 116 | 401 | | collider.enabled = isInsideBoundaries; |
| | 402 | | } |
| 237 | 403 | | } |
| | 404 | |
|
| | 405 | | private void SetComponentsInsideBoundariesValidState(IDCLEntity entity, bool isInsideBoundaries) |
| | 406 | | { |
| 359 | 407 | | if (entity.isInsideSceneBoundaries == isInsideBoundaries || !DataStore.i.sceneBoundariesChecker.componentsCh |
| 357 | 408 | | return; |
| | 409 | |
|
| 10 | 410 | | foreach (IOutOfSceneBoundariesHandler component in DataStore.i.sceneBoundariesChecker.componentsCheckSceneBo |
| 2 | 411 | | } |
| | 412 | | } |
| | 413 | | } |