From 2000eb5c91b4575034cc25e0b16f9fbc32a477ab Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 30 Apr 2026 15:03:15 +0100 Subject: [PATCH] Add Epiphany Unity editor bridge --- Assets/Editor.meta | 8 + Assets/Editor/Epiphany.meta | 8 + .../Editor/Epiphany/EpiphanyEditorBridge.cs | 1067 +++++++++++++++++ .../Epiphany/EpiphanyEditorBridge.cs.meta | 11 + 4 files changed, 1094 insertions(+) create mode 100644 Assets/Editor.meta create mode 100644 Assets/Editor/Epiphany.meta create mode 100644 Assets/Editor/Epiphany/EpiphanyEditorBridge.cs create mode 100644 Assets/Editor/Epiphany/EpiphanyEditorBridge.cs.meta diff --git a/Assets/Editor.meta b/Assets/Editor.meta new file mode 100644 index 000000000..05a765169 --- /dev/null +++ b/Assets/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0f35c6fb8d5f4f709d1fdbe54aeb5cc9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/Epiphany.meta b/Assets/Editor/Epiphany.meta new file mode 100644 index 000000000..5c81b87b5 --- /dev/null +++ b/Assets/Editor/Epiphany.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5e8f057c878e4b66a1e6274d7b3b47a9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/Epiphany/EpiphanyEditorBridge.cs b/Assets/Editor/Epiphany/EpiphanyEditorBridge.cs new file mode 100644 index 000000000..fa7d554f8 --- /dev/null +++ b/Assets/Editor/Epiphany/EpiphanyEditorBridge.cs @@ -0,0 +1,1067 @@ +#if UNITY_EDITOR +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace GameCult.Epiphany.Unity +{ + public static class EpiphanyEditorBridge + { + private const int DefaultMaxObjects = 600; + private const int DefaultMaxProperties = 100; + private const int DefaultMaxDependencies = 2000; + private const string ArtifactDirArg = "-epiphanyArtifactDir"; + private const string OperationArg = "-epiphanyOperation"; + private const string SceneArg = "-epiphanyScene"; + private const string AssetArg = "-epiphanyAsset"; + private const string GuidArg = "-epiphanyGuid"; + private const string MaxObjectsArg = "-epiphanyMaxObjects"; + private const string MaxPropertiesArg = "-epiphanyMaxProperties"; + private const string MaxDependenciesArg = "-epiphanyMaxDependencies"; + + private static string currentArtifactDir; + + [MenuItem("Epiphany/Inspect Project")] + public static void InspectProject() + { + RunCommand("inspect-project", CaptureProjectFacts); + } + + [MenuItem("Epiphany/Check Compilation")] + public static void CheckCompilation() + { + RunCommand("check-compilation", CaptureCompilationFacts); + } + + [MenuItem("Epiphany/Capture Scene Facts")] + public static void CaptureSceneFacts() + { + RunCommand("scene-facts", delegate { return CaptureSceneFactsPayload(GetScenePathArg()); }); + } + + [MenuItem("Epiphany/Capture Prefab Facts")] + public static void CapturePrefabFacts() + { + RunCommand("prefab-facts", delegate { return CapturePrefabFactsPayload(GetAssetPathArg()); }); + } + + [MenuItem("Epiphany/Capture Serialized Object Facts")] + public static void CaptureSerializedObjectFacts() + { + RunCommand("serialized-object", delegate { return CaptureSerializedObjectFactsPayload(GetAssetPathArg()); }); + } + + [MenuItem("Epiphany/Reference Search")] + public static void ReferenceSearch() + { + RunCommand("reference-search", CaptureReferenceSearchPayload); + } + + public static void RunProbe() + { + string operation = GetArg(OperationArg, "inspect-project"); + switch (operation) + { + case "inspect-project": + InspectProject(); + break; + case "check-compilation": + CheckCompilation(); + break; + case "scene-facts": + CaptureSceneFacts(); + break; + case "prefab-facts": + CapturePrefabFacts(); + break; + case "serialized-object": + CaptureSerializedObjectFacts(); + break; + case "reference-search": + ReferenceSearch(); + break; + default: + RunCommand(operation, delegate + { + Dictionary payload = BaseResult("unityProbeResult", operation); + payload["status"] = "blocked"; + payload["evidenceSummary"] = "Unknown Epiphany Unity probe operation."; + payload["knownOperations"] = new List + { + "inspect-project", + "check-compilation", + "scene-facts", + "prefab-facts", + "serialized-object", + "reference-search" + }; + return payload; + }); + EditorApplication.Exit(2); + break; + } + } + + private static void RunCommand(string operation, Func> buildPayload) + { + string artifactDir = ResolveArtifactDir(operation); + currentArtifactDir = artifactDir; + Directory.CreateDirectory(artifactDir); + + Dictionary payload; + try + { + payload = buildPayload(); + if (!payload.ContainsKey("operation")) + { + payload["operation"] = operation; + } + if (!payload.ContainsKey("status")) + { + payload["status"] = "passed"; + } + } + catch (Exception error) + { + payload = BaseResult("unityProbeResult", operation); + payload["status"] = "failed"; + payload["error"] = error.ToString(); + payload["evidenceSummary"] = "The Epiphany Unity editor bridge failed while gathering editor facts."; + WriteArtifacts(artifactDir, operation, payload); + Debug.LogError(error); + EditorApplication.Exit(1); + return; + } + + WriteArtifacts(artifactDir, operation, payload); + if (StringEquals(payload, "status", "failed")) + { + EditorApplication.Exit(1); + } + } + + private static Dictionary CaptureProjectFacts() + { + Dictionary payload = BaseResult("unityProjectInspection", "inspect-project"); + payload["status"] = "passed"; + payload["unityVersion"] = Application.unityVersion; + payload["projectPath"] = ProjectRoot(); + payload["dataPath"] = Application.dataPath; + payload["isBatchMode"] = Application.isBatchMode; + payload["isCompiling"] = EditorApplication.isCompiling; + payload["isUpdating"] = EditorApplication.isUpdating; + payload["activeScene"] = SceneSummary(SceneManager.GetActiveScene()); + payload["buildScenes"] = BuildScenes(); + payload["renderPipeline"] = RenderPipelineSummary(); + payload["assetDatabase"] = AssetDatabaseSummary(); + payload["evidenceSummary"] = "Unity editor project facts were captured from inside the pinned editor."; + WriteNamedJson("project-facts.json", payload); + return payload; + } + + private static Dictionary CaptureCompilationFacts() + { + Dictionary payload = BaseResult("unityCompilationProbe", "check-compilation"); + bool compiling = EditorApplication.isCompiling; + bool updating = EditorApplication.isUpdating; + payload["status"] = compiling || updating ? "pending" : "passed"; + payload["isCompiling"] = compiling; + payload["isUpdating"] = updating; + payload["unityVersion"] = Application.unityVersion; + payload["loadedAssemblies"] = LoadedAssemblySummary(); + payload["evidenceSummary"] = compiling || updating + ? "The editor bridge ran, but Unity reports compilation or asset update still in progress." + : "The editor bridge executed inside Unity, which means editor scripts compiled far enough for probes to run."; + WriteNamedJson("compilation.json", payload); + return payload; + } + + private static Dictionary CaptureSceneFactsPayload(string scenePath) + { + if (string.IsNullOrEmpty(scenePath)) + { + scenePath = DefaultScenePath(); + } + if (string.IsNullOrEmpty(scenePath)) + { + throw new InvalidOperationException("No scene path was supplied and no default build scene exists."); + } + + Scene scene = SceneManager.GetActiveScene(); + if (!string.Equals(scene.path, scenePath, StringComparison.OrdinalIgnoreCase)) + { + scene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single); + } + + Dictionary payload = BaseResult("unitySceneFacts", "scene-facts"); + payload["status"] = "passed"; + payload["scene"] = SceneSummary(scene); + payload["rootCount"] = scene.rootCount; + payload["isDirty"] = scene.isDirty; + + int maxObjects = GetIntArg(MaxObjectsArg, DefaultMaxObjects); + List objects = new List(); + int totalObjects = 0; + int missingScripts = 0; + foreach (GameObject root in scene.GetRootGameObjects()) + { + WalkGameObject(root, objects, ref totalObjects, ref missingScripts, maxObjects); + } + payload["totalGameObjectCount"] = totalObjects; + payload["capturedGameObjectCount"] = objects.Count; + payload["truncated"] = totalObjects > objects.Count; + payload["missingScriptCount"] = missingScripts; + payload["gameObjects"] = objects; + payload["evidenceSummary"] = "Scene hierarchy, components, prefab links, and serialized fields were captured inside Unity."; + WriteNamedJson("scene-facts.json", payload); + return payload; + } + + private static Dictionary CapturePrefabFactsPayload(string assetPath) + { + if (string.IsNullOrEmpty(assetPath)) + { + throw new InvalidOperationException("Prefab facts require -epiphanyAsset Assets/.../Prefab.prefab."); + } + GameObject root = PrefabUtility.LoadPrefabContents(assetPath); + try + { + Dictionary payload = BaseResult("unityPrefabFacts", "prefab-facts"); + payload["status"] = "passed"; + payload["assetPath"] = assetPath; + payload["guid"] = AssetDatabase.AssetPathToGUID(assetPath); + payload["prefabAssetType"] = PrefabUtility.GetPrefabAssetType(root).ToString(); + payload["root"] = ObjectReferenceSummary(root); + + int maxObjects = GetIntArg(MaxObjectsArg, DefaultMaxObjects); + List objects = new List(); + int totalObjects = 0; + int missingScripts = 0; + WalkGameObject(root, objects, ref totalObjects, ref missingScripts, maxObjects); + payload["totalGameObjectCount"] = totalObjects; + payload["capturedGameObjectCount"] = objects.Count; + payload["truncated"] = totalObjects > objects.Count; + payload["missingScriptCount"] = missingScripts; + payload["gameObjects"] = objects; + payload["evidenceSummary"] = "Prefab hierarchy, nested instances, overrides, components, and serialized fields were captured inside Unity."; + WriteNamedJson("prefab-facts.json", payload); + return payload; + } + finally + { + PrefabUtility.UnloadPrefabContents(root); + } + } + + private static Dictionary CaptureSerializedObjectFactsPayload(string assetPath) + { + if (string.IsNullOrEmpty(assetPath)) + { + throw new InvalidOperationException("Serialized object facts require -epiphanyAsset Assets/... path."); + } + + UnityEngine.Object[] assets = AssetDatabase.LoadAllAssetsAtPath(assetPath); + Dictionary payload = BaseResult("unitySerializedObjectFacts", "serialized-object"); + payload["status"] = "passed"; + payload["assetPath"] = assetPath; + payload["guid"] = AssetDatabase.AssetPathToGUID(assetPath); + payload["assetCount"] = assets == null ? 0 : assets.Length; + + List targets = new List(); + if (assets != null) + { + foreach (UnityEngine.Object asset in assets) + { + if (asset == null) + { + continue; + } + targets.Add(SerializedObjectSummary(asset)); + } + } + + payload["targets"] = targets; + payload["evidenceSummary"] = "Serialized Unity object properties were captured through SerializedObject inside the editor."; + WriteNamedJson("serialized-object-facts.json", payload); + return payload; + } + + private static Dictionary CaptureReferenceSearchPayload() + { + string assetPath = GetArg(AssetArg, null); + string guid = GetArg(GuidArg, null); + if (string.IsNullOrEmpty(guid) && !string.IsNullOrEmpty(assetPath)) + { + guid = AssetDatabase.AssetPathToGUID(assetPath); + } + if (string.IsNullOrEmpty(guid)) + { + throw new InvalidOperationException("Reference search requires -epiphanyGuid or -epiphanyAsset."); + } + + string targetPath = AssetDatabase.GUIDToAssetPath(guid); + int maxDependencies = GetIntArg(MaxDependenciesArg, DefaultMaxDependencies); + Dictionary payload = BaseResult("unityReferenceSearch", "reference-search"); + payload["status"] = "passed"; + payload["targetGuid"] = guid; + payload["targetPath"] = targetPath; + + List matches = new List(); + int scanned = 0; + string[] paths = AssetDatabase.GetAllAssetPaths(); + foreach (string path in paths) + { + if (!path.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + scanned++; + string[] dependencies = AssetDatabase.GetDependencies(path, true); + bool found = false; + foreach (string dependency in dependencies) + { + if (string.Equals(dependency, targetPath, StringComparison.OrdinalIgnoreCase)) + { + found = true; + break; + } + } + if (!found) + { + continue; + } + Dictionary match = new Dictionary(); + match["path"] = path; + match["guid"] = AssetDatabase.AssetPathToGUID(path); + match["type"] = AssetTypeName(path); + matches.Add(match); + if (matches.Count >= maxDependencies) + { + break; + } + } + + payload["scannedAssetCount"] = scanned; + payload["matchCount"] = matches.Count; + payload["truncated"] = matches.Count >= maxDependencies; + payload["matches"] = matches; + payload["evidenceSummary"] = "Unity asset dependencies were searched through AssetDatabase."; + WriteNamedJson("reference-search.json", payload); + return payload; + } + + private static void WalkGameObject(GameObject gameObject, List output, ref int totalObjects, ref int missingScripts, int maxObjects) + { + totalObjects++; + if (output.Count < maxObjects) + { + output.Add(GameObjectSummary(gameObject, ref missingScripts)); + } + else + { + Component[] missingCountOnly = gameObject.GetComponents(); + foreach (Component component in missingCountOnly) + { + if (component == null) + { + missingScripts++; + } + } + } + + Transform transform = gameObject.transform; + for (int i = 0; i < transform.childCount; i++) + { + WalkGameObject(transform.GetChild(i).gameObject, output, ref totalObjects, ref missingScripts, maxObjects); + } + } + + private static Dictionary GameObjectSummary(GameObject gameObject, ref int missingScripts) + { + Dictionary summary = new Dictionary(); + summary["name"] = gameObject.name; + summary["path"] = GameObjectPath(gameObject); + summary["activeSelf"] = gameObject.activeSelf; + summary["activeInHierarchy"] = gameObject.activeInHierarchy; + summary["tag"] = gameObject.tag; + summary["layer"] = gameObject.layer; + summary["scenePath"] = gameObject.scene.path; + summary["globalObjectId"] = GlobalId(gameObject); + summary["prefab"] = PrefabSummary(gameObject); + + List components = new List(); + Component[] componentArray = gameObject.GetComponents(); + foreach (Component component in componentArray) + { + if (component == null) + { + missingScripts++; + Dictionary missing = new Dictionary(); + missing["missing"] = true; + components.Add(missing); + continue; + } + components.Add(ComponentSummary(component)); + } + summary["components"] = components; + return summary; + } + + private static Dictionary ComponentSummary(Component component) + { + Dictionary summary = ObjectReferenceSummary(component); + Behaviour behaviour = component as Behaviour; + if (behaviour != null) + { + summary["enabled"] = behaviour.enabled; + } + Renderer renderer = component as Renderer; + if (renderer != null) + { + List materials = new List(); + foreach (Material material in renderer.sharedMaterials) + { + materials.Add(ObjectReferenceSummary(material)); + } + summary["sharedMaterials"] = materials; + } + summary["serializedProperties"] = SerializedProperties(component, GetIntArg(MaxPropertiesArg, DefaultMaxProperties)); + return summary; + } + + private static Dictionary SerializedObjectSummary(UnityEngine.Object target) + { + Dictionary summary = ObjectReferenceSummary(target); + summary["serializedProperties"] = SerializedProperties(target, GetIntArg(MaxPropertiesArg, DefaultMaxProperties)); + + GameObject gameObject = target as GameObject; + if (gameObject != null) + { + int missing = 0; + summary["gameObject"] = GameObjectSummary(gameObject, ref missing); + summary["missingScriptCount"] = missing; + } + + return summary; + } + + private static List SerializedProperties(UnityEngine.Object target, int maxProperties) + { + List properties = new List(); + if (target == null) + { + return properties; + } + + try + { + SerializedObject serializedObject = new SerializedObject(target); + SerializedProperty iterator = serializedObject.GetIterator(); + bool enterChildren = true; + int count = 0; + while (iterator.NextVisible(enterChildren)) + { + enterChildren = false; + properties.Add(SerializedPropertySummary(iterator)); + count++; + if (count >= maxProperties) + { + Dictionary truncated = new Dictionary(); + truncated["propertyPath"] = ""; + truncated["reason"] = "max property limit reached"; + truncated["maxProperties"] = maxProperties; + properties.Add(truncated); + break; + } + } + } + catch (Exception error) + { + Dictionary failed = new Dictionary(); + failed["propertyPath"] = ""; + failed["error"] = error.Message; + properties.Add(failed); + } + return properties; + } + + private static Dictionary SerializedPropertySummary(SerializedProperty property) + { + Dictionary summary = new Dictionary(); + summary["propertyPath"] = property.propertyPath; + summary["displayName"] = property.displayName; + summary["type"] = property.propertyType.ToString(); + summary["isArray"] = property.isArray; + summary["hasVisibleChildren"] = property.hasVisibleChildren; + summary["value"] = SerializedPropertyValue(property); + return summary; + } + + private static object SerializedPropertyValue(SerializedProperty property) + { + try + { + switch (property.propertyType) + { + case SerializedPropertyType.Integer: + return property.longValue; + case SerializedPropertyType.Boolean: + return property.boolValue; + case SerializedPropertyType.Float: + return property.doubleValue; + case SerializedPropertyType.String: + return property.stringValue; + case SerializedPropertyType.Color: + return ColorSummary(property.colorValue); + case SerializedPropertyType.ObjectReference: + return ObjectReferenceSummary(property.objectReferenceValue); + case SerializedPropertyType.LayerMask: + return property.intValue; + case SerializedPropertyType.Enum: + return property.enumDisplayNames != null && property.enumValueIndex >= 0 && property.enumValueIndex < property.enumDisplayNames.Length + ? property.enumDisplayNames[property.enumValueIndex] + : property.enumValueIndex.ToString(CultureInfo.InvariantCulture); + case SerializedPropertyType.Vector2: + return VectorSummary(property.vector2Value); + case SerializedPropertyType.Vector3: + return VectorSummary(property.vector3Value); + case SerializedPropertyType.Vector4: + return VectorSummary(property.vector4Value); + case SerializedPropertyType.Rect: + return RectSummary(property.rectValue); + case SerializedPropertyType.ArraySize: + return property.intValue; + case SerializedPropertyType.Character: + return property.intValue; + case SerializedPropertyType.Bounds: + return BoundsSummary(property.boundsValue); + case SerializedPropertyType.Quaternion: + return QuaternionSummary(property.quaternionValue); + default: + Dictionary unsupported = new Dictionary(); + unsupported["unsupported"] = true; + unsupported["propertyType"] = property.propertyType.ToString(); + return unsupported; + } + } + catch (Exception error) + { + Dictionary failed = new Dictionary(); + failed["error"] = error.Message; + return failed; + } + } + + private static Dictionary PrefabSummary(GameObject gameObject) + { + Dictionary summary = new Dictionary(); + summary["assetType"] = PrefabUtility.GetPrefabAssetType(gameObject).ToString(); + summary["instanceStatus"] = PrefabUtility.GetPrefabInstanceStatus(gameObject).ToString(); + UnityEngine.Object source = PrefabUtility.GetCorrespondingObjectFromSource(gameObject); + summary["source"] = ObjectReferenceSummary(source); + summary["nearestInstanceRoot"] = ObjectReferenceSummary(PrefabUtility.GetNearestPrefabInstanceRoot(gameObject)); + return summary; + } + + private static Dictionary ObjectReferenceSummary(UnityEngine.Object value) + { + Dictionary summary = new Dictionary(); + if (value == null) + { + summary["isNull"] = true; + return summary; + } + + summary["isNull"] = false; + summary["name"] = value.name; + summary["type"] = value.GetType().FullName; + summary["globalObjectId"] = GlobalId(value); + + string path = AssetDatabase.GetAssetPath(value); + if (!string.IsNullOrEmpty(path)) + { + summary["assetPath"] = path; + summary["guid"] = AssetDatabase.AssetPathToGUID(path); + } + + GameObject gameObject = value as GameObject; + if (gameObject != null) + { + summary["scenePath"] = gameObject.scene.path; + summary["gameObjectPath"] = GameObjectPath(gameObject); + } + + Component component = value as Component; + if (component != null) + { + summary["scenePath"] = component.gameObject.scene.path; + summary["gameObjectPath"] = GameObjectPath(component.gameObject); + } + + return summary; + } + + private static Dictionary SceneSummary(Scene scene) + { + Dictionary summary = new Dictionary(); + summary["name"] = scene.name; + summary["path"] = scene.path; + summary["isLoaded"] = scene.isLoaded; + summary["isDirty"] = scene.isDirty; + summary["rootCount"] = scene.rootCount; + summary["buildIndex"] = scene.buildIndex; + return summary; + } + + private static List BuildScenes() + { + List scenes = new List(); + foreach (EditorBuildSettingsScene scene in EditorBuildSettings.scenes) + { + Dictionary item = new Dictionary(); + item["path"] = scene.path; + item["guid"] = scene.guid.ToString(); + item["enabled"] = scene.enabled; + scenes.Add(item); + } + return scenes; + } + + private static Dictionary RenderPipelineSummary() + { + Dictionary summary = new Dictionary(); + UnityEngine.Rendering.RenderPipelineAsset pipeline = UnityEngine.Rendering.GraphicsSettings.defaultRenderPipeline; + summary["defaultRenderPipeline"] = ObjectReferenceSummary(pipeline); + return summary; + } + + private static Dictionary AssetDatabaseSummary() + { + Dictionary summary = new Dictionary(); + summary["globalArtifactDependencyVersion"] = AssetDatabase.GlobalArtifactDependencyVersion; + summary["globalArtifactProcessedVersion"] = AssetDatabase.GlobalArtifactProcessedVersion; + summary["activeRefreshImportMode"] = AssetDatabase.ActiveRefreshImportMode.ToString(); + return summary; + } + + private static List LoadedAssemblySummary() + { + List assemblies = new List(); + foreach (System.Reflection.Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + string name = assembly.GetName().Name; + if (string.IsNullOrEmpty(name)) + { + continue; + } + if (!name.StartsWith("Assembly-CSharp", StringComparison.OrdinalIgnoreCase) + && !name.StartsWith("Aetheria", StringComparison.OrdinalIgnoreCase) + && !name.StartsWith("Cult", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + Dictionary item = new Dictionary(); + item["name"] = name; + item["fullName"] = assembly.FullName; + assemblies.Add(item); + } + return assemblies; + } + + private static string DefaultScenePath() + { + Scene active = SceneManager.GetActiveScene(); + if (!string.IsNullOrEmpty(active.path)) + { + return active.path; + } + foreach (EditorBuildSettingsScene scene in EditorBuildSettings.scenes) + { + if (scene.enabled && !string.IsNullOrEmpty(scene.path)) + { + return scene.path; + } + } + return null; + } + + private static string GetScenePathArg() + { + return NormalizeAssetPath(GetArg(SceneArg, null)); + } + + private static string GetAssetPathArg() + { + return NormalizeAssetPath(GetArg(AssetArg, null)); + } + + private static string NormalizeAssetPath(string value) + { + if (string.IsNullOrEmpty(value)) + { + return value; + } + return value.Replace("\\", "/"); + } + + private static string AssetTypeName(string path) + { + Type type = AssetDatabase.GetMainAssetTypeAtPath(path); + return type == null ? null : type.FullName; + } + + private static string GameObjectPath(GameObject gameObject) + { + if (gameObject == null) + { + return null; + } + List names = new List(); + Transform current = gameObject.transform; + while (current != null) + { + names.Add(current.name); + current = current.parent; + } + names.Reverse(); + return string.Join("/", names.ToArray()); + } + + private static string GlobalId(UnityEngine.Object unityObject) + { + if (unityObject == null) + { + return null; + } + try + { + return GlobalObjectId.GetGlobalObjectIdSlow(unityObject).ToString(); + } + catch + { + return null; + } + } + + private static Dictionary BaseResult(string kind, string operation) + { + Dictionary payload = new Dictionary(); + payload["kind"] = kind; + payload["operation"] = operation; + payload["generatedAt"] = DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture); + payload["projectPath"] = ProjectRoot(); + payload["unityVersion"] = Application.unityVersion; + payload["isBatchMode"] = Application.isBatchMode; + return payload; + } + + private static string ProjectRoot() + { + return Directory.GetParent(Application.dataPath).FullName.Replace("\\", "/"); + } + + private static string ResolveArtifactDir(string operation) + { + string fromArgs = GetArg(ArtifactDirArg, null); + if (!string.IsNullOrEmpty(fromArgs)) + { + return Path.GetFullPath(fromArgs); + } + string root = Path.Combine(ProjectRoot(), "EpiphanyArtifacts"); + string stamp = DateTime.UtcNow.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture); + return Path.Combine(root, operation + "-" + stamp); + } + + private static void WriteArtifacts(string artifactDir, string operation, Dictionary payload) + { + Directory.CreateDirectory(artifactDir); + payload["artifactDir"] = artifactDir.Replace("\\", "/"); + + string resultPath = Path.Combine(artifactDir, "unity-probe-result.json"); + File.WriteAllText(resultPath, MiniJson.Serialize(payload), Encoding.UTF8); + + string markdownPath = Path.Combine(artifactDir, "unity-probe-result.md"); + File.WriteAllText(markdownPath, RenderMarkdown(payload), Encoding.UTF8); + Debug.Log("Epiphany Unity bridge wrote " + operation + " artifacts to " + artifactDir); + } + + private static void WriteNamedJson(string fileName, Dictionary payload) + { + string artifactDir = string.IsNullOrEmpty(currentArtifactDir) + ? ResolveArtifactDir(payload.ContainsKey("operation") ? Convert.ToString(payload["operation"]) : "probe") + : currentArtifactDir; + WriteNamedJsonFile(artifactDir, fileName, payload); + } + + private static string RenderMarkdown(Dictionary payload) + { + StringBuilder builder = new StringBuilder(); + builder.AppendLine("# Epiphany Unity Probe Result"); + builder.AppendLine(); + AppendMarkdownValue(builder, "Kind", payload, "kind"); + AppendMarkdownValue(builder, "Operation", payload, "operation"); + AppendMarkdownValue(builder, "Status", payload, "status"); + AppendMarkdownValue(builder, "Unity", payload, "unityVersion"); + AppendMarkdownValue(builder, "Project", payload, "projectPath"); + AppendMarkdownValue(builder, "Generated", payload, "generatedAt"); + AppendMarkdownValue(builder, "Summary", payload, "evidenceSummary"); + builder.AppendLine(); + builder.AppendLine("Raw facts are in `unity-probe-result.json` and operation-specific JSON files when available."); + return builder.ToString(); + } + + private static void AppendMarkdownValue(StringBuilder builder, string label, Dictionary payload, string key) + { + object value; + if (payload.TryGetValue(key, out value) && value != null) + { + builder.Append(label).Append(": ").AppendLine(Convert.ToString(value, CultureInfo.InvariantCulture)); + } + } + + private static void WriteNamedJsonFile(string artifactDir, string fileName, Dictionary payload) + { + Directory.CreateDirectory(artifactDir); + File.WriteAllText(Path.Combine(artifactDir, fileName), MiniJson.Serialize(payload), Encoding.UTF8); + } + + private static Dictionary ColorSummary(Color value) + { + Dictionary summary = new Dictionary(); + summary["r"] = value.r; + summary["g"] = value.g; + summary["b"] = value.b; + summary["a"] = value.a; + return summary; + } + + private static Dictionary VectorSummary(Vector2 value) + { + Dictionary summary = new Dictionary(); + summary["x"] = value.x; + summary["y"] = value.y; + return summary; + } + + private static Dictionary VectorSummary(Vector3 value) + { + Dictionary summary = VectorSummary((Vector2)value); + summary["z"] = value.z; + return summary; + } + + private static Dictionary VectorSummary(Vector4 value) + { + Dictionary summary = new Dictionary(); + summary["x"] = value.x; + summary["y"] = value.y; + summary["z"] = value.z; + summary["w"] = value.w; + return summary; + } + + private static Dictionary RectSummary(Rect value) + { + Dictionary summary = new Dictionary(); + summary["x"] = value.x; + summary["y"] = value.y; + summary["width"] = value.width; + summary["height"] = value.height; + return summary; + } + + private static Dictionary BoundsSummary(Bounds value) + { + Dictionary summary = new Dictionary(); + summary["center"] = VectorSummary(value.center); + summary["size"] = VectorSummary(value.size); + return summary; + } + + private static Dictionary QuaternionSummary(Quaternion value) + { + Dictionary summary = new Dictionary(); + summary["x"] = value.x; + summary["y"] = value.y; + summary["z"] = value.z; + summary["w"] = value.w; + return summary; + } + + private static bool StringEquals(Dictionary payload, string key, string expected) + { + object value; + return payload.TryGetValue(key, out value) + && string.Equals(Convert.ToString(value), expected, StringComparison.OrdinalIgnoreCase); + } + + private static string GetArg(string name, string fallback) + { + string[] args = Environment.GetCommandLineArgs(); + for (int i = 0; i < args.Length - 1; i++) + { + if (string.Equals(args[i], name, StringComparison.OrdinalIgnoreCase)) + { + return args[i + 1]; + } + } + return fallback; + } + + private static int GetIntArg(string name, int fallback) + { + string raw = GetArg(name, null); + int value; + return int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out value) ? value : fallback; + } + + private static class MiniJson + { + public static string Serialize(object value) + { + StringBuilder builder = new StringBuilder(); + WriteValue(builder, value); + builder.AppendLine(); + return builder.ToString(); + } + + private static void WriteValue(StringBuilder builder, object value) + { + if (value == null) + { + builder.Append("null"); + return; + } + string asString = value as string; + if (asString != null) + { + WriteString(builder, asString); + return; + } + if (value is bool) + { + builder.Append((bool)value ? "true" : "false"); + return; + } + if (IsNumber(value)) + { + builder.Append(Convert.ToString(value, CultureInfo.InvariantCulture)); + return; + } + IDictionary dictionary = value as IDictionary; + if (dictionary != null) + { + WriteObject(builder, dictionary); + return; + } + IEnumerable enumerable = value as IEnumerable; + if (enumerable != null) + { + WriteArray(builder, enumerable); + return; + } + WriteString(builder, Convert.ToString(value, CultureInfo.InvariantCulture)); + } + + private static void WriteObject(StringBuilder builder, IDictionary dictionary) + { + builder.Append("{"); + bool first = true; + foreach (DictionaryEntry entry in dictionary) + { + if (!first) + { + builder.Append(","); + } + first = false; + WriteString(builder, Convert.ToString(entry.Key, CultureInfo.InvariantCulture)); + builder.Append(":"); + WriteValue(builder, entry.Value); + } + builder.Append("}"); + } + + private static void WriteArray(StringBuilder builder, IEnumerable enumerable) + { + builder.Append("["); + bool first = true; + foreach (object item in enumerable) + { + if (!first) + { + builder.Append(","); + } + first = false; + WriteValue(builder, item); + } + builder.Append("]"); + } + + private static void WriteString(StringBuilder builder, string value) + { + builder.Append('"'); + foreach (char character in value) + { + switch (character) + { + case '"': + builder.Append("\\\""); + break; + case '\\': + builder.Append("\\\\"); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + if (character < 32) + { + builder.Append("\\u").Append(((int)character).ToString("x4", CultureInfo.InvariantCulture)); + } + else + { + builder.Append(character); + } + break; + } + } + builder.Append('"'); + } + + private static bool IsNumber(object value) + { + return value is byte + || value is sbyte + || value is short + || value is ushort + || value is int + || value is uint + || value is long + || value is ulong + || value is float + || value is double + || value is decimal; + } + } + } +} +#endif diff --git a/Assets/Editor/Epiphany/EpiphanyEditorBridge.cs.meta b/Assets/Editor/Epiphany/EpiphanyEditorBridge.cs.meta new file mode 100644 index 000000000..a829f1eac --- /dev/null +++ b/Assets/Editor/Epiphany/EpiphanyEditorBridge.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7b87b11639ab4ec29f10a88582f0d381 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: