using System; using System.Collections.Generic; using ModestTree; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.SceneManagement; namespace Zenject.Internal { [InitializeOnLoad] public static class SceneParentAutomaticLoader { static SceneParentAutomaticLoader() { EditorApplication.playModeStateChanged += OnPlayModeStateChanged; } static void OnPlayModeStateChanged(PlayModeStateChange state) { if (state == PlayModeStateChange.ExitingEditMode) { try { ValidateMultiSceneSetupAndLoadDefaultSceneParents(); } catch (Exception e) { EditorApplication.isPlaying = false; throw new ZenjectException( "Failure occurred when attempting to load default scene parent contracts!", e); } } else if (state == PlayModeStateChange.EnteredEditMode) { // It would be cool to restore the initial scene set up here but in order to do this // we would have to make sure that the user saves the scene before running which // would be too annoying, so just leave any changes we've made alone } } public static void ValidateMultiSceneSetupAndLoadDefaultSceneParents() { var defaultContractsMap = LoadDefaultContractsMap(); // NOTE: Even if configs is empty we still want to do the below logic to validate the // multi scene setup var sceneInfos = GetLoadedZenjectSceneInfos(); var contractMap = GetCurrentSceneContractsMap(sceneInfos); foreach (var sceneInfo in sceneInfos) { ProcessScene(sceneInfo, contractMap, defaultContractsMap); } } static Dictionary GetCurrentSceneContractsMap( List sceneInfos) { var contractMap = new Dictionary(); foreach (var info in sceneInfos) { AddToContractMap(contractMap, info); } return contractMap; } static void ProcessScene( LoadedSceneInfo sceneInfo, Dictionary contractMap, Dictionary defaultContractsMap) { if (sceneInfo.SceneContext != null) { Assert.IsNull(sceneInfo.DecoratorContext); ProcessSceneParents(sceneInfo, contractMap, defaultContractsMap); } else { Assert.IsNotNull(sceneInfo.DecoratorContext); ProcessSceneDecorators(sceneInfo, contractMap, defaultContractsMap); } } static void ProcessSceneDecorators( LoadedSceneInfo sceneInfo, Dictionary contractMap, Dictionary defaultContractsMap) { var decoratedContractName = sceneInfo.DecoratorContext.DecoratedContractName; LoadedSceneInfo decoratedSceneInfo; if (contractMap.TryGetValue(decoratedContractName, out decoratedSceneInfo)) { ValidateDecoratedSceneMatch(sceneInfo, decoratedSceneInfo); return; } decoratedSceneInfo = LoadDefaultSceneForContract( sceneInfo, decoratedContractName, defaultContractsMap); EditorSceneManager.MoveSceneAfter(decoratedSceneInfo.Scene, sceneInfo.Scene); ValidateDecoratedSceneMatch(sceneInfo, decoratedSceneInfo); ProcessScene(decoratedSceneInfo, contractMap, defaultContractsMap); } static void ProcessSceneParents( LoadedSceneInfo sceneInfo, Dictionary contractMap, Dictionary defaultContractsMap) { foreach (var parentContractName in sceneInfo.SceneContext.ParentContractNames) { LoadedSceneInfo parentInfo; if (contractMap.TryGetValue(parentContractName, out parentInfo)) { ValidateParentChildMatch(parentInfo, sceneInfo); continue; } parentInfo = LoadDefaultSceneForContract(sceneInfo, parentContractName, defaultContractsMap); AddToContractMap(contractMap, parentInfo); EditorSceneManager.MoveSceneBefore(parentInfo.Scene, sceneInfo.Scene); ValidateParentChildMatch(parentInfo, sceneInfo); ProcessScene(parentInfo, contractMap, defaultContractsMap); } } static LoadedSceneInfo LoadDefaultSceneForContract( LoadedSceneInfo sceneInfo, string contractName, Dictionary defaultContractsMap) { string scenePath; if (!defaultContractsMap.TryGetValue(contractName, out scenePath)) { throw Assert.CreateException( "Could not fill contract '{0}' for scene '{1}'. No scenes with that contract name are loaded, and could not find a match in any default scene contract configs to auto load one either." .Fmt(contractName, sceneInfo.Scene.name)); } Scene scene; try { scene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Additive); } catch (Exception e) { throw new ZenjectException( "Error while attempting to load contracts for scene '{0}'".Fmt(sceneInfo.Scene.name), e); } return CreateLoadedSceneInfo(scene); } static void ValidateDecoratedSceneMatch( LoadedSceneInfo decoratorInfo, LoadedSceneInfo decoratedInfo) { var decoratorIndex = GetSceneIndex(decoratorInfo.Scene); var decoratedIndex = GetSceneIndex(decoratedInfo.Scene); var activeIndex = GetSceneIndex(EditorSceneManager.GetActiveScene()); Assert.That(decoratorIndex < decoratedIndex, "Decorator scene '{0}' must be loaded before decorated scene '{1}'. Please drag the decorator scene to be placed above the other scene in the scene hierarchy.", decoratorInfo.Scene.name, decoratedInfo.Scene.name); if (activeIndex > decoratorIndex) { EditorSceneManager.SetActiveScene(decoratorInfo.Scene); } } static void ValidateParentChildMatch( LoadedSceneInfo parentSceneInfo, LoadedSceneInfo sceneInfo) { var parentIndex = GetSceneIndex(parentSceneInfo.Scene); var childIndex = GetSceneIndex(sceneInfo.Scene); var activeIndex = GetSceneIndex(EditorSceneManager.GetActiveScene()); Assert.That(parentIndex < childIndex, "Parent scene '{0}' must be loaded before child scene '{1}'. Please drag it to be placed above its child in the scene hierarchy.", parentSceneInfo.Scene.name, sceneInfo.Scene.name); if (activeIndex > parentIndex) { EditorSceneManager.SetActiveScene(parentSceneInfo.Scene); } } static int GetSceneIndex(Scene scene) { for (int i = 0; i < EditorSceneManager.sceneCount; i++) { if (EditorSceneManager.GetSceneAt(i) == scene) { return i; } } throw Assert.CreateException(); } static Dictionary LoadDefaultContractsMap() { var configs = Resources.LoadAll(DefaultSceneContractConfig.ResourcePath); var map = new Dictionary(); foreach (var config in configs) { foreach (var info in config.DefaultContracts) { if (info.ContractName.Trim().IsEmpty()) { Log.Warn("Found empty contract name in default scene contract config at path '{0}'", AssetDatabase.GetAssetPath(config)); continue; } Assert.That(!map.ContainsKey(info.ContractName), "Found duplicate contract '{0}' in default scene contract config at '{1}'! Default contract already specified", info.ContractName, AssetDatabase.GetAssetPath(config)); map.Add(info.ContractName, AssetDatabase.GetAssetPath(info.Scene)); } } return map; } static LoadedSceneInfo CreateLoadedSceneInfo(Scene scene) { var info = TryCreateLoadedSceneInfo(scene); Assert.IsNotNull(info, "Expected scene '{0}' to be a zenject scene", scene.name); return info; } static LoadedSceneInfo TryCreateLoadedSceneInfo(Scene scene) { var sceneContext = ZenUnityEditorUtil.TryGetSceneContextForScene(scene); var decoratorContext = ZenUnityEditorUtil.TryGetDecoratorContextForScene(scene); if (sceneContext == null && decoratorContext == null) { return null; } var info = new LoadedSceneInfo { Scene = scene }; if (sceneContext != null) { Assert.IsNull(decoratorContext, "Found both SceneContext and SceneDecoratorContext in scene '{0}'", scene.name); info.SceneContext = sceneContext; } else { Assert.IsNotNull(decoratorContext); info.DecoratorContext = decoratorContext; } return info; } static List GetLoadedZenjectSceneInfos() { var result = new List(); for (int i = 0; i < EditorSceneManager.sceneCount; i++) { var scene = EditorSceneManager.GetSceneAt(i); var info = TryCreateLoadedSceneInfo(scene); if (info != null) { result.Add(info); } } return result; } static void AddToContractMap( Dictionary contractMap, LoadedSceneInfo info) { if (info.SceneContext == null) { return; } foreach (var contractName in info.SceneContext.ContractNames) { LoadedSceneInfo currentInfo; if (contractMap.TryGetValue(contractName, out currentInfo)) { throw Assert.CreateException( "Found multiple scene contracts with name '{0}'. Scene '{1}' and scene '{2}'", contractName, currentInfo.Scene.name, info.Scene.name); } contractMap.Add(contractName, info); } } public class LoadedSceneInfo { public SceneContext SceneContext; public SceneDecoratorContext DecoratorContext; public Scene Scene; } } }