Unity-Customer-Template/Assets/RL_DevPlus1/Editor/ProjectAnalyzer/RLReportExporter.cs
Frank Harris 28c15035a4
All checks were successful
Code Documentation / code-docs (push) Successful in 6s
Project Report / project-report (push) Successful in 4s
Validate Code / validate (push) Successful in 3s
added unity files
2026-06-15 12:03:33 -05:00

567 lines
25 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEditor;
using UnityEngine;
namespace RunlevelSystems.DevPlus1.EditorTools
{
[Serializable]
public class RLProjectAnalysisData
{
public string projectName;
public string unityVersion;
public string generatedAt;
public List<RLSceneInfo> scenes = new List<RLSceneInfo>();
public List<RLScriptInfo> scripts = new List<RLScriptInfo>();
public List<RLPrefabInfo> prefabs = new List<RLPrefabInfo>();
public RLGitReportSummary gitReports = new RLGitReportSummary();
public List<string> suggestedFirstFiles = new List<string>();
public List<string> risksOrUnknowns = new List<string>();
}
[Serializable]
public class RLSceneInfo
{
public string scenePath;
public string sceneName;
public int buildIndex = -1;
public int rootGameObjectCount;
public int totalGameObjects;
public int activeGameObjects;
public int inactiveGameObjects;
public int missingScriptCount;
public int missingReferenceCount;
public List<string> rootGameObjects = new List<string>();
public List<RLGameObjectInfo> gameObjects = new List<RLGameObjectInfo>();
public List<RLUnityEventInfo> unityEvents = new List<RLUnityEventInfo>();
public string notes;
}
[Serializable]
public class RLGameObjectInfo
{
public string hierarchyPath;
public bool activeSelf;
public string tag;
public int layer;
public List<string> components = new List<string>();
public List<string> attachedScripts = new List<string>();
public List<string> referencedObjects = new List<string>();
public int missingScriptCount;
public int missingReferenceCount;
}
[Serializable]
public class RLScriptInfo
{
public string scriptPath;
public string className;
public string baseClass;
public bool isMonoBehaviour;
public bool isScriptableObject;
public List<string> lifecycleMethods = new List<string>();
public List<string> publicMethods = new List<string>();
public List<string> serializedFields = new List<string>();
public List<string> publicFields = new List<string>();
public List<string> unityApiUsage = new List<string>();
public string possibleRole;
}
[Serializable]
public class RLPrefabInfo
{
public string prefabPath;
public List<string> components = new List<string>();
public List<string> attachedScripts = new List<string>();
public List<string> nestedPrefabs = new List<string>();
public List<string> importantReferences = new List<string>();
public int missingScriptCount;
public int missingReferenceCount;
}
[Serializable]
public class RLUnityEventInfo
{
public string sourcePath;
public string gameObjectPath;
public string componentName;
public string eventName;
public string targetObject;
public string targetComponent;
public string targetMethod;
}
[Serializable]
public class RLGitReportSummary
{
public bool reportsFolderFound;
public bool validationReportFound;
public bool validationFoundErrors;
public int validationWarnings = -1;
public int validationErrors = -1;
public bool apiReferenceFound;
public bool filePurposeFound;
public bool staticCallMapFound;
public bool unityScriptReportFound;
public List<string> likelyEntryPoints = new List<string>();
public List<string> importantFiles = new List<string>();
public List<string> foundReports = new List<string>();
public List<string> missingReports = new List<string>();
public string notes;
}
public static class RLReportExporter
{
public const string RootFolder = "Assets/RL_DevPlus1";
public const string ReportFolder = RootFolder + "/Reports";
public static void EnsureFolders()
{
EnsureAssetFolder("Assets", "RL_DevPlus1");
EnsureAssetFolder(RootFolder, "Reports");
EnsureAssetFolder(RootFolder, "Docs");
EnsureAssetFolder(RootFolder, "Runtime");
}
public static string AbsoluteReportFolder
{
get { return Path.GetFullPath(Path.Combine(Application.dataPath, "RL_DevPlus1/Reports")); }
}
public static void ExportAll(RLProjectAnalysisData data)
{
EnsureFolders();
WriteProjectSummary(data);
WriteSceneMap(data);
WriteGameObjectMap(data);
WriteScriptMap(data);
WriteEventMap(data);
WritePrefabMap(data);
WriteExecutionOrder(data);
WriteMissingReferences(data);
WriteGitSummary(data.gitReports);
WriteProjectDataJson(data);
WriteAIContext(data);
WriteReportIndex();
AssetDatabase.Refresh();
}
public static void WriteGitSummary(RLGitReportSummary git)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("# Runlevel Unity Git Report Summary");
sb.AppendLine();
sb.AppendLine("- Git reports folder found: " + BoolText(git.reportsFolderFound));
sb.AppendLine("- Validation report found: " + BoolText(git.validationReportFound));
sb.AppendLine("- Validation found errors: " + BoolText(git.validationFoundErrors));
sb.AppendLine("- Validation warnings: " + ValueOrUnknown(git.validationWarnings));
sb.AppendLine("- Validation errors: " + ValueOrUnknown(git.validationErrors));
sb.AppendLine("- API reference found: " + BoolText(git.apiReferenceFound));
sb.AppendLine("- File purpose report found: " + BoolText(git.filePurposeFound));
sb.AppendLine("- Static call map found: " + BoolText(git.staticCallMapFound));
sb.AppendLine("- Unity script report found: " + BoolText(git.unityScriptReportFound));
AppendList(sb, "Likely Entry Points From Git Reports", git.likelyEntryPoints);
AppendList(sb, "Important Files From Git Reports", git.importantFiles);
AppendList(sb, "Found Reports", git.foundReports);
AppendList(sb, "Missing Reports", git.missingReports);
sb.AppendLine("## Notes");
sb.AppendLine();
sb.AppendLine(string.IsNullOrEmpty(git.notes) ? "Best-effort import only. Missing Git reports are allowed." : git.notes);
WriteReport("RL_Unity_Git_Report_Summary.md", sb.ToString());
}
private static void WriteProjectSummary(RLProjectAnalysisData data)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("# Runlevel Unity Project Summary");
sb.AppendLine();
sb.AppendLine("- Project name: " + data.projectName);
sb.AppendLine("- Unity version: " + data.unityVersion);
sb.AppendLine("- Generated at: " + data.generatedAt);
sb.AppendLine("- Scenes analyzed: " + data.scenes.Count);
sb.AppendLine("- Scripts analyzed: " + data.scripts.Count);
sb.AppendLine("- Prefabs analyzed: " + data.prefabs.Count);
sb.AppendLine("- Missing scripts detected: " + CountMissingScripts(data));
sb.AppendLine("- Missing references detected: " + CountMissingReferences(data));
sb.AppendLine();
sb.AppendLine("This report is best-effort and needs manual confirmation before project changes are made.");
WriteReport("RL_Unity_Project_Summary.md", sb.ToString());
}
private static void WriteSceneMap(RLProjectAnalysisData data)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("# Runlevel Unity Scene Map");
sb.AppendLine();
foreach (RLSceneInfo scene in data.scenes)
{
sb.AppendLine("## " + scene.sceneName);
sb.AppendLine();
sb.AppendLine("- Scene path: `" + scene.scenePath + "`");
sb.AppendLine("- Build index: " + scene.buildIndex);
sb.AppendLine("- Root GameObjects: " + scene.rootGameObjectCount);
sb.AppendLine("- Total GameObjects: " + scene.totalGameObjects);
sb.AppendLine("- Active/Inactive: " + scene.activeGameObjects + "/" + scene.inactiveGameObjects);
sb.AppendLine("- Missing scripts: " + scene.missingScriptCount);
sb.AppendLine("- Missing references: " + scene.missingReferenceCount);
AppendList(sb, "Root GameObjects", scene.rootGameObjects);
if (!string.IsNullOrEmpty(scene.notes))
{
sb.AppendLine("Notes: " + scene.notes);
sb.AppendLine();
}
}
WriteReport("RL_Unity_Scene_Map.md", sb.ToString());
}
private static void WriteGameObjectMap(RLProjectAnalysisData data)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("# Runlevel Unity GameObject Map");
sb.AppendLine();
foreach (RLSceneInfo scene in data.scenes)
{
sb.AppendLine("## Scene: " + scene.sceneName);
sb.AppendLine();
foreach (RLGameObjectInfo go in scene.gameObjects)
{
sb.AppendLine("- `" + go.hierarchyPath + "` | active=" + go.activeSelf + " | tag=" + go.tag + " | layer=" + go.layer + " | components=" + string.Join(", ", go.components.ToArray()));
}
sb.AppendLine();
}
WriteReport("RL_Unity_GameObject_Map.md", sb.ToString());
}
private static void WriteScriptMap(RLProjectAnalysisData data)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("# Runlevel Unity Script Map");
sb.AppendLine();
foreach (RLScriptInfo script in data.scripts)
{
sb.AppendLine("## `" + script.scriptPath + "`");
sb.AppendLine();
sb.AppendLine("- Class: " + script.className);
sb.AppendLine("- Base class: " + script.baseClass);
sb.AppendLine("- MonoBehaviour: " + BoolText(script.isMonoBehaviour));
sb.AppendLine("- ScriptableObject: " + BoolText(script.isScriptableObject));
sb.AppendLine("- Possible role: " + script.possibleRole);
AppendList(sb, "Lifecycle Methods", script.lifecycleMethods);
AppendList(sb, "Public Methods", script.publicMethods);
AppendList(sb, "Serialized Fields", script.serializedFields);
AppendList(sb, "Public Fields", script.publicFields);
AppendList(sb, "Unity API Usage", script.unityApiUsage);
}
WriteReport("RL_Unity_Script_Map.md", sb.ToString());
}
private static void WriteEventMap(RLProjectAnalysisData data)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("# Runlevel Unity Event Map");
sb.AppendLine();
bool any = false;
foreach (RLSceneInfo scene in data.scenes)
{
foreach (RLUnityEventInfo evt in scene.unityEvents)
{
any = true;
sb.AppendLine("- Scene `" + scene.scenePath + "` GameObject `" + evt.gameObjectPath + "` component `" + evt.componentName + "` event `" + evt.eventName + "` targets `" + evt.targetObject + "." + evt.targetMethod + "`");
}
}
if (!any)
{
sb.AppendLine("No UnityEvent or Button callback bindings detected in analyzed scenes.");
}
WriteReport("RL_Unity_Event_Map.md", sb.ToString());
}
private static void WritePrefabMap(RLProjectAnalysisData data)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("# Runlevel Unity Prefab Map");
sb.AppendLine();
foreach (RLPrefabInfo prefab in data.prefabs)
{
sb.AppendLine("## `" + prefab.prefabPath + "`");
sb.AppendLine();
AppendList(sb, "Components", prefab.components);
AppendList(sb, "Attached Scripts", prefab.attachedScripts);
AppendList(sb, "Nested Prefabs", prefab.nestedPrefabs);
AppendList(sb, "Important References", prefab.importantReferences);
sb.AppendLine("- Missing scripts: " + prefab.missingScriptCount);
sb.AppendLine("- Missing references: " + prefab.missingReferenceCount);
sb.AppendLine();
}
WriteReport("RL_Unity_Prefab_Map.md", sb.ToString());
}
private static void WriteExecutionOrder(RLProjectAnalysisData data)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("# Runlevel Unity Execution Order");
sb.AppendLine();
sb.AppendLine("## Script Execution Order Settings");
sb.AppendLine();
bool any = false;
string[] scriptGuids = AssetDatabase.FindAssets("t:MonoScript");
foreach (string guid in scriptGuids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
MonoScript script = AssetDatabase.LoadAssetAtPath<MonoScript>(path);
if (script == null)
{
continue;
}
int order = MonoImporter.GetExecutionOrder(script);
if (order != 0)
{
any = true;
sb.AppendLine("- " + script.name + ": " + order);
}
}
if (!any)
{
sb.AppendLine("- No custom script execution order detected.");
}
sb.AppendLine();
sb.AppendLine("## Probable Runtime Order");
sb.AppendLine();
sb.AppendLine("- Scene load");
sb.AppendLine("- Awake");
sb.AppendLine("- OnEnable");
sb.AppendLine("- Start");
sb.AppendLine("- Update / FixedUpdate / LateUpdate");
sb.AppendLine("- UnityEvents / UI callbacks");
sb.AppendLine("- Scene transitions");
AppendList(sb, "Lifecycle Methods By Script", BuildLifecycleLines(data.scripts));
WriteReport("RL_Unity_Execution_Order.md", sb.ToString());
}
private static void WriteMissingReferences(RLProjectAnalysisData data)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("# Runlevel Unity Missing References");
sb.AppendLine();
foreach (RLSceneInfo scene in data.scenes)
{
foreach (RLGameObjectInfo go in scene.gameObjects)
{
if (go.missingScriptCount > 0 || go.missingReferenceCount > 0)
{
sb.AppendLine("- Scene `" + scene.scenePath + "` GameObject `" + go.hierarchyPath + "` missing scripts=" + go.missingScriptCount + " missing references=" + go.missingReferenceCount);
}
}
}
foreach (RLPrefabInfo prefab in data.prefabs)
{
if (prefab.missingScriptCount > 0 || prefab.missingReferenceCount > 0)
{
sb.AppendLine("- Prefab `" + prefab.prefabPath + "` missing scripts=" + prefab.missingScriptCount + " missing references=" + prefab.missingReferenceCount);
}
}
if (sb.ToString().TrimEnd().EndsWith("References", StringComparison.Ordinal))
{
sb.AppendLine("No missing scripts or missing serialized object references detected in analyzed assets.");
}
WriteReport("RL_Unity_Missing_References.md", sb.ToString());
}
private static void WriteProjectDataJson(RLProjectAnalysisData data)
{
WriteReport("RL_Unity_Project_Data.json", JsonUtility.ToJson(data, true));
}
private static void WriteAIContext(RLProjectAnalysisData data)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("# Runlevel AI Project Context");
sb.AppendLine();
sb.AppendLine("This report is intended to be fed into AI/Codex before deeper Unity project work.");
sb.AppendLine();
sb.AppendLine("- Project name: " + data.projectName);
sb.AppendLine("- Unity version: " + data.unityVersion);
sb.AppendLine("- Main likely entry scene: " + GetLikelyEntryScene(data));
sb.AppendLine("- Git validation status: " + (data.gitReports.validationFoundErrors ? "Detected customer code errors" : "No customer code errors detected or report missing"));
AppendList(sb, "Scenes", SceneLines(data.scenes));
AppendList(sb, "Important Managers/Controllers", ManagerLines(data.scripts));
AppendList(sb, "MonoBehaviour Scripts", ScriptLines(data.scripts, true));
AppendList(sb, "ScriptableObjects", ScriptLines(data.scripts, false));
AppendList(sb, "Prefabs", PrefabLines(data.prefabs));
AppendList(sb, "Scene Transitions", UsageLines(data.scripts, "SceneManager"));
AppendList(sb, "Suggested First Files For AI To Inspect", data.suggestedFirstFiles);
AppendList(sb, "Suggested Risks Or Unknowns", data.risksOrUnknowns);
sb.AppendLine("## Limitations");
sb.AppendLine();
sb.AppendLine("- Best-effort static and editor inspection only.");
sb.AppendLine("- Runtime-created objects, reflection, dependency injection, addressable content, and generated code may be incomplete.");
sb.AppendLine("- Findings need manual confirmation before changes are made.");
WriteReport("RL_AI_Project_Context.md", sb.ToString());
WriteReport("RL_AI_Project_Context.json", JsonUtility.ToJson(data, true));
}
private static void WriteReportIndex()
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("# Runlevel Unity Report Index");
sb.AppendLine();
sb.AppendLine("- `RL_Unity_Project_Summary.md`: high-level project counts and status.");
sb.AppendLine("- `RL_Unity_Scene_Map.md`: scene paths, roots, counts, and missing reference totals.");
sb.AppendLine("- `RL_Unity_GameObject_Map.md`: GameObject hierarchy and component overview.");
sb.AppendLine("- `RL_Unity_Script_Map.md`: C# script lifecycle, fields, methods, and Unity API usage.");
sb.AppendLine("- `RL_Unity_Event_Map.md`: detected UnityEvent and Button callback bindings.");
sb.AppendLine("- `RL_Unity_Prefab_Map.md`: prefab components, scripts, nested prefabs, and missing references.");
sb.AppendLine("- `RL_Unity_Execution_Order.md`: script execution order settings and probable lifecycle order.");
sb.AppendLine("- `RL_Unity_Missing_References.md`: missing scripts and serialized reference issues.");
sb.AppendLine("- `RL_Unity_Git_Report_Summary.md`: imported root `reports/` summary if available.");
sb.AppendLine("- `RL_Unity_Project_Data.json`: AI-readable Unity analysis data.");
sb.AppendLine("- `RL_AI_Project_Context.md`: primary AI/Codex context report.");
sb.AppendLine("- `RL_AI_Project_Context.json`: machine-readable AI context data.");
WriteReport("RL_Unity_Report_Index.md", sb.ToString());
}
public static void WriteReport(string fileName, string contents)
{
EnsureFolders();
File.WriteAllText(Path.Combine(AbsoluteReportFolder, fileName), contents, Encoding.UTF8);
}
public static void RevealReportFolder()
{
EnsureFolders();
EditorUtility.RevealInFinder(AbsoluteReportFolder);
}
private static void EnsureAssetFolder(string parent, string child)
{
string full = parent + "/" + child;
if (!AssetDatabase.IsValidFolder(full))
{
AssetDatabase.CreateFolder(parent, child);
}
}
private static string BoolText(bool value)
{
return value ? "yes" : "no";
}
private static string ValueOrUnknown(int value)
{
return value >= 0 ? value.ToString() : "unknown";
}
private static void AppendList(StringBuilder sb, string title, List<string> values)
{
sb.AppendLine("## " + title);
sb.AppendLine();
if (values == null || values.Count == 0)
{
sb.AppendLine("- None detected.");
}
else
{
foreach (string value in values)
{
sb.AppendLine("- " + value);
}
}
sb.AppendLine();
}
private static int CountMissingScripts(RLProjectAnalysisData data)
{
int count = 0;
foreach (RLSceneInfo scene in data.scenes) count += scene.missingScriptCount;
foreach (RLPrefabInfo prefab in data.prefabs) count += prefab.missingScriptCount;
return count;
}
private static int CountMissingReferences(RLProjectAnalysisData data)
{
int count = 0;
foreach (RLSceneInfo scene in data.scenes) count += scene.missingReferenceCount;
foreach (RLPrefabInfo prefab in data.prefabs) count += prefab.missingReferenceCount;
return count;
}
private static List<string> BuildLifecycleLines(List<RLScriptInfo> scripts)
{
List<string> lines = new List<string>();
foreach (RLScriptInfo script in scripts)
{
if (script.lifecycleMethods.Count > 0)
{
lines.Add(script.className + ": " + string.Join(", ", script.lifecycleMethods.ToArray()));
}
}
return lines;
}
private static string GetLikelyEntryScene(RLProjectAnalysisData data)
{
foreach (RLSceneInfo scene in data.scenes)
{
if (scene.buildIndex == 0) return scene.scenePath;
}
return data.scenes.Count > 0 ? data.scenes[0].scenePath : "No scene analyzed";
}
private static List<string> SceneLines(List<RLSceneInfo> scenes)
{
List<string> lines = new List<string>();
foreach (RLSceneInfo scene in scenes) lines.Add(scene.scenePath + " (" + scene.totalGameObjects + " GameObjects)");
return lines;
}
private static List<string> ManagerLines(List<RLScriptInfo> scripts)
{
List<string> lines = new List<string>();
foreach (RLScriptInfo script in scripts)
{
string lower = (script.className + " " + script.possibleRole).ToLowerInvariant();
if (lower.Contains("manager") || lower.Contains("controller"))
{
lines.Add(script.scriptPath + " - " + script.possibleRole);
}
}
return lines;
}
private static List<string> ScriptLines(List<RLScriptInfo> scripts, bool monoBehaviour)
{
List<string> lines = new List<string>();
foreach (RLScriptInfo script in scripts)
{
if ((monoBehaviour && script.isMonoBehaviour) || (!monoBehaviour && script.isScriptableObject))
{
lines.Add(script.scriptPath + " - " + script.className);
}
}
return lines;
}
private static List<string> PrefabLines(List<RLPrefabInfo> prefabs)
{
List<string> lines = new List<string>();
foreach (RLPrefabInfo prefab in prefabs) lines.Add(prefab.prefabPath);
return lines;
}
private static List<string> UsageLines(List<RLScriptInfo> scripts, string usage)
{
List<string> lines = new List<string>();
foreach (RLScriptInfo script in scripts)
{
if (script.unityApiUsage.Contains(usage))
{
lines.Add(script.scriptPath);
}
}
return lines;
}
}
}