using System.IO; using UnityEngine; using UnityEditor; using System.Linq; using System.Text; using System.Reflection; using System.Collections.Generic; using IntelligentProjectAnalyzer.Editor.Graphing; namespace LLM.Editor.Analysis { /// /// Responsible for gathering and serializing context about staged assets /// into a detailed, human-readable, and referential format for the LLM. /// public static class ContextBuilder { /// /// Builds a detailed Tier 1 summary of the provided objects, including a pruned hierarchy for GameObjects. /// /// The list of objects from the UI's staging area. /// A formatted string summarizing the context for the LLM. public static string BuildTier1Summary(List stagedObjects) { if (stagedObjects == null || stagedObjects.Count == 0 || stagedObjects.All(o => o == null)) { return "No context provided."; } var summaryBuilder = new StringBuilder(); for (var i = 0; i < stagedObjects.Count; i++) { var obj = stagedObjects[i]; if (obj == null) continue; var stableId = GetStableIdForObject(obj); summaryBuilder.AppendLine($"\n## Subject {i + 1}: {obj.name} (ID: {stableId})"); summaryBuilder.AppendLine($"- Type: {obj.GetType().FullName}"); switch (obj) { case GameObject go: AppendGameObjectContext(go, summaryBuilder); break; case MonoScript script: AppendScriptContext(script, summaryBuilder); break; case ScriptableObject so: AppendScriptableObjectContext(so, summaryBuilder); break; } } return summaryBuilder.ToString(); } private static void AppendGameObjectContext(GameObject go, StringBuilder builder) { builder.AppendLine($"- Tag: {go.tag}"); builder.AppendLine($"- Layer: {LayerMask.LayerToName(go.layer)}"); var prefabPath = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(go); if (!string.IsNullOrEmpty(prefabPath)) { builder.AppendLine($"- Prefab Source: {prefabPath}"); } builder.AppendLine("- Hierarchy (pruned to show nodes with custom scripts):"); AppendHierarchyNode(go.transform, builder, " "); } /// /// Recursively builds a string representation of the hierarchy, intelligently pruning branches /// that do not contain any custom user scripts to keep the context concise. /// private static void AppendHierarchyNode(Transform transform, StringBuilder builder, string indent) { // Always include the root of the hierarchy being analyzed. builder.AppendLine($"{indent}- {transform.name} (ID: {transform.gameObject.GetInstanceID()})"); // List components on the current GameObject, prioritizing custom scripts. var components = transform.GetComponents().Where(c => c).ToList(); var customScripts = components.Where(IsCustomScript).ToList(); var otherComponents = components.Except(customScripts); foreach (var script in customScripts) { var monoScript = MonoScript.FromMonoBehaviour(script as MonoBehaviour); var scriptGuid = monoScript ? AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(monoScript)) : "N/A"; builder.AppendLine($"{indent} - Component: {script.GetType().FullName} (Custom Script, GUID: {scriptGuid})"); } foreach (var component in otherComponents) { builder.AppendLine($"{indent} - Component: {component.GetType().FullName}"); } // --- Pruning Logic --- var childrenWithScripts = new List(); var childrenWithoutScripts = new List(); foreach (Transform child in transform) { if (HasCustomScriptInChildren(child)) { childrenWithScripts.Add(child); } else { childrenWithoutScripts.Add(child); } } // Recurse into children that have scripts. foreach (var child in childrenWithScripts) { AppendHierarchyNode(child, builder, indent + " "); } // Summarize the pruned children. if (childrenWithoutScripts.Count > 0) { builder.AppendLine($"{indent} [+ {childrenWithoutScripts.Count} other child object(s) with no custom scripts]"); } } private static void AppendScriptContext(MonoScript script, StringBuilder builder) { var scriptClass = script.GetClass(); if (scriptClass != null) { if (scriptClass.BaseType != null && scriptClass.BaseType != typeof(MonoBehaviour)) { builder.AppendLine($"- Inherits from: {scriptClass.BaseType.FullName}"); } var interfaces = scriptClass.GetInterfaces(); if (interfaces.Length > 0) { builder.AppendLine($"- Implements: {string.Join(", ", interfaces.Select(i => i.Name))}"); } var methods = scriptClass.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) .Where(m => !m.IsSpecialName); // Exclude property getters/setters, etc. var methodInfos = methods as MethodInfo[] ?? methods.ToArray(); if (methodInfos.Any()) { builder.AppendLine("- Public API:"); foreach (var method in methodInfos) { var parameters = string.Join(", ", method.GetParameters().Select(p => $"{p.ParameterType.Name} {p.Name}")); builder.AppendLine($" - {method.ReturnType.Name} {method.Name}({parameters})"); } } } var scriptPath = AssetDatabase.GetAssetPath(script); if (!File.Exists(scriptPath)) return; builder.AppendLine("- Script Content:"); builder.AppendLine("```csharp"); builder.AppendLine(File.ReadAllText(scriptPath)); builder.AppendLine("```"); // Append dependency graph if available var guid = AssetDatabase.AssetPathToGUID(scriptPath); _ = GraphJsonExporter.TryAppendHumanReadableGraph(guid, builder); } private static void AppendScriptableObjectContext(ScriptableObject so, StringBuilder builder) { builder.AppendLine("- Note: This is a ScriptableObject data asset."); var fields = so.GetType() .GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) .Where(field => field.IsPublic || field.GetCustomAttribute() != null); var fieldInfos = fields as FieldInfo[] ?? fields.ToArray(); if (!fieldInfos.Any()) return; builder.AppendLine(" - Public Fields:"); foreach (var field in fieldInfos) { builder.AppendLine($" - {field.Name}: {field.GetValue(so)}"); } } private static string GetStableIdForObject(Object obj) { if (!AssetDatabase.TryGetGUIDAndLocalFileIdentifier(obj, out var guid, out long _)) return obj.GetInstanceID().ToString(); if (!string.IsNullOrEmpty(guid) && guid != "00000000000000000000000000000000") { return guid; } return obj.GetInstanceID().ToString(); } /// /// Checks if a component is a user-defined script (i.e., part of the Assets folder). /// private static bool IsCustomScript(Component component) { if (!component || component is not MonoBehaviour monoBehaviour) return false; var script = MonoScript.FromMonoBehaviour(monoBehaviour); if (!script) return false; var path = AssetDatabase.GetAssetPath(script); return !string.IsNullOrEmpty(path) && path.StartsWith("Assets/"); } /// /// Recursively checks if a transform or any of its descendants contain a custom script. /// private static bool HasCustomScriptInChildren(Transform transform) { if (transform.GetComponents().Any(IsCustomScript)) { return true; } foreach (Transform child in transform) { if (HasCustomScriptInChildren(child)) { return true; } } return false; } } }