using System; using System.Collections.Generic; using System.IO; using System.Reflection; using System.Text.RegularExpressions; using Needle.Engine.Deployment; using UnityEditor; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.SceneManagement; using Object = UnityEngine.Object; namespace Needle.Engine.Samples.Helpers { public static class SampleUpdater { internal static void PatchActiveScene() { try { RemoveKnownMissingComponents(); UpgradeLightValues(); CleanDeployToComponents(); } catch (Exception e) { Debug.LogError($"Error when patching scene \"{SceneManager.GetActiveScene().name}\": {e}"); } } private static Dictionary knownComponentGuids = new Dictionary() { { "474bcb49853aa07438625e644c072ee6", "UniversalAdditionalLightData" }, { "a79441f348de89743a2939f4d699eac1", "UniversalAdditionalCameraData" }, }; private static MethodInfo _CreateMissingReferenceObject; private static FieldInfo m_InstanceId; private static Object CreateMissingReferenceObject(int instanceId) { #if UNITY_2022_1_OR_NEWER if (_CreateMissingReferenceObject == null) _CreateMissingReferenceObject = typeof(Object).GetMethod("CreateMissingReferenceObject", (BindingFlags)(-1)); return _CreateMissingReferenceObject.Invoke(null, new object[] { instanceId }) as Object; #else if (m_InstanceId == null) m_InstanceId = typeof(Object).GetField("m_InstanceID", (BindingFlags)(-1)); var obj = new Object(); if (m_InstanceId != null) m_InstanceId.SetValue(obj, instanceId); return obj; #endif } private static void RemoveKnownMissingComponents() { // traverse the scene and remove missing components on Cameras and Lights, under the assumption that it's // URPAdditionalLightData or URPAdditionalCameraData var objects = Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); var missingComponents = new List<(GameObject obj, Object missingReferenceObject, long localID, string guid)>(); foreach (var obj in objects) { if (GameObjectUtility.GetMonoBehavioursWithMissingScriptCount(obj) <= 0) continue; var so = new SerializedObject(obj); var components = so.FindProperty("m_Component"); var arrayLength = components.arraySize; for (var i = arrayLength - 1; i >= 0; i--) { var component = components.GetArrayElementAtIndex(i).FindPropertyRelative("component"); if (component.objectReferenceValue) continue; var instanceId = component.objectReferenceInstanceIDValue; var missingReferenceObject = CreateMissingReferenceObject(instanceId); var localID = GetLocalID(missingReferenceObject); missingComponents.Add((obj, missingReferenceObject, localID, null)); } } // We need to manually parse the scene file to find the GUID of the missing scripts... no API for that var sceneFile = SceneManager.GetActiveScene().path; var componentLocalIdToGuid = new Dictionary(); void CollectGuidsFromFile(string assetDatabasePath) { // File types we need to check as they contain scene data and components: // .unity - scene file // .prefab - prefab file // .asset - scriptable object file var type = Path.GetExtension(assetDatabasePath); if (type != ".unity" && type != ".prefab" && type != ".asset") return; var allText = File.ReadAllText(assetDatabasePath); allText = allText.Replace("\r\n", "\n"); var regexForAllComponents = new Regex(@"--- !u!114 &([0-9]+)\nMonoBehaviour:\n[\s\S]*? m_GameObject: {fileID: ([0-9]+)}\n[\s\S]*? m_Script: {fileID: 11500000, guid: ([a-f0-9]+), type: \d}", RegexOptions.Multiline); var allMatches = regexForAllComponents.Matches(allText); for (var i = 0; i < allMatches.Count; i++) { var match = allMatches[i]; var localID = long.Parse(match.Groups[1].Value); var guid = match.Groups[3].Value; // Debug.Log("Found component with localID " + localID + " and gameObjectID " + gameObjectID + " and GUID " + guid); componentLocalIdToGuid[localID] = guid; } } // as references can also be inside Prefabs, we need to check all dependencies var dependencies = AssetDatabase.GetDependencies(sceneFile, true); foreach (var dependency in dependencies) CollectGuidsFromFile(dependency); // At this point, we have // - a list of all missing localIDs, which gameObject localID they belong to, and what their script GUID is // - a list of MissingComponentObjects for (var i = 0; i < missingComponents.Count; i++) { var missing = missingComponents[i]; var componentLocalID = GetLocalID(missing.missingReferenceObject); if (componentLocalIdToGuid.TryGetValue(componentLocalID, out var guid)) { // Debug.Log("Component " + componentLocalID + " is missing in " + missing.obj + ", has guid: " + guid); // We know the GUID now, so we can assign it here missing.guid = guid; missingComponents[i] = missing; if (knownComponentGuids.ContainsKey(guid)) { var componentString = knownComponentGuids.TryGetValue(guid, out var name) ? name : "GUID: " + guid; NeedleDebug.Log(TracingScenario.Samples, $"Removing missing component {componentString} from {missing.obj}", missing.obj); // We can destroy the wrapped missing reference object, and it will delete the underlying missing component Object.DestroyImmediate(missing.missingReferenceObject); if (missing.missingReferenceObject) { Debug.LogError("Couldn't remove missing component from " + missing.obj + ". Please report a bug to Needle.", missing.obj); } } else { NeedleDebug.LogWarning(TracingScenario.Samples, $"Found unknown missing component with GUID {guid} on {missing.obj}", missing.obj); } } else { NeedleDebug.LogWarning(TracingScenario.Samples, $"Found missing component with unknown GUID on {missing.obj}; LocalID: {missing.localID}", missing.obj); } } } /// /// All Sample scenes are authored with these modern settings: /// /// QualitySettings.activeColorSpace == ColorSpace.Linear /// GraphicsSettings.lightsUseLinearIntensity == true /// GraphicsSettings.lightsUseColorTemperature == true /// /// This method allows us to patch the freshly copied scene to match the current scene settings. /// private static void UpgradeLightValues() { var needsLightValueUpgrade = GraphicsSettings.lightsUseLinearIntensity == false; if (!needsLightValueUpgrade) return; var lights = Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); // TODO implement linear <> non-linear conversion and URP <> BiRP light intensity conversion } private static void CleanDeployToComponents() { var deployToFtpComponents = Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); foreach (var component in deployToFtpComponents) { if (!component) continue; // This reference lives in Needle test projects, so won't be missing during tests, but // will be missing when installing the actual samples. component.FTPServer = null; } } private static long GetLocalID(Object obj) { // This gives us a format like this: GlobalObjectId_V1-2-385fb38372a85417096e20d059988fc5-212764579-0 // which contains the GUID of the scene file and the LocalID of the missing component. // Turns out that this data is different to AssetDatabase.TryGetGUIDAndLocalFileIdentifier! var globalIdentifier = GlobalObjectId.GetGlobalObjectIdSlow(obj); return (long) globalIdentifier.targetObjectId; } } }