From f21578ae57bf4dde5844af971c62acc18acd89bf Mon Sep 17 00:00:00 2001 From: Subaka <108687175+SniffBakaSniff@users.noreply.github.com> Date: Fri, 2 Oct 2026 18:26:06 -0500 Subject: [PATCH 1/8] chore/code-cleanup See CHANGELOG.md [1.1.1-dev-alpha] for details. --- CHANGELOG.md | 26 +- Events/EventManager.cs | 9 +- SceneManager/SceneLoadWaiter.cs | 315 ++++++++++++++++++++++ SceneManager/SceneManager.cs | 460 ++------------------------------ 4 files changed, 361 insertions(+), 449 deletions(-) create mode 100644 SceneManager/SceneLoadWaiter.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index c759746..241e065 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,31 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] - 2026-10-02 +## [1.1.1-dev-alpha] - 2026-10-02 + +### Added + +* `SceneLoadWaiter.GetPlayer()` — extracted helper that resolves the player `GameObject` from `Deadshot.GameManager` with explicit error logging for failure cases. +* `SceneLoadWaiter.DisableAllCameras()` — extracted helper that disables all cameras except the player's `Head/MainCamera`. +* `SceneLoadWaiter.DisableMenuAndSceneRenderers()` — extracted helper that hides `MainMenu` canvases and `C1L2` scene renderers, excluding the player. +* `SceneLoadWaiter.GetCustomScene()` — extracted helper that scans loaded scenes by name and returns the matching `Scene`. +* `SceneLoadWaiter.FindCustomPlayerSpawn(Scene)` — updated to accept the already-resolved `Scene` instead of re-scanning for it. +* Added null guards for `parent`, root `GameObject`s, and child `Transform`s in `FindChildRecursive`. +* Player `CharacterController` is now restored in a `finally` block, ensuring it is re-enabled even if player placement throws. +* `SceneLoadWaiter.Update()` now logs an error and returns when the player is unavailable before attempting to place it. + +### Changed + +* `SceneLoadWaiter.InitializePlayer()` refactored to delegate player, camera, and renderer setup to dedicated helper methods. +* Corrected the renderer-filtering scene name from `C1L1` to `C1L2`. +* `SceneManager.LoadScene()` no longer handles player, camera, or spawn logic; it now only creates and initializes a `SceneLoadWaiter` on a `DontDestroyOnLoad` `GameObject`. +* `FindChildRecursive` changed to a shallow, single-level child scan. + +### Fixed + +* Removed stray blank lines from `EventManager.LevelPlaytimeEvent()`. + +## [1.1.0-dev-alpha] - 2026-10-02 ### Added - `SceneLoadWaiter` MonoBehaviour for additive custom-scene loading while preserving the existing gameplay scene and player. diff --git a/Events/EventManager.cs b/Events/EventManager.cs index 8cd7121..c15ca4a 100644 --- a/Events/EventManager.cs +++ b/Events/EventManager.cs @@ -10,22 +10,15 @@ public class EventManager : MonoBehaviour { public static void Update() { - LevelEvent.Update(); + LevelEvent.LevelPlaytimeEvent(); } public static class LevelEvent { private static float _lastLevelTime; - public static event Action LevelTimeChanged; - public static event Action LevelCompleted; - internal static void Update() - { - LevelPlaytimeEvent(); - } - internal static void LevelPlaytimeEvent() { try diff --git a/SceneManager/SceneLoadWaiter.cs b/SceneManager/SceneLoadWaiter.cs new file mode 100644 index 0000000..4d610ed --- /dev/null +++ b/SceneManager/SceneLoadWaiter.cs @@ -0,0 +1,315 @@ +using System; +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace DeadshotModAPI; + +internal class SceneLoadWaiter : MonoBehaviour +{ + private string _sceneName = string.Empty; + private GameObject _player; + private bool _initialized; + private bool _sceneLoadRequested; + private bool _playerPlaced; + + internal void Initialize(string sceneName) + { + try + { + Logger.Log($"SceneLoadWaiter initialized with scene: '{sceneName}'"); + + if (string.IsNullOrWhiteSpace(sceneName)) + { + Logger.Error("SceneLoadWaiter received an empty scene name."); + + Destroy(gameObject); + return; + } + + _sceneName = sceneName; + } + catch (Exception ex) + { + Logger.Error($"SceneLoadWaiter.Initialize failed: {ex}"); + } + } + + internal void Update() + { + try + { + if (string.IsNullOrEmpty(_sceneName)) + { + return; + } + + if (!_initialized) + { + InitializePlayer(); + } + + Scene customScene = GetCustomScene(); + + if (!customScene.IsValid() || !customScene.isLoaded) + { + if (!_sceneLoadRequested) + { + _sceneLoadRequested = true; + + Logger.Log($"Loading custom scene: {_sceneName}"); + + UnityEngine.SceneManagement.SceneManager.LoadScene(_sceneName, LoadSceneMode.Additive); + } + + return; + } + + if (_playerPlaced) + { + return; + } + + // Unity's overloaded == also catches destroyed objects. + if (_player == null) + { + Logger.Error("Player is not available yet; cannot place player."); + return; + } + + Logger.Log($"Custom scene loaded: {_sceneName}"); + + GameObject spawnPoint = FindCustomPlayerSpawn(customScene); + + if (spawnPoint == null) + { + Logger.Error("Could not find CustomPlayerSpawn."); + return; + } + + CharacterController controller = _player.GetComponent(); + + if (controller == null) + { + Logger.Error("Failed to find player controller."); + return; + } + + controller.enabled = false; + + try + { + _player.transform.SetPositionAndRotation( + spawnPoint.transform.position, + spawnPoint.transform.rotation + ); + } + finally + { + controller.enabled = true; + } + + Logger.Log($"Moved player to CustomPlayerSpawn: {spawnPoint.transform.position}"); + + _playerPlaced = true; + } + catch (Exception ex) + { + Logger.Error($"SceneLoadWaiter.Update failed: {ex}"); + } + } + + private void InitializePlayer() + { + try + { + if (!GetPlayer()) return; + if (!DisableAllCameras()) return; + + DisableMenuAndSceneRenderers(); + + _initialized = true; + } + catch (Exception ex) + { + Logger.Error($"SceneLoadWaiter.InitializePlayer failed: {ex}"); + } + } + + private bool GetPlayer() + { + Deadshot.GameManager gameManager = Deadshot.GameManager.INSTANCE; + + if (gameManager == null) + { + Logger.Error("Could not find GameManager."); + return false; + } + + if (gameManager.PlayerManager == null) + { + Logger.Error("Could not find PlayerManager."); + return false; + } + + _player = gameManager.PlayerManager.gameObject; + + if (_player == null) + { + Logger.Error("Could not find player GameObject."); + return false; + } + + Logger.Log($"Found player: {_player.name}"); + return true; + } + + private bool DisableAllCameras() + { + Transform playerCameraTransform = _player.transform.Find("Head/MainCamera"); + + if (playerCameraTransform == null) + { + Logger.Error("Could not find Player/Head/MainCamera."); + return false; + } + + Camera playerCamera = playerCameraTransform.GetComponent(); + + if (playerCamera == null) + { + Logger.Error("Player MainCamera has no Camera component."); + return false; + } + + foreach (Camera camera in FindObjectsByType( + FindObjectsInactive.Include, + FindObjectsSortMode.None)) + { + if (camera == null) + { + continue; + } + + camera.enabled = camera == playerCamera; + } + + return true; + } + + private void DisableMenuAndSceneRenderers() + { + foreach (Canvas canvas in FindObjectsByType( + FindObjectsInactive.Include, + FindObjectsSortMode.None)) + { + if (canvas == null) + { + continue; + } + + if (canvas.gameObject.scene.name == "MainMenu") + { + canvas.enabled = false; + } + } + + foreach (Renderer renderer in FindObjectsByType( + FindObjectsInactive.Include, + FindObjectsSortMode.None)) + { + if (renderer == null) + { + continue; + } + + if (renderer.gameObject.scene.name != "C1L2") + { + continue; + } + + if (renderer.transform.IsChildOf(_player.transform)) + { + continue; + } + + renderer.enabled = false; + } + } + + private Scene GetCustomScene() + { + for (int i = 0; i < UnityEngine.SceneManagement.SceneManager.sceneCount; i++) + { + Scene scene = UnityEngine.SceneManagement.SceneManager.GetSceneAt(i); + + if (scene.name == _sceneName) + { + return scene; + } + } + + return default; + } + + private static GameObject FindCustomPlayerSpawn(Scene customScene) + { + try + { + if (!customScene.IsValid() || !customScene.isLoaded) + { + return null; + } + + foreach (GameObject root in customScene.GetRootGameObjects()) + { + if (root == null) + { + continue; + } + + GameObject spawn = FindChildRecursive(root.transform, "CustomPlayerSpawn"); + + if (spawn != null) + { + return spawn; + } + } + } + catch (Exception ex) + { + Logger.Error($"SceneLoadWaiter.FindCustomPlayerSpawn failed: {ex}"); + } + + return null; + } + + private static GameObject FindChildRecursive(Transform parent, string name) + { + if (parent == null) + { + return null; + } + + if (parent.name == name) + { + return parent.gameObject; + } + + for (int i = 0; i < parent.childCount; i++) + { + Transform child = parent.GetChild(i); + + if (child == null) + { + continue; + } + + if (child.name == name) + { + return child.gameObject; + } + } + + return null; + } +} \ No newline at end of file diff --git a/SceneManager/SceneManager.cs b/SceneManager/SceneManager.cs index b7e72cd..548e8bd 100644 --- a/SceneManager/SceneManager.cs +++ b/SceneManager/SceneManager.cs @@ -1,8 +1,7 @@ using System; using System.Collections.Generic; -using System.Reflection; using UnityEngine; -using Deadshot.Player; +using UnityEngine.SceneManagement; namespace DeadshotModAPI; @@ -28,19 +27,11 @@ public static void LoadScene(string sceneName) try { - // ----------------------------------------------------- - // CHECK IF THIS SCENE IS ALREADY LOADED - // ----------------------------------------------------- + Scene existingScene = default; - UnityEngine.SceneManagement.Scene existingScene = - default; - - for (int i = 0; - i < UnityEngine.SceneManagement.SceneManager.sceneCount; - i++) + for (int i = 0; i < UnityEngine.SceneManagement.SceneManager.sceneCount; i++) { - UnityEngine.SceneManagement.Scene scene = - UnityEngine.SceneManagement.SceneManager.GetSceneAt(i); + Scene scene = UnityEngine.SceneManagement.SceneManager.GetSceneAt(i); if (scene.name == sceneName) { @@ -49,44 +40,19 @@ public static void LoadScene(string sceneName) } } - // ----------------------------------------------------- - // IF ALREADY LOADED, UNLOAD IT FIRST. - // - // This allows F6 to reload the custom scene instead - // of creating another copy/waiter. - // ----------------------------------------------------- - - if (existingScene.IsValid() && - existingScene.isLoaded) + if (existingScene.IsValid() && existingScene.isLoaded) { - Logger.Log( - $"Scene '{sceneName}' is already loaded. " + - "Unloading before reloading." - ); - - UnityEngine.SceneManagement.SceneManager.UnloadSceneAsync( - existingScene - ); - - // The next waiter will wait until the scene is gone - // before loading it again. + Logger.Log($"Scene '{sceneName}' is already loaded. Unloading before reloading."); + _ = UnityEngine.SceneManagement.SceneManager.UnloadSceneAsync(existingScene); } - // ----------------------------------------------------- - // LOAD C1L2 IF NEEDED - // ----------------------------------------------------- - bool c1l2Loaded = false; - for (int i = 0; - i < UnityEngine.SceneManagement.SceneManager.sceneCount; - i++) + for (int i = 0; i < UnityEngine.SceneManagement.SceneManager.sceneCount; i++) { - UnityEngine.SceneManagement.Scene scene = - UnityEngine.SceneManagement.SceneManager.GetSceneAt(i); + Scene scene = UnityEngine.SceneManagement.SceneManager.GetSceneAt(i); - if (scene.name == "C1L2" && - scene.isLoaded) + if (scene.name == "C1L2" && scene.isLoaded) { c1l2Loaded = true; break; @@ -95,42 +61,19 @@ public static void LoadScene(string sceneName) if (!c1l2Loaded) { - Logger.Log( - "Loading Deadshot gameplay scene: C1L2" - ); - - UnityEngine.SceneManagement.SceneManager.LoadScene( - "C1L2", - UnityEngine.SceneManagement.LoadSceneMode.Additive - ); + Logger.Log("Loading Deadshot gameplay scene: C1L2"); - UnityEngine.SceneManagement.SceneManager.LoadScene( - "C1L2", - UnityEngine.SceneManagement.LoadSceneMode.Additive - ); + UnityEngine.SceneManagement.SceneManager.LoadScene("C1L2", LoadSceneMode.Additive); } - // ----------------------------------------------------- - // CREATE WAITER - // ----------------------------------------------------- - - var waiterObject = - new GameObject("DeadshotModAPI_SceneLoadWaiter"); - - UnityEngine.Object.DontDestroyOnLoad( - waiterObject - ); - - SceneLoadWaiter waiter = - waiterObject.AddComponent(); - + var waiterObject = new GameObject("DeadshotModAPI_SceneLoadWaiter"); + UnityEngine.Object.DontDestroyOnLoad(waiterObject); + SceneLoadWaiter waiter = waiterObject.AddComponent(); waiter.Initialize(sceneName); } catch (Exception ex) { - Logger.Error( - $"Failed to load scene '{sceneName}': {ex}" - ); + Logger.Error($"Failed to load scene '{sceneName}': {ex}"); } } @@ -150,382 +93,19 @@ internal static void LoadModsBundle() "DeadshotModApi/deadshotapi_assets" }; - List bundles = - bundleManager.LoadAssetBundles(bundlesFiles); + List bundles = bundleManager.LoadAssetBundles(bundlesFiles); if (bundles == null) { - Logger.Error( - "Failed to load asset bundles." - ); - + Logger.Error("Failed to load asset bundles."); return; } - Logger.Info( - $"Loaded {bundles.Count} asset bundles." - ); + Logger.Info($"Loaded {bundles.Count} asset bundles."); } catch (Exception ex) { - Logger.Error( - $"Failed to load Mods Menu bundle: {ex}" - ); - } - } -} - -// TODO: Review ChatGPT Code for SceneLoadWaiter, then fix issues! -internal class SceneLoadWaiter : MonoBehaviour -{ - private string _sceneName = string.Empty; - private GameObject _player; - private bool _initialized; - private bool _sceneLoadRequested; - private bool _playerPlaced; - private int _startupFrames; - - internal void Initialize(string sceneName) - { - Logger.Log( - $"SceneLoadWaiter initialized with scene: '{sceneName}'" - ); - - if (string.IsNullOrWhiteSpace(sceneName)) - { - Logger.Error( - "SceneLoadWaiter received an empty scene name." - ); - - Destroy(gameObject); - return; - } - - _sceneName = sceneName; - } - - internal void Update() - { - - // --------------------------------------------------------- - // WAIT FOR DEADSHOT TO FINISH INITIALIZING - // --------------------------------------------------------- - - if (_startupFrames < 30) - { - _startupFrames++; - return; - } - - // --------------------------------------------------------- - // INITIALIZE PLAYER - // --------------------------------------------------------- - - if (!_initialized) - { - InitializePlayer(); - - if (_player == null) - { - return; - } - - _initialized = true; - - Logger.Log( - "Player initialization complete." - ); - } - - // --------------------------------------------------------- - // FIND CUSTOM SCENE - // --------------------------------------------------------- - - UnityEngine.SceneManagement.Scene customScene = - default; - - for (int i = 0; - i < UnityEngine.SceneManagement.SceneManager.sceneCount; - i++) - { - UnityEngine.SceneManagement.Scene scene = - UnityEngine.SceneManagement.SceneManager.GetSceneAt(i); - - if (scene.name == _sceneName) - { - customScene = scene; - break; - } - } - - // --------------------------------------------------------- - // LOAD CUSTOM SCENE - // --------------------------------------------------------- - - if (!customScene.IsValid() || - !customScene.isLoaded) - { - if (!_sceneLoadRequested) - { - _sceneLoadRequested = true; - - Logger.Log( - $"Loading custom scene: {_sceneName}" - ); - - UnityEngine.SceneManagement.SceneManager.LoadScene( - _sceneName, - UnityEngine.SceneManagement.LoadSceneMode.Additive - ); - } - - return; - } - - // --------------------------------------------------------- - // CUSTOM SCENE LOADED - // --------------------------------------------------------- - - if (_playerPlaced) - { - return; + Logger.Error($"Failed to load Mods Menu bundle: {ex}"); } - - Logger.Log( - $"Custom scene loaded: {_sceneName}" - ); - - GameObject spawnPoint = - FindCustomPlayerSpawn(); - - if (spawnPoint == null) - { - Logger.Error( - "Could not find CustomPlayerSpawn." - ); - - return; - } - - // --------------------------------------------------------- - // TELEPORT - // --------------------------------------------------------- - - CharacterController controller = - _player.GetComponent(); - - if (controller == null) - { - Logger.Log("Failed to find player controller."); - return; - } - - controller.enabled = false; - - _player.transform.SetPositionAndRotation( - spawnPoint.transform.position, - spawnPoint.transform.rotation - ); - - if (controller != null) - { - controller.enabled = true; - } - - Logger.Log( - $"Moved player to CustomPlayerSpawn: " + - $"{spawnPoint.transform.position}" - ); - - _playerPlaced = true; - - Logger.Log( - $"Grounded={controller?.isGrounded} | " + - $"Velocity={controller?.velocity} | " + - $"Position={_player.transform.position}" - ); - } - - private void InitializePlayer() - { - Camera _playerCamera; - - Deadshot.GameManager gameManager = - Deadshot.GameManager.INSTANCE; - - if (gameManager == null) - { - return; - } - - if (gameManager.PlayerManager == null) - { - return; - } - - _player = - gameManager.PlayerManager.gameObject; - - if (_player == null) - { - return; - } - - Logger.Log( - $"Found player: {_player.name}" - ); - - Transform playerCameraTransform = - _player.transform.Find( - "Head/MainCamera" - ); - - if (playerCameraTransform == null) - { - Logger.Error( - "Could not find Player/Head/MainCamera." - ); - - return; - } - - _playerCamera = - playerCameraTransform.GetComponent(); - - if (_playerCamera == null) - { - Logger.Error( - "Player MainCamera has no Camera component." - ); - - return; - } - - // --------------------------------------------------------- - // CAMERA - // --------------------------------------------------------- - - foreach (Camera camera in - UnityEngine.Object.FindObjectsByType( - FindObjectsInactive.Include, - FindObjectsSortMode.None)) - { - camera.enabled = - camera == _playerCamera; - } - - // --------------------------------------------------------- - // HIDE MAIN MENU - // --------------------------------------------------------- - - foreach (Canvas canvas in - UnityEngine.Object.FindObjectsByType( - FindObjectsInactive.Include, - FindObjectsSortMode.None)) - { - if (canvas.gameObject.scene.name == "MainMenu") - { - canvas.enabled = false; - } - } - - // --------------------------------------------------------- - // HIDE C1L1 VISUALS - // - // Keep ALL C1L1 COLLIDERS AND GAMEPLAY OBJECTS ALIVE. - // --------------------------------------------------------- - - foreach (Renderer renderer in - UnityEngine.Object.FindObjectsByType( - FindObjectsInactive.Include, - FindObjectsSortMode.None)) - { - if (renderer.gameObject.scene.name != "C1L1") - { - continue; - } - - if (renderer.transform.IsChildOf( - _player.transform)) - { - continue; - } - - renderer.enabled = false; - } - } - - private GameObject FindCustomPlayerSpawn() - { - UnityEngine.SceneManagement.Scene customScene = - default; - - for (int i = 0; - i < UnityEngine.SceneManagement.SceneManager.sceneCount; - i++) - { - UnityEngine.SceneManagement.Scene scene = - UnityEngine.SceneManagement.SceneManager.GetSceneAt(i); - - if (scene.name == _sceneName) - { - customScene = scene; - break; - } - } - - if (!customScene.IsValid() || - !customScene.isLoaded) - { - return null; - } - - foreach (GameObject root in - customScene.GetRootGameObjects()) - { - GameObject spawn = - FindChildRecursive( - root.transform, - "CustomPlayerSpawn" - ); - - if (spawn != null) - { - return spawn; - } - } - - return null; - } - - private static GameObject FindChildRecursive( - Transform parent, - string name) - { - if (parent.name == name) - { - return parent.gameObject; - } - - for (int i = 0; - i < parent.childCount; - i++) - { - Transform child = - parent.GetChild(i); - - GameObject result = - FindChildRecursive( - child, - name - ); - - if (result != null) - { - return result; - } - } - - return null; } } \ No newline at end of file From 3aeb9082d3ead607f4ae8a5896e89f59cb2a5838 Mon Sep 17 00:00:00 2001 From: Subaka <108687175+SniffBakaSniff@users.noreply.github.com> Date: Fri, 2 Oct 2026 18:31:10 -0500 Subject: [PATCH 2/8] Update plugin version Forgot to update the plugin version. --- Plugin.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugin.cs b/Plugin.cs index 1dc3a7a..f5ace58 100644 --- a/Plugin.cs +++ b/Plugin.cs @@ -4,7 +4,7 @@ namespace DeadshotModAPI; -[BepInPlugin("com.subaka.deadshotmodapi", "Deadshot Mod API", "v1.0.0-dev-alpha")] +[BepInPlugin("com.subaka.deadshotmodapi", "Deadshot Mod API", "v1.1.1-dev-alpha")] public class Plugin : BasePlugin { From 24f73b415de31ea2ce02f2e80e16c85320850dfe Mon Sep 17 00:00:00 2001 From: Cyrusbye720 Date: Sun, 4 Oct 2026 06:15:06 +0530 Subject: [PATCH 3/8] fix(scenemanager): resolve waiter leak, async reload race, and child recursion --- CHANGELOG.md | 16 ++++--- Events/EventManager.cs | 2 +- SceneManager/SceneLoadWaiter.cs | 78 +++++++++++++++++++-------------- SceneManager/SceneManager.cs | 29 ++++++++---- 4 files changed, 77 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 241e065..2078f1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,24 +8,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -* `SceneLoadWaiter.GetPlayer()` — extracted helper that resolves the player `GameObject` from `Deadshot.GameManager` with explicit error logging for failure cases. +* `SceneLoadWaiter.GetPlayer()` — extracted helper that resolves the player `GameObject` from `Deadshot.GameManager`. * `SceneLoadWaiter.DisableAllCameras()` — extracted helper that disables all cameras except the player's `Head/MainCamera`. * `SceneLoadWaiter.DisableMenuAndSceneRenderers()` — extracted helper that hides `MainMenu` canvases and `C1L2` scene renderers, excluding the player. * `SceneLoadWaiter.GetCustomScene()` — extracted helper that scans loaded scenes by name and returns the matching `Scene`. * `SceneLoadWaiter.FindCustomPlayerSpawn(Scene)` — updated to accept the already-resolved `Scene` instead of re-scanning for it. -* Added null guards for `parent`, root `GameObject`s, and child `Transform`s in `FindChildRecursive`. +* Added null guards and restored deep recursive traversal in `FindChildRecursive` to support nested hierarchy spawn points. * Player `CharacterController` is now restored in a `finally` block, ensuring it is re-enabled even if player placement throws. -* `SceneLoadWaiter.Update()` now logs an error and returns when the player is unavailable before attempting to place it. +* `SceneManager.BaseGameplaySceneName` constant to avoid hardcoded scene name strings. +* `AsyncOperation` tracking for asynchronous scene unloads to prevent reload race conditions. ### Changed * `SceneLoadWaiter.InitializePlayer()` refactored to delegate player, camera, and renderer setup to dedicated helper methods. * Corrected the renderer-filtering scene name from `C1L1` to `C1L2`. -* `SceneManager.LoadScene()` no longer handles player, camera, or spawn logic; it now only creates and initializes a `SceneLoadWaiter` on a `DontDestroyOnLoad` `GameObject`. -* `FindChildRecursive` changed to a shallow, single-level child scan. +* `SceneManager.LoadScene()` cleans up existing waiters and passes any pending unload operation to the new waiter. +* `SceneLoadWaiter` destroys its `GameObject` upon placement completion or spawn search timeout. +* `EventManager.Update()` converted from a static method to an instance lifecycle method on `MonoBehaviour`. ### Fixed +* Resolved persistent `SceneLoadWaiter` memory leak and CPU overhead after scene load. +* Fixed broken child traversal in `FindChildRecursive` failing on nested spawn points. +* Fixed race condition where reloading an already loaded scene attempted placement before unloader completed. +* Fixed `EventManager.Update()` not being invoked by Unity's engine loop. * Removed stray blank lines from `EventManager.LevelPlaytimeEvent()`. ## [1.1.0-dev-alpha] - 2026-10-02 diff --git a/Events/EventManager.cs b/Events/EventManager.cs index c15ca4a..7bc6fcf 100644 --- a/Events/EventManager.cs +++ b/Events/EventManager.cs @@ -8,7 +8,7 @@ namespace DeadshotModAPI; public class EventManager : MonoBehaviour { - public static void Update() + internal void Update() { LevelEvent.LevelPlaytimeEvent(); } diff --git a/SceneManager/SceneLoadWaiter.cs b/SceneManager/SceneLoadWaiter.cs index 4d610ed..cc82ebc 100644 --- a/SceneManager/SceneLoadWaiter.cs +++ b/SceneManager/SceneLoadWaiter.cs @@ -6,13 +6,16 @@ namespace DeadshotModAPI; internal class SceneLoadWaiter : MonoBehaviour { + private const int MaxSpawnSearchAttempts = 300; + private string _sceneName = string.Empty; + private AsyncOperation _unloadOperation; private GameObject _player; private bool _initialized; private bool _sceneLoadRequested; - private bool _playerPlaced; + private int _spawnSearchAttempts; - internal void Initialize(string sceneName) + internal void Initialize(string sceneName, AsyncOperation unloadOperation = null) { try { @@ -21,12 +24,12 @@ internal void Initialize(string sceneName) if (string.IsNullOrWhiteSpace(sceneName)) { Logger.Error("SceneLoadWaiter received an empty scene name."); - Destroy(gameObject); return; } _sceneName = sceneName; + _unloadOperation = unloadOperation; } catch (Exception ex) { @@ -43,6 +46,16 @@ internal void Update() return; } + if (_unloadOperation != null) + { + if (!_unloadOperation.isDone) + { + return; + } + + _unloadOperation = null; + } + if (!_initialized) { InitializePlayer(); @@ -55,34 +68,30 @@ internal void Update() if (!_sceneLoadRequested) { _sceneLoadRequested = true; - Logger.Log($"Loading custom scene: {_sceneName}"); - UnityEngine.SceneManagement.SceneManager.LoadScene(_sceneName, LoadSceneMode.Additive); } return; } - if (_playerPlaced) - { - return; - } - - // Unity's overloaded == also catches destroyed objects. if (_player == null) { - Logger.Error("Player is not available yet; cannot place player."); + _initialized = false; return; } - Logger.Log($"Custom scene loaded: {_sceneName}"); - GameObject spawnPoint = FindCustomPlayerSpawn(customScene); if (spawnPoint == null) { - Logger.Error("Could not find CustomPlayerSpawn."); + _spawnSearchAttempts++; + if (_spawnSearchAttempts > MaxSpawnSearchAttempts) + { + Logger.Error($"Could not find CustomPlayerSpawn in scene '{_sceneName}'."); + Destroy(gameObject); + } + return; } @@ -91,9 +100,13 @@ internal void Update() if (controller == null) { Logger.Error("Failed to find player controller."); + Destroy(gameObject); return; } + DisableAllCameras(); + DisableMenuAndSceneRenderers(); + controller.enabled = false; try @@ -109,8 +122,7 @@ internal void Update() } Logger.Log($"Moved player to CustomPlayerSpawn: {spawnPoint.transform.position}"); - - _playerPlaced = true; + Destroy(gameObject); } catch (Exception ex) { @@ -122,11 +134,17 @@ private void InitializePlayer() { try { - if (!GetPlayer()) return; - if (!DisableAllCameras()) return; + if (!GetPlayer()) + { + return; + } - DisableMenuAndSceneRenderers(); + if (!DisableAllCameras()) + { + return; + } + DisableMenuAndSceneRenderers(); _initialized = true; } catch (Exception ex) @@ -139,15 +157,8 @@ private bool GetPlayer() { Deadshot.GameManager gameManager = Deadshot.GameManager.INSTANCE; - if (gameManager == null) - { - Logger.Error("Could not find GameManager."); - return false; - } - - if (gameManager.PlayerManager == null) + if (gameManager == null || gameManager.PlayerManager == null) { - Logger.Error("Could not find PlayerManager."); return false; } @@ -155,7 +166,6 @@ private bool GetPlayer() if (_player == null) { - Logger.Error("Could not find player GameObject."); return false; } @@ -169,7 +179,6 @@ private bool DisableAllCameras() if (playerCameraTransform == null) { - Logger.Error("Could not find Player/Head/MainCamera."); return false; } @@ -177,7 +186,6 @@ private bool DisableAllCameras() if (playerCamera == null) { - Logger.Error("Player MainCamera has no Camera component."); return false; } @@ -222,7 +230,7 @@ private void DisableMenuAndSceneRenderers() continue; } - if (renderer.gameObject.scene.name != "C1L2") + if (renderer.gameObject.scene.name != SceneManager.BaseGameplaySceneName) { continue; } @@ -304,9 +312,11 @@ private static GameObject FindChildRecursive(Transform parent, string name) continue; } - if (child.name == name) + GameObject result = FindChildRecursive(child, name); + + if (result != null) { - return child.gameObject; + return result; } } diff --git a/SceneManager/SceneManager.cs b/SceneManager/SceneManager.cs index 548e8bd..c05b2ce 100644 --- a/SceneManager/SceneManager.cs +++ b/SceneManager/SceneManager.cs @@ -10,6 +10,8 @@ namespace DeadshotModAPI; /// public static class SceneManager { + internal const string BaseGameplaySceneName = "C1L2"; + /// /// Loads a scene while keeping Deadshot's gameplay scene loaded /// so the existing player and gameplay systems remain available. @@ -27,6 +29,7 @@ public static void LoadScene(string sceneName) try { + AsyncOperation unloadOperation = null; Scene existingScene = default; for (int i = 0; i < UnityEngine.SceneManagement.SceneManager.sceneCount; i++) @@ -43,33 +46,43 @@ public static void LoadScene(string sceneName) if (existingScene.IsValid() && existingScene.isLoaded) { Logger.Log($"Scene '{sceneName}' is already loaded. Unloading before reloading."); - _ = UnityEngine.SceneManagement.SceneManager.UnloadSceneAsync(existingScene); + unloadOperation = UnityEngine.SceneManagement.SceneManager.UnloadSceneAsync(existingScene); } - bool c1l2Loaded = false; + bool baseGameplaySceneLoaded = false; for (int i = 0; i < UnityEngine.SceneManagement.SceneManager.sceneCount; i++) { Scene scene = UnityEngine.SceneManagement.SceneManager.GetSceneAt(i); - if (scene.name == "C1L2" && scene.isLoaded) + if (scene.name == BaseGameplaySceneName && scene.isLoaded) { - c1l2Loaded = true; + baseGameplaySceneLoaded = true; break; } } - if (!c1l2Loaded) + if (!baseGameplaySceneLoaded) { - Logger.Log("Loading Deadshot gameplay scene: C1L2"); + Logger.Log($"Loading Deadshot gameplay scene: {BaseGameplaySceneName}"); + + UnityEngine.SceneManagement.SceneManager.LoadScene(BaseGameplaySceneName, LoadSceneMode.Additive); + } - UnityEngine.SceneManagement.SceneManager.LoadScene("C1L2", LoadSceneMode.Additive); + SceneLoadWaiter[] existingWaiters = UnityEngine.Object.FindObjectsByType( + FindObjectsInactive.Include, + FindObjectsSortMode.None + ); + + for (int i = 0; i < existingWaiters.Length; i++) + { + UnityEngine.Object.Destroy(existingWaiters[i].gameObject); } var waiterObject = new GameObject("DeadshotModAPI_SceneLoadWaiter"); UnityEngine.Object.DontDestroyOnLoad(waiterObject); SceneLoadWaiter waiter = waiterObject.AddComponent(); - waiter.Initialize(sceneName); + waiter.Initialize(sceneName, unloadOperation); } catch (Exception ex) { From 9857fdbed160de03b59a86b8f1674660781d6347 Mon Sep 17 00:00:00 2001 From: Cyrusbye720 Date: Sun, 4 Oct 2026 06:21:00 +0530 Subject: [PATCH 4/8] fix(scenemanager): handle load exceptions and self-destruct waiter on failure --- SceneManager/SceneLoadWaiter.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/SceneManager/SceneLoadWaiter.cs b/SceneManager/SceneLoadWaiter.cs index cc82ebc..081d42e 100644 --- a/SceneManager/SceneLoadWaiter.cs +++ b/SceneManager/SceneLoadWaiter.cs @@ -69,7 +69,18 @@ internal void Update() { _sceneLoadRequested = true; Logger.Log($"Loading custom scene: {_sceneName}"); - UnityEngine.SceneManagement.SceneManager.LoadScene(_sceneName, LoadSceneMode.Additive); + + try + { + UnityEngine.SceneManagement.SceneManager.LoadScene(_sceneName, LoadSceneMode.Additive); + } + catch (Exception ex) + { + _sceneLoadRequested = false; + Logger.Error($"Failed to load custom scene '{_sceneName}': {ex}"); + Destroy(gameObject); + return; + } } return; From 5475abcaa60326838366e16b315b054395714a24 Mon Sep 17 00:00:00 2001 From: Cyrusbye720 Date: Sun, 4 Oct 2026 06:25:06 +0530 Subject: [PATCH 5/8] fix(scenemanager): guard against concurrent scene loads and check empty bundle list --- SceneManager/SceneManager.cs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/SceneManager/SceneManager.cs b/SceneManager/SceneManager.cs index c05b2ce..6f9a59a 100644 --- a/SceneManager/SceneManager.cs +++ b/SceneManager/SceneManager.cs @@ -29,6 +29,12 @@ public static void LoadScene(string sceneName) try { + if (UnityEngine.Object.FindAnyObjectByType() != null) + { + Logger.Log($"Scene load already in progress; ignoring duplicate request for '{sceneName}'."); + return; + } + AsyncOperation unloadOperation = null; Scene existingScene = default; @@ -69,16 +75,6 @@ public static void LoadScene(string sceneName) UnityEngine.SceneManagement.SceneManager.LoadScene(BaseGameplaySceneName, LoadSceneMode.Additive); } - SceneLoadWaiter[] existingWaiters = UnityEngine.Object.FindObjectsByType( - FindObjectsInactive.Include, - FindObjectsSortMode.None - ); - - for (int i = 0; i < existingWaiters.Length; i++) - { - UnityEngine.Object.Destroy(existingWaiters[i].gameObject); - } - var waiterObject = new GameObject("DeadshotModAPI_SceneLoadWaiter"); UnityEngine.Object.DontDestroyOnLoad(waiterObject); SceneLoadWaiter waiter = waiterObject.AddComponent(); @@ -108,7 +104,7 @@ internal static void LoadModsBundle() List bundles = bundleManager.LoadAssetBundles(bundlesFiles); - if (bundles == null) + if (bundles == null || bundles.Count == 0) { Logger.Error("Failed to load asset bundles."); return; From f66345b2638bf8f01c989ec3c8c63b971233f915 Mon Sep 17 00:00:00 2001 From: Cyrusbye720 Date: Sun, 4 Oct 2026 06:34:46 +0530 Subject: [PATCH 6/8] fix(scenemanager): prevent stale disabled renderers, remove startup waiter, and track loading state --- Plugin.cs | 1 - SceneManager/SceneLoadWaiter.cs | 36 +++++++++++++++++++++++++++------ SceneManager/SceneManager.cs | 15 +++++++++----- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/Plugin.cs b/Plugin.cs index f5ace58..7613150 100644 --- a/Plugin.cs +++ b/Plugin.cs @@ -15,6 +15,5 @@ public override void Load() harmony.PatchAll(); AddComponent(); AddComponent(); - AddComponent(); } } \ No newline at end of file diff --git a/SceneManager/SceneLoadWaiter.cs b/SceneManager/SceneLoadWaiter.cs index 081d42e..0732f71 100644 --- a/SceneManager/SceneLoadWaiter.cs +++ b/SceneManager/SceneLoadWaiter.cs @@ -13,6 +13,7 @@ internal class SceneLoadWaiter : MonoBehaviour private GameObject _player; private bool _initialized; private bool _sceneLoadRequested; + private bool _playerPlaced; private int _spawnSearchAttempts; internal void Initialize(string sceneName, AsyncOperation unloadOperation = null) @@ -132,6 +133,7 @@ internal void Update() controller.enabled = true; } + _playerPlaced = true; Logger.Log($"Moved player to CustomPlayerSpawn: {spawnPoint.transform.position}"); Destroy(gameObject); } @@ -141,6 +143,16 @@ internal void Update() } } + internal void OnDestroy() + { + SceneManager.IsSceneLoading = false; + + if (!_playerPlaced) + { + RestoreMenuAndSceneRenderers(); + } + } + private void InitializePlayer() { try @@ -150,12 +162,6 @@ private void InitializePlayer() return; } - if (!DisableAllCameras()) - { - return; - } - - DisableMenuAndSceneRenderers(); _initialized = true; } catch (Exception ex) @@ -255,6 +261,24 @@ private void DisableMenuAndSceneRenderers() } } + private void RestoreMenuAndSceneRenderers() + { + foreach (Renderer renderer in FindObjectsByType( + FindObjectsInactive.Include, + FindObjectsSortMode.None)) + { + if (renderer == null) + { + continue; + } + + if (renderer.gameObject.scene.name == SceneManager.BaseGameplaySceneName) + { + renderer.enabled = true; + } + } + } + private Scene GetCustomScene() { for (int i = 0; i < UnityEngine.SceneManagement.SceneManager.sceneCount; i++) diff --git a/SceneManager/SceneManager.cs b/SceneManager/SceneManager.cs index 6f9a59a..23317ff 100644 --- a/SceneManager/SceneManager.cs +++ b/SceneManager/SceneManager.cs @@ -12,6 +12,8 @@ public static class SceneManager { internal const string BaseGameplaySceneName = "C1L2"; + internal static bool IsSceneLoading { get; set; } + /// /// Loads a scene while keeping Deadshot's gameplay scene loaded /// so the existing player and gameplay systems remain available. @@ -27,13 +29,15 @@ public static void LoadScene(string sceneName) return; } + if (IsSceneLoading) + { + Logger.Log($"Scene load already in progress; ignoring duplicate request for '{sceneName}'."); + return; + } + try { - if (UnityEngine.Object.FindAnyObjectByType() != null) - { - Logger.Log($"Scene load already in progress; ignoring duplicate request for '{sceneName}'."); - return; - } + IsSceneLoading = true; AsyncOperation unloadOperation = null; Scene existingScene = default; @@ -82,6 +86,7 @@ public static void LoadScene(string sceneName) } catch (Exception ex) { + IsSceneLoading = false; Logger.Error($"Failed to load scene '{sceneName}': {ex}"); } } From 523a54a2927c2edf5a223e742032b01c8371234b Mon Sep 17 00:00:00 2001 From: Cyrusbye720 Date: Sun, 4 Oct 2026 06:38:31 +0530 Subject: [PATCH 7/8] fix(scenemanager): prevent unloading and loading base gameplay scene as custom scene --- SceneManager/SceneManager.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/SceneManager/SceneManager.cs b/SceneManager/SceneManager.cs index 23317ff..aa88d5d 100644 --- a/SceneManager/SceneManager.cs +++ b/SceneManager/SceneManager.cs @@ -29,6 +29,12 @@ public static void LoadScene(string sceneName) return; } + if (string.Equals(sceneName, BaseGameplaySceneName, StringComparison.OrdinalIgnoreCase)) + { + Logger.Error($"Cannot load '{BaseGameplaySceneName}' as a custom scene; it is the base gameplay scene."); + return; + } + if (IsSceneLoading) { Logger.Log($"Scene load already in progress; ignoring duplicate request for '{sceneName}'."); From 0c19a9947a18643a60761162c1dbefb229d11e27 Mon Sep 17 00:00:00 2001 From: Cyrusbye720 Date: Sun, 4 Oct 2026 18:17:22 +0530 Subject: [PATCH 8/8] fix(scenemanager): add timeout for custom scene loading to prevent waiter hanging --- SceneManager/SceneLoadWaiter.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/SceneManager/SceneLoadWaiter.cs b/SceneManager/SceneLoadWaiter.cs index 0732f71..72aced2 100644 --- a/SceneManager/SceneLoadWaiter.cs +++ b/SceneManager/SceneLoadWaiter.cs @@ -6,6 +6,7 @@ namespace DeadshotModAPI; internal class SceneLoadWaiter : MonoBehaviour { + private const int MaxSceneLoadAttempts = 300; private const int MaxSpawnSearchAttempts = 300; private string _sceneName = string.Empty; @@ -14,6 +15,7 @@ internal class SceneLoadWaiter : MonoBehaviour private bool _initialized; private bool _sceneLoadRequested; private bool _playerPlaced; + private int _sceneLoadAttempts; private int _spawnSearchAttempts; internal void Initialize(string sceneName, AsyncOperation unloadOperation = null) @@ -83,6 +85,16 @@ internal void Update() return; } } + else + { + _sceneLoadAttempts++; + if (_sceneLoadAttempts > MaxSceneLoadAttempts) + { + Logger.Error($"Timed out waiting for scene '{_sceneName}' to load."); + Destroy(gameObject); + return; + } + } return; }