diff --git a/Editor/ErrorReporting/ComponentValidation.cs b/Editor/ErrorReporting/ComponentValidation.cs index 81bb0946..e80b2224 100644 --- a/Editor/ErrorReporting/ComponentValidation.cs +++ b/Editor/ErrorReporting/ComponentValidation.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using nadena.dev.modular_avatar.core; +using nadena.dev.ndmf; #if MA_VRCSDK3_AVATARS using nadena.dev.modular_avatar.core.menu; @@ -16,64 +17,61 @@ namespace nadena.dev.modular_avatar.editor.ErrorReporting /// /// /// Null if valid, otherwise a list of configuration errors - internal static List CheckComponent(this AvatarTagComponent tagComponent) + internal static void CheckComponent(this AvatarTagComponent tagComponent) { - switch (tagComponent) + ErrorReport.WithContextObject(tagComponent, () => { - case ModularAvatarBlendshapeSync bs: - return CheckInternal(bs); - case ModularAvatarBoneProxy bp: - return CheckInternal(bp); + switch (tagComponent) + { + case ModularAvatarBlendshapeSync bs: + CheckInternal(bs); + break; + case ModularAvatarBoneProxy bp: + CheckInternal(bp); + break; #if MA_VRCSDK3_AVATARS - case ModularAvatarMenuInstaller mi: - return CheckInternal(mi); - case ModularAvatarMergeAnimator obj: - return CheckInternal(obj); + case ModularAvatarMenuInstaller mi: + CheckInternal(mi); + break; + case ModularAvatarMergeAnimator obj: + CheckInternal(obj); + break; #endif - case ModularAvatarMergeArmature obj: - return CheckInternal(obj); - default: - return null; - } + case ModularAvatarMergeArmature obj: + CheckInternal(obj); + break; + default: + return; + } + }); } - internal static List ValidateAll(GameObject root) + internal static void ValidateAll(GameObject root) { - List logs = new List(); foreach (var component in root.GetComponentsInChildren(true)) { - var componentLogs = component.CheckComponent(); - if (componentLogs != null) - { - logs.AddRange(componentLogs); - } + component.CheckComponent(); } - - return logs; } - private static List CheckInternal(ModularAvatarBlendshapeSync bs) - { + private static void CheckInternal(ModularAvatarBlendshapeSync bs) + { var localMesh = bs.GetComponent(); if (localMesh == null) { - return new List - {new ErrorLog(ReportLevel.Validation, "validation.blendshape_sync.no_local_renderer", bs)}; + BuildReport.Log(ErrorSeverity.NonFatal, "validation.blendshape_sync.no_local_renderer", bs); } if (localMesh.sharedMesh == null) { - return new List - {new ErrorLog(ReportLevel.Validation, "validation.blendshape_sync.no_local_mesh", bs)}; + BuildReport.Log(ErrorSeverity.NonFatal, "validation.blendshape_sync.no_local_mesh", bs); } if (bs.Bindings == null || bs.Bindings.Count == 0) { - return new List - {new ErrorLog(ReportLevel.Validation, "validation.blendshape_sync.no_bindings", bs)}; + BuildReport.Log(ErrorSeverity.Information,"validation.blendshape_sync.no_bindings", bs); } - List errorLogs = new List(); foreach (var binding in bs.Bindings) { var localShape = string.IsNullOrWhiteSpace(binding.LocalBlendshape) @@ -82,113 +80,82 @@ namespace nadena.dev.modular_avatar.editor.ErrorReporting if (localMesh.sharedMesh.GetBlendShapeIndex(localShape) == -1) { - errorLogs.Add(new ErrorLog(ReportLevel.Validation, "validation.blendshape_sync.missing_local_shape", - new string[] {localShape}, bs)); + BuildReport.Log(ErrorSeverity.NonFatal, "validation.blendshape_sync.missing_local_shape", + localShape, bs); } var targetObj = binding.ReferenceMesh.Get(bs.transform); if (targetObj == null) { - errorLogs.Add(new ErrorLog(ReportLevel.Validation, "validation.blendshape_sync.no_target", bs)); + BuildReport.Log(ErrorSeverity.NonFatal, "validation.blendshape_sync.no_target", bs); continue; } var targetRenderer = targetObj.GetComponent(); if (targetRenderer == null) { - errorLogs.Add(new ErrorLog(ReportLevel.Validation, - "validation.blendshape_sync.missing_target_renderer", bs, targetRenderer)); + BuildReport.Log(ErrorSeverity.NonFatal, + "validation.blendshape_sync.missing_target_renderer", bs, targetRenderer); continue; } var targetMesh = targetRenderer.sharedMesh; if (targetMesh == null) { - errorLogs.Add(new ErrorLog(ReportLevel.Validation, "validation.blendshape_sync.missing_target_mesh", - bs, targetRenderer)); + BuildReport.Log(ErrorSeverity.NonFatal, "validation.blendshape_sync.missing_target_mesh", + bs, targetRenderer); continue; } if (targetMesh.GetBlendShapeIndex(binding.Blendshape) == -1) { - errorLogs.Add(new ErrorLog(ReportLevel.Validation, - "validation.blendshape_sync.missing_target_shape", new string[] {binding.Blendshape}, bs, - targetRenderer)); + BuildReport.Log(ErrorSeverity.NonFatal, + "validation.blendshape_sync.missing_target_shape", binding.Blendshape, bs, + targetRenderer); } } - - if (errorLogs.Count == 0) - { - return null; - } - else - { - return errorLogs; - } } - private static List CheckInternal(ModularAvatarBoneProxy bp) + private static void CheckInternal(ModularAvatarBoneProxy bp) { if (bp.target == null) { - return new List() - { - new ErrorLog(ReportLevel.Validation, "validation.bone_proxy.no_target", bp) - }; + BuildReport.Log(ErrorSeverity.NonFatal, "validation.bone_proxy.no_target", bp); } - - return null; } #if MA_VRCSDK3_AVATARS - private static List CheckInternal(ModularAvatarMenuInstaller mi) + private static void CheckInternal(ModularAvatarMenuInstaller mi) { // TODO - check that target menu is in the avatar if (mi.menuToAppend == null && mi.GetComponent() == null) { - return new List() - { - new ErrorLog(ReportLevel.Validation, "validation.menu_installer.no_menu", mi) - }; + BuildReport.Log(ErrorSeverity.NonFatal, "validation.menu_installer.no_menu", mi); } - - return null; } - private static List CheckInternal(ModularAvatarMergeAnimator ma) + private static void CheckInternal(ModularAvatarMergeAnimator ma) { if (ma.animator == null) { - return new List() - { - new ErrorLog(ReportLevel.Validation, "validation.merge_animator.no_animator", ma) - }; + BuildReport.Log(ErrorSeverity.NonFatal, "validation.merge_animator.no_animator", ma); } - - return null; } #endif - private static List CheckInternal(ModularAvatarMergeArmature ma) + private static void CheckInternal(ModularAvatarMergeArmature ma) { if (ma.mergeTargetObject == null) { - return new List() - { - new ErrorLog(ReportLevel.Validation, "validation.merge_armature.no_target", ma) - }; + BuildReport.Log(ErrorSeverity.NonFatal, "validation.merge_armature.no_target", ma); + return; } if (ma.mergeTargetObject == ma.gameObject || ma.mergeTargetObject.transform.IsChildOf(ma.transform)) { - return new List() - { - new ErrorLog(ReportLevel.Validation, "error.merge_armature.merge_into_self", ma, - ma.mergeTargetObject) - }; + BuildReport.Log(ErrorSeverity.Error, "error.merge_armature.circular_dependency", ma, + ma.mergeTargetObject); } - - return null; } } } \ No newline at end of file diff --git a/Editor/ErrorReporting/ErrorElement.cs b/Editor/ErrorReporting/ErrorElement.cs deleted file mode 100644 index 045d4fe1..00000000 --- a/Editor/ErrorReporting/ErrorElement.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using nadena.dev.modular_avatar.core.editor; -using UnityEditor; -using UnityEngine; -using UnityEngine.SceneManagement; -using UnityEngine.UIElements; - -namespace nadena.dev.modular_avatar.editor.ErrorReporting -{ - internal class ErrorElement : Box - { - private readonly ErrorLog log; - - Texture2D GetIcon() - { - switch (log.reportLevel) - { - case ReportLevel.Info: - return EditorGUIUtility.FindTexture("d_console.infoicon"); - case ReportLevel.Warning: - return EditorGUIUtility.FindTexture("d_console.warnicon"); - default: - return EditorGUIUtility.FindTexture("d_console.erroricon"); - } - } - - public ErrorElement(ErrorLog log, ObjectRefLookupCache cache) - { - this.log = log; - - AddToClassList("ErrorElement"); - var tex = GetIcon(); - if (tex != null) - { - var image = new Image(); - image.image = tex; - Add(image); - } - - var inner = new Box(); - Add(inner); - - var label = new Label(GetLabelText()); - inner.Add(label); - - foreach (var obj in log.referencedObjects) - { - var referenced = obj.Lookup(cache); - if (referenced != null) - { - inner.Add(new SelectionButton(obj.typeName, referenced)); - } - } - - if (!string.IsNullOrWhiteSpace(log.stacktrace)) - { - var foldout = new Foldout(); - foldout.text = Localization.S("error.stack_trace"); - var field = new TextField(); - field.value = log.stacktrace; - field.isReadOnly = true; - field.multiline = true; - foldout.Add(field); - foldout.value = false; - inner.Add(foldout); - } - } - - private static GameObject FindObject(string path) - { - var scene = SceneManager.GetActiveScene(); - foreach (var root in scene.GetRootGameObjects()) - { - if (root.name == path) return root; - if (path.StartsWith(root.name + "/")) - { - return root.transform.Find(path.Substring(root.name.Length + 1))?.gameObject; - } - } - - return null; - } - - private string GetLabelText() - { - var objArray = new object[log.substitutions.Length]; - for (int i = 0; i < log.substitutions.Length; i++) - { - objArray[i] = log.substitutions[i]; - } - - try - { - return string.Format(Localization.S(log.messageCode), objArray); - } - catch (FormatException e) - { - Debug.LogError("Error formatting message code: " + log.messageCode); - Debug.LogException(e); - return log.messageCode + "\n" + string.Join("\n", objArray); - } - } - } -} \ No newline at end of file diff --git a/Editor/ErrorReporting/ErrorElement.cs.meta b/Editor/ErrorReporting/ErrorElement.cs.meta deleted file mode 100644 index 763f54eb..00000000 --- a/Editor/ErrorReporting/ErrorElement.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: a534edd7151c4cd49fe07919ae526004 -timeCreated: 1674132977 \ No newline at end of file diff --git a/Editor/ErrorReporting/ErrorLog.cs b/Editor/ErrorReporting/ErrorLog.cs index 52b279f9..2f068c18 100644 --- a/Editor/ErrorReporting/ErrorLog.cs +++ b/Editor/ErrorReporting/ErrorLog.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; using nadena.dev.modular_avatar.core; +using nadena.dev.modular_avatar.core.editor; +using nadena.dev.ndmf; using Newtonsoft.Json; using UnityEngine; using UnityEditor; @@ -11,376 +13,38 @@ using Object = UnityEngine.Object; namespace nadena.dev.modular_avatar.editor.ErrorReporting { - internal class AvatarReport - { - [JsonProperty] internal ObjectRef objectRef; - - [JsonProperty] internal bool successful; - - [JsonProperty] internal List logs = new List(); - } - - internal class ObjectRefLookupCache - { - private Dictionary> _cache = - new Dictionary>(); - - internal UnityEngine.Object FindByGuidAndLocalId(string guid, long localId) - { - if (!_cache.TryGetValue(guid, out var fileContents)) - { - var path = AssetDatabase.GUIDToAssetPath(guid); - if (string.IsNullOrEmpty(path)) - { - return null; - } - - var assets = AssetDatabase.LoadAllAssetsAtPath(path); - fileContents = new Dictionary(assets.Length); - foreach (var asset in assets) - { - if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(asset, out var _, out long detectedId)) - { - fileContents[detectedId] = asset; - } - } - - _cache[guid] = fileContents; - } - - if (fileContents.TryGetValue(localId, out var obj)) - { - return obj; - } - else - { - return null; - } - } - } - - internal struct ObjectRef - { - [JsonProperty] internal string guid; - [JsonProperty] internal long? localId; - [JsonProperty] internal string path, name; - [JsonProperty] internal string typeName; - - internal ObjectRef(Object obj) - { - this.guid = null; - localId = null; - - if (obj == null) - { - this.guid = path = name = null; - localId = null; - typeName = null; - return; - } - - typeName = obj.GetType().Name; - - long id; - if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(obj, out var guid, out id)) - { - this.guid = guid; - localId = id; - } - - if (obj is Component c) - { - path = RuntimeUtil.RelativePath(null, c.gameObject); - } - else if (obj is GameObject go) - { - path = RuntimeUtil.RelativePath(null, go); - } - else - { - path = null; - } - - name = string.IsNullOrWhiteSpace(obj.name) ? "" : obj.name; - } - - internal UnityEngine.Object Lookup(ObjectRefLookupCache cache) - { - if (path != null) - { - return FindObject(path); - } - else if (guid != null && localId.HasValue) - { - return cache.FindByGuidAndLocalId(guid, localId.Value); - } - else - { - return null; - } - } - - private static GameObject FindObject(string path) - { - var scene = SceneManager.GetActiveScene(); - foreach (var root in scene.GetRootGameObjects()) - { - if (root.name == path) return root; - if (path.StartsWith(root.name + "/")) - { - return root.transform.Find(path.Substring(root.name.Length + 1))?.gameObject; - } - } - - return null; - } - - public ObjectRef Remap(string original, string cloned) - { - if (path == cloned) - { - path = original; - name = path.Substring(path.LastIndexOf('/') + 1); - } - else if (path != null && path.StartsWith(cloned + "/")) - { - path = original + path.Substring(cloned.Length); - name = path.Substring(path.LastIndexOf('/') + 1); - } - - return this; - } - } - - internal enum ReportLevel - { - Validation, - Info, - Warning, - Error, - InternalError, - } - - internal class ErrorLog - { - [JsonProperty] internal List referencedObjects; - [JsonProperty] internal ReportLevel reportLevel; - [JsonProperty] internal string messageCode; - [JsonProperty] internal string[] substitutions; - [JsonProperty] internal string stacktrace; - - internal ErrorLog(ReportLevel level, string code, string[] strings, params object[] args) - { - reportLevel = level; - - substitutions = strings.Select(s => s.ToString()).ToArray(); - - referencedObjects = args.Where(o => o is Component || o is GameObject) - .Select(o => new ObjectRef(o is Component c ? c.gameObject : (GameObject) o)) - .ToList(); - referencedObjects.AddRange(BuildReport.CurrentReport.GetActiveReferences()); - - messageCode = code; - stacktrace = null; - } - - internal ErrorLog(ReportLevel level, string code, params object[] args) : this(level, code, - Array.Empty(), args) - { - } - - internal ErrorLog(Exception e, string additionalStackTrace = "") - { - reportLevel = ReportLevel.InternalError; - messageCode = "error.internal_error"; - substitutions = new string[] {e.Message, e.TargetSite?.Name}; - referencedObjects = BuildReport.CurrentReport.GetActiveReferences().ToList(); - stacktrace = e.ToString() + additionalStackTrace; - } - - public override string ToString() - { - return "[" + reportLevel + "] " + messageCode + " " + "subst: " + string.Join(", ", substitutions); - } - } - internal class BuildReport { private const string Path = "Library/ModularAvatarBuildReport.json"; - private static BuildReport _report; - private AvatarReport _currentAvatar; - private Stack _references = new Stack(); - - [JsonProperty] internal List Avatars = new List(); - internal AvatarReport CurrentAvatar => _currentAvatar; - - public static BuildReport CurrentReport + internal static void Log(ErrorSeverity severity, string code, params object[] objects) { - get - { - if (_report == null) _report = LoadReport() ?? new BuildReport(); - return _report; - } + ErrorReport.ReportError(Localization.L, severity, code, objects); } - static BuildReport() + internal static void LogFatal(string code, params object[] objects) { - EditorApplication.playModeStateChanged += change => - { - switch (change) - { - case PlayModeStateChange.ExitingEditMode: - // TODO - skip if we're doing a VRCSDK build - _report = new BuildReport(); - break; - } - }; - } - - private static BuildReport LoadReport() - { - try - { - var data = File.ReadAllText(Path); - return JsonConvert.DeserializeObject(data); - } - catch (Exception) - { - return null; - } - } - - internal static void SaveReport() - { - var report = CurrentReport; - var json = JsonConvert.SerializeObject(report); - - File.WriteAllText(Path, json); - - ErrorReportUI.reloadErrorReport(); - } - - private class AvatarReportScope : IDisposable - { - public void Dispose() - { - var successful = CurrentReport._currentAvatar.successful; - CurrentReport._currentAvatar = null; - BuildReport.SaveReport(); - if (!successful) throw new Exception("Avatar processing failed"); - } - } - - internal IDisposable ReportingOnAvatar(GameObject avatarGameObject) - { - if (avatarGameObject != null) - { - AvatarReport report = new AvatarReport(); - report.objectRef = new ObjectRef(avatarGameObject); - Avatars.Add(report); - _currentAvatar = report; - _currentAvatar.successful = true; - - _currentAvatar.logs.AddRange(ComponentValidation.ValidateAll(avatarGameObject)); - } - - return new AvatarReportScope(); - } - - internal static void Log(ReportLevel level, string code, object[] strings, params Object[] objects) - { - ErrorLog errorLog = - new ErrorLog(level, code, strings: strings.Select(s => s.ToString()).ToArray(), objects); - - var avatarReport = CurrentReport._currentAvatar; - if (avatarReport == null) - { - Debug.LogWarning("Error logged when not processing an avatar: " + errorLog); - return; - } - - avatarReport.logs.Add(errorLog); - } - - internal static void LogFatal(string code, object[] strings, params Object[] objects) - { - Log(ReportLevel.Error, code, strings: strings, objects: objects); - if (CurrentReport._currentAvatar != null) - { - CurrentReport._currentAvatar.successful = false; - } - else - { - throw new Exception("Fatal error without error reporting scope"); - } + ErrorReport.ReportError(Localization.L, ErrorSeverity.Error, code, objects); } internal static void LogException(Exception e, string additionalStackTrace = "") { - var avatarReport = CurrentReport._currentAvatar; - if (avatarReport == null) - { - Debug.LogException(e); - return; - } - else - { - avatarReport.logs.Add(new ErrorLog(e, additionalStackTrace)); - } + ErrorReport.ReportException(e, additionalStackTrace); } internal static T ReportingObject(UnityEngine.Object obj, Func action) { - if (obj != null) CurrentReport._references.Push(obj); - try - { - return action(); - } - catch (Exception e) - { - var additionalStackTrace = string.Join("\n", Environment.StackTrace.Split('\n').Skip(1)) + "\n"; - LogException(e, additionalStackTrace); - return default; - } - finally - { - if (obj != null) CurrentReport._references.Pop(); - } + return ErrorReport.WithContextObject(obj, action); } internal static void ReportingObject(UnityEngine.Object obj, Action action) { - ReportingObject(obj, () => - { - action(); - return true; - }); - } - - internal IEnumerable GetActiveReferences() - { - return _references.Select(o => new ObjectRef(o)); - } - - public static void Clear() - { - _report = new BuildReport(); + ErrorReport.WithContextObject(obj, action); } + [Obsolete("Use NDMF's ObjectRegistry instead")] public static void RemapPaths(string original, string cloned) { - foreach (var av in CurrentReport.Avatars) - { - av.objectRef = av.objectRef.Remap(original, cloned); - - foreach (var log in av.logs) - { - log.referencedObjects = log.referencedObjects.Select(o => o.Remap(original, cloned)).ToList(); - } - } - - ErrorReportUI.reloadErrorReport(); } } } \ No newline at end of file diff --git a/Editor/ErrorReporting/ErrorReportUI.cs b/Editor/ErrorReporting/ErrorReportUI.cs deleted file mode 100644 index 97f9b3a9..00000000 --- a/Editor/ErrorReporting/ErrorReportUI.cs +++ /dev/null @@ -1,308 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; -using nadena.dev.modular_avatar.core; -using nadena.dev.modular_avatar.core.editor; -using UnityEditor; -using UnityEngine; -using UnityEngine.UIElements; - -namespace nadena.dev.modular_avatar.editor.ErrorReporting -{ - internal class ErrorReportUI : EditorWindow - { - internal static Action reloadErrorReport = () => { }; - - [MenuItem("Tools/Modular Avatar/Show error report", false, 100)] - public static void OpenErrorReportUI() - { - GetWindow().Show(); - } - - public static void MaybeOpenErrorReportUI() - { - if (Application.isBatchMode) return; // headless unit tests - - if (BuildReport.CurrentReport.Avatars.Any(av => av.logs.Count > 0)) - { - OpenErrorReportUI(); - } - } - - private Vector2 _avatarScrollPos, _errorScrollPos; - private int _selectedAvatar = -1; - private List