diff --git a/CHANGELOG-PRERELEASE.md b/CHANGELOG-PRERELEASE.md index 9ccaeb1a9..5623495ac 100644 --- a/CHANGELOG-PRERELEASE.md +++ b/CHANGELOG-PRERELEASE.md @@ -8,6 +8,9 @@ The format is based on [Keep a Changelog]. ## [Unreleased] ### Added +- Error Reporting System `#124` + - This adds window shows errors on build + - This is based on Modular Avatar's Error Reporting Window. thanks `@bdunderscore` ### Changed diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f95a7d51..557ae424e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ The format is based on [Keep a Changelog]. ## [Unreleased] ### Added +- Error Reporting System `#124` + - This adds window shows errors on build + - This is based on Modular Avatar's Error Reporting Window. thanks `@bdunderscore` ### Changed diff --git a/Editor/AvatarGlobalComponentEditorBase.cs b/Editor/AvatarGlobalComponentEditorBase.cs index 58c28fba4..96a6ba1d2 100644 --- a/Editor/AvatarGlobalComponentEditorBase.cs +++ b/Editor/AvatarGlobalComponentEditorBase.cs @@ -1,12 +1,25 @@ +using Anatawa12.AvatarOptimizer.ErrorReporting; using CustomLocalization4EditorExtension; using UnityEditor; using UnityEngine; +using VRC.Core; using VRC.SDK3.Avatars.Components; namespace Anatawa12.AvatarOptimizer { + [InitializeOnLoad] abstract class AvatarGlobalComponentEditorBase : AvatarTagComponentEditorBase { + static AvatarGlobalComponentEditorBase() + { + ComponentValidation.RegisterValidator(component => + { + if (!component.GetComponent()) + return new[] { ErrorLog.Validation("AvatarGlobalComponent:NotOnAvatarDescriptor") }; + return null; + }); + } + protected override void OnInspectorGUIInner() { if (!((Component)serializedObject.targetObject).GetComponent()) diff --git a/Editor/MergePhysBoneEditor.cs b/Editor/MergePhysBoneEditor.cs index dd2208cfe..1caf28a80 100644 --- a/Editor/MergePhysBoneEditor.cs +++ b/Editor/MergePhysBoneEditor.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Anatawa12.AvatarOptimizer.ErrorReporting; using CustomLocalization4EditorExtension; using JetBrains.Annotations; using UnityEditor; @@ -371,7 +372,7 @@ protected override void TransformSection() { EditorGUILayout.LabelField("Multi Child Type", "Must be Ignore"); var multiChildType = _sourcePhysBone.FindProperty("multiChildType"); if (multiChildType.enumValueIndex != 0 || multiChildType.hasMultipleDifferentValues) - EditorGUILayout.HelpBox("Some PysBone has multi child type != Ignore", MessageType.Error); + EditorGUILayout.HelpBox(CL4EE.Tr("MergePhysBone:error:multiChildType"), MessageType.Error); } protected override void OptionParameter() { EditorGUILayout.PropertyField(_mergedPhysBone.FindProperty("parameter")); @@ -786,4 +787,138 @@ private static int PopupNoIndent(Rect position, int selectedIndex, string[] disp return result; } } + + [InitializeOnLoad] + sealed class MergePhysBoneValidator : MergePhysBoneEditorModificationUtils + { + private readonly List _errorLogs; + private readonly List _differProps = new List(); + private readonly MergePhysBone _mergePhysBone; + + static MergePhysBoneValidator() + { + ComponentValidation.RegisterValidator(Validate); + } + + private static List Validate(MergePhysBone mergePhysBone) + { + var list = new List(); + if (mergePhysBone.makeParent && mergePhysBone.transform.childCount != 0) + list.Add(ErrorLog.Validation("MergePhysBone:error:makeParentWithChildren", mergePhysBone)); + + new MergePhysBoneValidator(list, mergePhysBone).DoProcess(); + + return list; + } + + public MergePhysBoneValidator(List errorLogs, MergePhysBone mergePhysBone) + : base(new SerializedObject(mergePhysBone)) + { + _errorLogs = errorLogs; + _mergePhysBone = mergePhysBone; + } + + private static void Void() + { + } + + protected override void BeginPbConfig() => Void(); + protected override bool BeginSection(string name, string docTag) => true; + protected override void EndSection() => Void(); + protected override void EndPbConfig() { + if (_differProps.Count != 0) + { + _errorLogs.Add(ErrorLog.Validation("MergePhysBone:error:differValues", + new[] { string.Join(", ", _differProps) })); + } + } + + protected override void NoSource() => + _errorLogs.Add(ErrorLog.Validation("MergePhysBone:error:noSources")); + + protected override void TransformSection() + { + if (!_mergePhysBone.makeParent) + { + var differ = _sourcePhysBone.targetObjects.Cast() + .Select(x => x.transform.parent) + .ZipWithNext() + .Any(x => x.Item1 != x.Item2); + if (differ) + _errorLogs.Add(ErrorLog.Validation("MergePhysBone:error:parentDiffer")); + } + var multiChildType = _sourcePhysBone.FindProperty(nameof(VRCPhysBoneBase.multiChildType)); + if (multiChildType.enumValueIndex != 0 || multiChildType.hasMultipleDifferentValues) + _errorLogs.Add(ErrorLog.Validation("MergePhysBone:error:multiChildType")); + } + + protected override void OptionParameter() => Void(); + protected override void OptionIsAnimated() => Void(); + + protected override void UnsupportedPbVersion() => + _errorLogs.Add(ErrorLog.Validation("MergePhysBone:error:unsupportedPbVersion")); + + protected override void PbVersionProp(string label, string pbPropName, SerializedProperty overridePropName, + params SerializedProperty[] overrides) + => PbProp(label, pbPropName, overridePropName, overrides); + + protected override void PbProp(string label, string pbPropName, SerializedProperty overridePropName, + params SerializedProperty[] overrides) => + PbPropImpl(label, overridePropName, overrides, + () => _sourcePhysBone.FindProperty(pbPropName).hasMultipleDifferentValues); + + protected override void PbCurveProp(string label, string pbPropName, string pbCurvePropName, + SerializedProperty overridePropName, + params SerializedProperty[] overrides) => + PbPropImpl(label, overridePropName, overrides, + () => _sourcePhysBone.FindProperty(pbPropName).hasMultipleDifferentValues + || _sourcePhysBone.FindProperty(pbCurvePropName).hasMultipleDifferentValues); + + protected override void PbPermissionProp(string label, string pbPropName, string pbFilterPropName, + SerializedProperty overridePropName, + params SerializedProperty[] overrides) + { + PbPropImpl(label, overridePropName, overrides, () => + { + var sourceValueProp = _sourcePhysBone.FindProperty(pbPropName); + var sourceFilterProp = _sourcePhysBone.FindProperty(pbFilterPropName); + return sourceValueProp.hasMultipleDifferentValues + || sourceValueProp.enumValueIndex == 2 && sourceFilterProp.hasMultipleDifferentValues; + }); + } + + protected override void Pb3DCurveProp(string label, string pbPropName, string pbXCurveLabel, + string pbXCurvePropName, string pbYCurveLabel, + string pbYCurvePropName, string pbZCurveLabel, string pbZCurvePropName, SerializedProperty overridePropName, + params SerializedProperty[] overrides) => + PbPropImpl(label, overridePropName, overrides, () => + _sourcePhysBone.FindProperty(pbPropName).hasMultipleDifferentValues || + _sourcePhysBone.FindProperty(pbXCurvePropName).hasMultipleDifferentValues || + _sourcePhysBone.FindProperty(pbYCurvePropName).hasMultipleDifferentValues || + _sourcePhysBone.FindProperty(pbZCurvePropName).hasMultipleDifferentValues); + + private void PbPropImpl(string label, + SerializedProperty overrideProp, + SerializedProperty[] overrides, + Func copy) + { + if (overrides.Any(x => x.boolValue) || overrideProp.boolValue) return; + + // Copy mode + var differ = copy(); + + if (differ) + _differProps.Add(label); + } + + protected override void ColliderProp(string label, string pbProp, SerializedProperty overrideProp) + { + if (_mergePhysBone.colliders == CollidersSettings.Copy) + { + if (_sourcePhysBone.FindProperty(pbProp).hasMultipleDifferentValues) + _errorLogs.Add(ErrorLog.Validation("MergePhysBone:error:differValue", new[] { label })); + } + } + } + } diff --git a/Editor/MergeSkinnedMeshEditor.cs b/Editor/MergeSkinnedMeshEditor.cs index f5dabe68a..b3c7c0288 100644 --- a/Editor/MergeSkinnedMeshEditor.cs +++ b/Editor/MergeSkinnedMeshEditor.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Anatawa12.AvatarOptimizer.ErrorReporting; using CustomLocalization4EditorExtension; using UnityEditor; using UnityEngine; @@ -7,6 +8,7 @@ namespace Anatawa12.AvatarOptimizer { [CustomEditor(typeof(MergeSkinnedMesh))] + [InitializeOnLoad] internal class MergeSkinnedMeshEditor : AvatarTagComponentEditorBase { private static class Style @@ -24,6 +26,16 @@ private static class Style }; } + static MergeSkinnedMeshEditor() + { + ComponentValidation.RegisterValidator(component => + { + if (component.GetComponent().sharedMesh) + return new[] { ErrorLog.Warning("MergeSkinnedMesh:warning:MeshIsNotNone") }; + return null; + }); + } + SerializedProperty _renderersSetProp; SerializedProperty _staticRenderersSetProp; SerializedProperty _removeEmptyRendererObjectProp; diff --git a/Editor/OptimizerProcessor.cs b/Editor/OptimizerProcessor.cs index 2aaa97ae2..cce73c1d5 100644 --- a/Editor/OptimizerProcessor.cs +++ b/Editor/OptimizerProcessor.cs @@ -1,7 +1,9 @@ using System; +using Anatawa12.AvatarOptimizer.ErrorReporting; using UnityEditor; using UnityEngine; +using VRC.SDK3.Avatars.Components; using VRC.SDKBase.Editor.BuildPipeline; namespace Anatawa12.AvatarOptimizer @@ -49,8 +51,11 @@ private static void ProcessObject(OptimizerSession session) private static void DoProcessObject(OptimizerSession session) { - new Processors.UnusedBonesByReferencesToolEarlyProcessor().Process(session); - session.MarkDirtyAll(); + using (BuildReport.ReportingOnAvatar(session.GetRootComponent())) + { + new Processors.UnusedBonesByReferencesToolEarlyProcessor().Process(session); + session.MarkDirtyAll(); + } } } @@ -83,6 +88,7 @@ public static void ProcessObject(OptimizerSession session) { if (_processing) return; using (Utils.StartEditingScope(true)) + using (BuildReport.ReportingOnAvatar(session.GetRootComponent())) { try { diff --git a/Editor/Processors/ApplyObjectMapping.cs b/Editor/Processors/ApplyObjectMapping.cs index cf2cef741..e0355b74d 100644 --- a/Editor/Processors/ApplyObjectMapping.cs +++ b/Editor/Processors/ApplyObjectMapping.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Anatawa12.AvatarOptimizer.ErrorReporting; using UnityEditor; using UnityEditor.Animations; using UnityEngine; @@ -34,7 +35,9 @@ public void Apply(OptimizerSession session) if (mapper == null) mapper = new AnimatorControllerMapper(mapping, component.transform, session); - var mapped = mapper.MapAnimatorController(controller); + // ReSharper disable once AccessToModifiedClosure + var mapped = BuildReport.ReportingObject(controller, + () => mapper.MapAnimatorController(controller)); if (mapped != null) p.objectReferenceValue = mapped; } diff --git a/Editor/Processors/ClearEndpointPositionProcessor.cs b/Editor/Processors/ClearEndpointPositionProcessor.cs index 49d00ea82..7d69b1e33 100644 --- a/Editor/Processors/ClearEndpointPositionProcessor.cs +++ b/Editor/Processors/ClearEndpointPositionProcessor.cs @@ -1,3 +1,4 @@ +using Anatawa12.AvatarOptimizer.ErrorReporting; using UnityEditor; using UnityEngine; using VRC.Dynamics; @@ -9,7 +10,7 @@ internal class ClearEndpointPositionProcessor public void Process(OptimizerSession session) { foreach (var component in session.GetComponents()) - Process(component.GetComponent()); + BuildReport.ReportingObject(component, () => Process(component.GetComponent())); } public static void Process(VRCPhysBoneBase pb) diff --git a/Editor/Processors/DeleteGameObjectProcessor.cs b/Editor/Processors/DeleteGameObjectProcessor.cs index cb62b56d3..b32f10fbd 100644 --- a/Editor/Processors/DeleteGameObjectProcessor.cs +++ b/Editor/Processors/DeleteGameObjectProcessor.cs @@ -1,3 +1,4 @@ +using Anatawa12.AvatarOptimizer.ErrorReporting; using UnityEngine; namespace Anatawa12.AvatarOptimizer.Processors @@ -6,7 +7,7 @@ internal class DeleteGameObjectProcessor { public void Process(OptimizerSession session) { - foreach (var mergePhysBone in session.GetComponents()) + BuildReport.ReportingObjects(session.GetComponents(), mergePhysBone => { void Destroy(Object obj) { @@ -25,7 +26,7 @@ void Destroy(Object obj) Destroy(component); return true; }); - } + }); } } } diff --git a/Editor/Processors/EditSkinnedMeshComponentProcessor.cs b/Editor/Processors/EditSkinnedMeshComponentProcessor.cs index a87dea0d9..79dd37d11 100644 --- a/Editor/Processors/EditSkinnedMeshComponentProcessor.cs +++ b/Editor/Processors/EditSkinnedMeshComponentProcessor.cs @@ -23,6 +23,7 @@ public void Process(OptimizerSession session) foreach (var processor in processors.GetSorted()) { + // TODO processor.Process(session, target, holder); target.AssertInvariantContract( $"after {processor.GetType().Name} " + diff --git a/Editor/Processors/MakeChildrenProcessor.cs b/Editor/Processors/MakeChildrenProcessor.cs index a766f9824..e19d955c7 100644 --- a/Editor/Processors/MakeChildrenProcessor.cs +++ b/Editor/Processors/MakeChildrenProcessor.cs @@ -1,15 +1,17 @@ +using Anatawa12.AvatarOptimizer.ErrorReporting; + namespace Anatawa12.AvatarOptimizer.Processors { internal class MakeChildrenProcessor { public void Process(OptimizerSession session) { - foreach (var makeChildren in session.GetComponents()) + BuildReport.ReportingObjects(session.GetComponents(), makeChildren => { foreach (var makeChildrenChild in makeChildren.children.GetAsSet()) if (makeChildrenChild) makeChildrenChild.parent = makeChildren.transform; - } + }); } } } diff --git a/Editor/Processors/MergeBoneProcessor.cs b/Editor/Processors/MergeBoneProcessor.cs index 17e26fa5a..b4cac532f 100644 --- a/Editor/Processors/MergeBoneProcessor.cs +++ b/Editor/Processors/MergeBoneProcessor.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using Anatawa12.AvatarOptimizer.ErrorReporting; using Unity.Collections; using UnityEngine; using UnityEngine.Assertions; @@ -24,11 +25,11 @@ public void Process(OptimizerSession session) // normalize map mergeMapping.FlattenMapping(); - foreach (var renderer in session.GetComponents()) + BuildReport.ReportingObjects(session.GetComponents(), renderer => { if (renderer.bones.Where(x => x).Any(mergeMapping.ContainsKey)) DoBoneMap(session, renderer, mergeMapping); - } + }); foreach (var pair in mergeMapping) { diff --git a/Editor/Processors/MergePhysBoneProcessor.cs b/Editor/Processors/MergePhysBoneProcessor.cs index 389ad6755..6b964d655 100644 --- a/Editor/Processors/MergePhysBoneProcessor.cs +++ b/Editor/Processors/MergePhysBoneProcessor.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Anatawa12.AvatarOptimizer.ErrorReporting; using CustomLocalization4EditorExtension; using UnityEditor; using UnityEngine; @@ -13,10 +14,8 @@ internal class MergePhysBoneProcessor { public void Process(OptimizerSession session) { - foreach (var mergePhysBone in session.GetComponents()) - { - DoMerge(mergePhysBone, session); - } + BuildReport.ReportingObjects(session.GetComponents(), + mergePhysBone => DoMerge(mergePhysBone, session)); } private static bool SetEq(IEnumerable a, IEnumerable b) => @@ -38,7 +37,7 @@ internal static void DoMerge(MergePhysBone merge, OptimizerSession session) root = merge.transform; if (root.childCount != 0) - throw new InvalidOperationException(CL4EE.Tr("MergePhysBone:error:makeParentWithChildren")); + return; // error reported by validator foreach (var physBone in sourceComponents) physBone.GetTarget().parent = root; @@ -51,7 +50,7 @@ internal static void DoMerge(MergePhysBone merge, OptimizerSession session) .Any(x => x.Item1 != x.Item2); if (parentDiffer) - throw new InvalidOperationException(CL4EE.Tr("MergePhysBone:error:parentDiffer")); + return; // differ error reported by validator if (sourceComponents.Count == pb.GetTarget().parent.childCount) { @@ -126,16 +125,19 @@ protected override void EndPbConfig() { } - protected override void NoSource() => throw new InvalidOperationException("No sources"); + protected override void NoSource() + { + // differ error reported by validator + } - protected override void UnsupportedPbVersion() => - throw new InvalidOperationException("Unsupported Pb Version"); + protected override void UnsupportedPbVersion() + { + // differ error reported by validator + } protected override void TransformSection() { - var multiChildType = _sourcePhysBone.FindProperty(nameof(VRCPhysBoneBase.multiChildType)); - if (multiChildType.enumValueIndex != 0 || multiChildType.hasMultipleDifferentValues) - throw new InvalidOperationException("Some PysBone has multi child type != Ignore"); + // differ error reported by validator } protected override void OptionParameter() @@ -157,12 +159,7 @@ protected override void PbProp(string label, SerializedProperty overridePropName, params SerializedProperty[] overrides) { - PbPropImpl(label, overridePropName, overrides, () => - { - var sourceProp = _sourcePhysBone.FindProperty(pbPropName); - _mergedPhysBone.FindProperty(pbPropName).CopyDataFrom(sourceProp); - return sourceProp.hasMultipleDifferentValues; - }); + PbPropImpl(label, overridePropName, overrides, pbPropName); } protected override void PbCurveProp(string label, @@ -171,15 +168,7 @@ protected override void PbCurveProp(string label, SerializedProperty overridePropName, params SerializedProperty[] overrides) { - PbPropImpl(label, overridePropName, overrides, () => - { - var sourceValueProp = _sourcePhysBone.FindProperty(pbPropName); - _mergedPhysBone.FindProperty(pbPropName).CopyDataFrom(sourceValueProp); - var sourceCurveProp = _sourcePhysBone.FindProperty(pbCurvePropName); - _mergedPhysBone.FindProperty(pbCurvePropName).CopyDataFrom(sourceCurveProp); - - return sourceValueProp.hasMultipleDifferentValues || sourceCurveProp.hasMultipleDifferentValues; - }); + PbPropImpl(label, overridePropName, overrides, pbPropName, pbCurvePropName); } protected override void PbPermissionProp(string label, @@ -188,28 +177,7 @@ protected override void PbPermissionProp(string label, SerializedProperty overridePropName, params SerializedProperty[] overrides) { - PbPropImpl(label, overridePropName, overrides, () => - { - var sourceValueProp = _sourcePhysBone.FindProperty(pbPropName); - _mergedPhysBone.FindProperty(pbPropName).CopyDataFrom(sourceValueProp); - - if (sourceValueProp.enumValueIndex == 2) - { - var sourceFilterProp = _sourcePhysBone.FindProperty(pbFilterPropName); - var mergedFilterProp = _mergedPhysBone.FindProperty(pbFilterPropName); - - var sourceAllowSelf = sourceFilterProp.FindPropertyRelative("allowSelf"); - mergedFilterProp.FindPropertyRelative("allowSelf").CopyDataFrom(sourceAllowSelf); - var sourceAllowOthers = sourceFilterProp.FindPropertyRelative("allowOthers"); - mergedFilterProp.FindPropertyRelative("allowOthers").CopyDataFrom(sourceAllowOthers); - - return sourceValueProp.hasMultipleDifferentValues || sourceFilterProp.hasMultipleDifferentValues; - } - else - { - return sourceValueProp.hasMultipleDifferentValues; - } - }); + PbPropImpl(label, overridePropName, overrides, pbPropName, pbFilterPropName); } protected override void Pb3DCurveProp(string label, @@ -220,40 +188,21 @@ protected override void Pb3DCurveProp(string label, SerializedProperty overridePropName, params SerializedProperty[] overrides) { - PbPropImpl(label, overridePropName, overrides, () => - { - var sourceValueProp = _sourcePhysBone.FindProperty(pbPropName); - var sourceXCurveProp = _sourcePhysBone.FindProperty(pbXCurvePropName); - var sourceYCurveProp = _sourcePhysBone.FindProperty(pbYCurvePropName); - var sourceZCurveProp = _sourcePhysBone.FindProperty(pbZCurvePropName); - _mergedPhysBone.FindProperty(pbPropName).CopyDataFrom(sourceValueProp); - _mergedPhysBone.FindProperty(pbXCurvePropName).CopyDataFrom(sourceXCurveProp); - _mergedPhysBone.FindProperty(pbYCurvePropName).CopyDataFrom(sourceYCurveProp); - _mergedPhysBone.FindProperty(pbZCurvePropName).CopyDataFrom(sourceZCurveProp); - - return sourceValueProp.hasMultipleDifferentValues - || sourceXCurveProp.hasMultipleDifferentValues - || sourceYCurveProp.hasMultipleDifferentValues - || sourceZCurveProp.hasMultipleDifferentValues; - }); + PbPropImpl(label, overridePropName, overrides, pbPropName, + pbXCurvePropName, pbYCurvePropName, pbZCurvePropName); } private void PbPropImpl(string label, SerializedProperty overrideProp, SerializedProperty[] overrides, - Func copy) + params string[] props) { if (overrides.Any(x => x.boolValue) || overrideProp.boolValue) return; // Copy mode - var differ = copy(); - - if (differ) - { - throw new InvalidOperationException( - $"The value of {label} is differ between two or more sources. " + - "You have to set same value OR override this property"); - } + // differ error reported by validator + foreach (var prop in props) + _mergedPhysBone.FindProperty(prop).CopyDataFrom(_sourcePhysBone.FindProperty(prop)); } protected override void ColliderProp(string label, string pbProp, SerializedProperty overrideProp) diff --git a/Editor/Processors/UnusedBonesByReferencesToolEarlyProcessor.cs b/Editor/Processors/UnusedBonesByReferencesToolEarlyProcessor.cs index afe5fac1b..c33df6ae9 100644 --- a/Editor/Processors/UnusedBonesByReferencesToolEarlyProcessor.cs +++ b/Editor/Processors/UnusedBonesByReferencesToolEarlyProcessor.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; +using Anatawa12.AvatarOptimizer.ErrorReporting; using UnityEngine; namespace Anatawa12.AvatarOptimizer.Processors @@ -13,9 +14,12 @@ public void Process(OptimizerSession session) var configuration = session.GetRootComponent(); if (!configuration) return; - UnusedBonesByReferences.Make(BoneReference.Make(configuration.transform, - configuration.detectExtraChild), configuration.preserveEndBone) - .SetEditorOnlyToBones(); + BuildReport.ReportingObject(configuration, () => + { + UnusedBonesByReferences.Make(BoneReference.Make(configuration.transform, + configuration.detectExtraChild), configuration.preserveEndBone) + .SetEditorOnlyToBones(); + }); } #region UnusedBonesByReferencesTool diff --git a/Editor/com.anatawa12.avatar-optimizer.editor.asmdef b/Editor/com.anatawa12.avatar-optimizer.editor.asmdef index 5958d1d77..0779fbe16 100644 --- a/Editor/com.anatawa12.avatar-optimizer.editor.asmdef +++ b/Editor/com.anatawa12.avatar-optimizer.editor.asmdef @@ -1,6 +1,8 @@ { "name": "com.anatawa12.avatar-optimizer.editor", "references": [ + "GUID:b23bdfe8d18741a29e869995d47026d0", + "GUID:c03011c168164e1a954438f640cbd728", "GUID:295ffbe0b63504ae3a7879cf089501ba", "GUID:8264e72376854221affe9980c91c2fff", "GUID:f69eeb3e25674f4a9bd20e6d7e69e0e6", diff --git a/Internal/ErrorReporter.meta b/Internal/ErrorReporter.meta new file mode 100644 index 000000000..0bf0afc5d --- /dev/null +++ b/Internal/ErrorReporter.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5b6a11b6cf8b4cb19d98e8b9133516b2 +timeCreated: 1682837060 diff --git a/Internal/ErrorReporter/Editor.meta b/Internal/ErrorReporter/Editor.meta new file mode 100644 index 000000000..41a972661 --- /dev/null +++ b/Internal/ErrorReporter/Editor.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 950e3fae7aac4e6594cb4e0854338f8f +timeCreated: 1682838469 diff --git a/Internal/ErrorReporter/Editor/.generate.ts b/Internal/ErrorReporter/Editor/.generate.ts new file mode 100644 index 000000000..718410f8c --- /dev/null +++ b/Internal/ErrorReporter/Editor/.generate.ts @@ -0,0 +1,21 @@ +// Execute this script on the time changed this script. + +console.log(`// this file is generated with .generate.ts`) +console.log(``) +console.log(`using System;`) +console.log(`using System.Reflection;`) +console.log(``) +console.log(`namespace Anatawa12.AvatarOptimizer.ErrorReporting`) +console.log(`{`) +console.log(` public partial class ErrorLog`) +console.log(` {`) +for (const level of ["Validation", "Info", "Warning", "Error"]) { + console.log(` public static ErrorLog ${level}(string code, string[] strings, params object[] args)`) + console.log(` => new ErrorLog(ReportLevel.${level}, code, strings, args, Assembly.GetCallingAssembly());`) + console.log(``) + console.log(` public static ErrorLog ${level}(string code, params object[] args)`) + console.log(` => new ErrorLog(ReportLevel.${level}, code, Array.Empty(), args, Assembly.GetCallingAssembly());`) + console.log(``) +} +console.log(` }`) +console.log(`}`) diff --git a/Internal/ErrorReporter/Editor/BuildReport.cs b/Internal/ErrorReporter/Editor/BuildReport.cs new file mode 100644 index 000000000..87729c955 --- /dev/null +++ b/Internal/ErrorReporter/Editor/BuildReport.cs @@ -0,0 +1,210 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using UnityEditor; +using UnityEngine; +using VRC.SDK3.Avatars.Components; +using Object = UnityEngine.Object; + +namespace Anatawa12.AvatarOptimizer.ErrorReporting +{ + [Serializable] + internal class AvatarReport + { + [SerializeField] internal ObjectRef objectRef; + [SerializeField] internal bool successful; + [SerializeField] internal List logs = new List(); + } + + [InitializeOnLoad] + [Serializable] + public class BuildReport + { + private const string Path = "Library/com.anatawa12.error-reporting.json"; + + private static BuildReport _report; + private Stack _references = new Stack(); + + [SerializeField] internal List avatars = new List(); + + internal ConditionalWeakTable AvatarsByObject = + new ConditionalWeakTable(); + private AvatarReport CurrentAvatar { get; set; } + + internal static BuildReport CurrentReport + { + get + { + if (_report == null) _report = LoadReport() ?? new BuildReport(); + return _report; + } + } + + static BuildReport() + { + 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 JsonUtility.FromJson(data); + } + catch (Exception e) + { + return null; + } + } + + internal static void SaveReport() + { + var report = CurrentReport; + var json = JsonUtility.ToJson(report); + + File.WriteAllText(Path, json); + + ErrorReportUI.reloadErrorReport(); + } + + private class AvatarReportScope : IDisposable + { + public void Dispose() + { + var successful = CurrentReport.CurrentAvatar.successful; + CurrentReport.CurrentAvatar = null; + BuildReport.SaveReport(); + ErrorReportUI.MaybeOpenErrorReportUI(); + if (!successful) throw new Exception("Avatar processing failed"); + } + } + + public static IDisposable ReportingOnAvatar(VRCAvatarDescriptor descriptor) + { + if (descriptor != null) + { + if (!CurrentReport.AvatarsByObject.TryGetValue(descriptor, out var report)) + { + Debug.LogWarning("Reporting on Avatar is called before ErrorReporting Initializer Processor"); + report = CurrentReport.Initialize(descriptor); + } + CurrentReport.CurrentAvatar = report; + } + + return new AvatarReportScope(); + } + + internal AvatarReport Initialize(VRCAvatarDescriptor descriptor) + { + if (descriptor == null) return null; + + AvatarReport report = new AvatarReport(); + report.objectRef = new ObjectRef(descriptor.gameObject); + avatars.Add(report); + report.successful = true; + + report.logs.AddRange(ComponentValidation.ValidateAll(descriptor.gameObject)); + + AvatarsByObject.Add(descriptor, report); + return report; + } + + 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"); + } + } + + 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)); + } + } + + public 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(); + } + } + + public static void ReportingObject(UnityEngine.Object obj, Action action) + { + ReportingObject(obj, () => + { + action(); + return true; + }); + } + + public static void ReportingObjects(IEnumerable objs, Action action) where T : Object + { + foreach (var obj in objs) + ReportingObject(obj, () => action(obj)); + } + + internal IEnumerable GetActiveReferences() + { + return _references.Select(o => new ObjectRef(o)); + } + + public static void Clear() + { + _report = new BuildReport(); + } + } +} diff --git a/Internal/ErrorReporter/Editor/BuildReport.cs.meta b/Internal/ErrorReporter/Editor/BuildReport.cs.meta new file mode 100644 index 000000000..837b43fbd --- /dev/null +++ b/Internal/ErrorReporter/Editor/BuildReport.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ca68f5432bfe4dd18d55a1e33a4a7bd0 +timeCreated: 1683098837 \ No newline at end of file diff --git a/Internal/ErrorReporter/Editor/ComponentValidation.cs b/Internal/ErrorReporter/Editor/ComponentValidation.cs new file mode 100644 index 000000000..8dd559bd1 --- /dev/null +++ b/Internal/ErrorReporter/Editor/ComponentValidation.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace Anatawa12.AvatarOptimizer.ErrorReporting +{ + using Validator = Func>; + + public static class ComponentValidation + { + private static readonly ConditionalWeakTable Validators = new ConditionalWeakTable(); + + internal static List ValidateAll(GameObject root) + { + return root.GetComponentsInChildren(true) + .SelectMany(Validate) + .ToList(); + } + + private static IEnumerable Validate(IStaticValidated component) => + GetValidator(component.GetType()) + ?.Invoke(component) + ?.OnEach(x => x.referencedObjects.Add(new ObjectRef((Object)component))) + ?? Array.Empty(); + + private static Validator GetValidator(Type type) + { + // fast path: registered + if (Validators.TryGetValue(type, out var validator)) + return validator; + + // find validators + var finding = type; + while (finding != null && typeof(IStaticValidated).IsAssignableFrom(finding)) + { + if (Validators.TryGetValue(finding, out validator)) + break; + + finding = finding.BaseType; + } + + if (validator == null) + { + // if not found, make warning and set empty validator + Debug.LogWarning($"The StaticValidator for {type} not found. This must be a bug of {type.Assembly}"); + validator = _ => null; + } + + Validators.Add(type, validator); + return validator; + } + + /// + /// Registers validator. + /// + /// The validator to be registered. + /// + /// If the type is invalid. The type is invalid if + ///
    + ///
  • The type is interface
  • + ///
  • The type is static class
  • + ///
  • The type does not implements IStaticValidated, or
  • + ///
  • The type implements ISelfStaticValidated
  • + ///
+ ///
+ public static void RegisterValidator(Func> validator) + where T : IStaticValidated + { + RegisterValidator(typeof(T), x => validator((T)x)); + } + + /// + /// Registers validator for the specified type. + /// + /// The type validator is for. + /// The validator to be registered. + /// + /// If the type is invalid. The type is invalid if + ///
    + ///
  • The type is interface
  • + ///
  • The type is static class
  • + ///
  • The type does not implements IStaticValidated, or
  • + ///
+ ///
+ public static void RegisterValidator([NotNull] Type type, [NotNull] Validator validator) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + if (validator == null) throw new ArgumentNullException(nameof(validator)); + if (type.IsInterface) + throw new ArgumentException("You cannot register Validator for interfaces."); + if (type.IsSealed && type.IsAbstract) + throw new ArgumentException("You cannot register Validator for static class."); + if (!typeof(IStaticValidated).IsAssignableFrom(type)) + throw new ArgumentException( + "You cannot register Validator for class does not implements IStaticValidated."); + + Validators.Add(type, validator); + } + } +} diff --git a/Internal/ErrorReporter/Editor/ComponentValidation.cs.meta b/Internal/ErrorReporter/Editor/ComponentValidation.cs.meta new file mode 100644 index 000000000..e589c3181 --- /dev/null +++ b/Internal/ErrorReporter/Editor/ComponentValidation.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3340de0ad53d42d1aa990be35777457a +timeCreated: 1682840642 diff --git a/Internal/ErrorReporter/Editor/ErrorElement.cs b/Internal/ErrorReporter/Editor/ErrorElement.cs new file mode 100644 index 000000000..60085e995 --- /dev/null +++ b/Internal/ErrorReporter/Editor/ErrorElement.cs @@ -0,0 +1,109 @@ +using System; +using System.Linq; +using CustomLocalization4EditorExtension; +using UnityEditor; +using UnityEngine; +using UnityEngine.SceneManagement; +using UnityEngine.UIElements; + +namespace Anatawa12.AvatarOptimizer.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 = CL4EE.Tr("ErrorReporting: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 + { + var assembly = log.MessageAssembly; + if (assembly == null) + return string.Format(log.messageCode, objArray); + return string.Format(CL4EE.GetLocalization(log.messageAssembly)?.Tr(log.messageCode) ?? 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); + } + } + } +} diff --git a/Internal/ErrorReporter/Editor/ErrorElement.cs.meta b/Internal/ErrorReporter/Editor/ErrorElement.cs.meta new file mode 100644 index 000000000..c264581f9 --- /dev/null +++ b/Internal/ErrorReporter/Editor/ErrorElement.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: fdf6fd69b5e34039b10619c37ac3b3f3 +timeCreated: 1682840642 diff --git a/Internal/ErrorReporter/Editor/ErrorLog.cs b/Internal/ErrorReporter/Editor/ErrorLog.cs new file mode 100644 index 000000000..b01e619dc --- /dev/null +++ b/Internal/ErrorReporter/Editor/ErrorLog.cs @@ -0,0 +1,223 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using JetBrains.Annotations; +//using Newtonsoft.Json; +using UnityEngine; +using UnityEditor; +using UnityEngine.SceneManagement; +using Object = UnityEngine.Object; + +namespace Anatawa12.AvatarOptimizer.ErrorReporting +{ + internal class ObjectRefLookupCache + { + private readonly Dictionary<(string, long), Object> _cache = new Dictionary<(string, long), Object>(); + + internal Object FindByGuidAndLocalId(string guid, long localId) + { + if (!_cache.TryGetValue((guid, localId), out var obj)) + { + if (GlobalObjectId.TryParse($"GlobalObjectId_V1-{1}-{guid}-{localId}-{0}", out var goid)) + { + obj = GlobalObjectId.GlobalObjectIdentifierToObjectSlow(goid); + if (obj) _cache[(guid, localId)] = obj; + } + } + + return obj; + } + } + + [Serializable] + internal struct ObjectRef + { + [SerializeField] internal string guid; + // 0 is null. + [SerializeField] internal long localId; + [SerializeField] internal string path, name; + [SerializeField] internal string typeName; + + internal ObjectRef(Object obj) + { + if (obj == null) + { + this = default; + } + else + { + var name = string.IsNullOrWhiteSpace(obj.name) ? "" : obj.name; + if (obj is Component c) + { + this = new ObjectRef( + path: Utils.RelativePath(null, c.gameObject), + name: name, + typeName: obj.GetType().Name); + } + else if (obj is GameObject go) + { + this = new ObjectRef( + path: Utils.RelativePath(null, go), + name: name, + typeName: obj.GetType().Name); + } + else if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(obj, out var guid, out long id)) + { + this = new ObjectRef(guid, id, name, obj.GetType().Name); + } + else + { + this = default; // fallback + } + } + } + + private ObjectRef([NotNull] string path, [NotNull] string name, [NotNull] string typeName) + { + guid = null; + localId = 0; + this.path = path ?? throw new ArgumentNullException(nameof(path)); + this.name = name ?? throw new ArgumentNullException(nameof(name)); + this.typeName = typeName ?? throw new ArgumentNullException(nameof(typeName)); + } + + private ObjectRef([NotNull] string guid, long localId, [NotNull] string name, [NotNull] string typeName) + { + this.guid = guid ?? throw new ArgumentNullException(nameof(guid)); + if (localId == 0) throw new ArgumentOutOfRangeException(nameof(guid), "guid must not be zero"); + this.localId = localId; + path = null; + this.name = name ?? throw new ArgumentNullException(nameof(name)); + this.typeName = typeName ?? throw new ArgumentNullException(nameof(typeName)); + } + + internal Object Lookup(ObjectRefLookupCache cache) + { + if (path != null) + return FindObject(path); + if (guid != null && localId != 0) + return cache.FindByGuidAndLocalId(guid, localId); + 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 + "/", StringComparison.Ordinal)) + { + return root.transform.Find(path.Substring(root.name.Length + 1))?.gameObject; + } + } + + return null; + } + + public ObjectRef Remap(string original, string cloned) + { + if (path == cloned) + { + return new ObjectRef( + path: original, + name: path.Substring(path.LastIndexOf('/') + 1), + typeName: typeName); + } + if (path != null && path.StartsWith(cloned + "/", StringComparison.Ordinal)) + { + return new ObjectRef( + path: original + path.Substring(cloned.Length), + name: path.Substring(path.LastIndexOf('/') + 1), + typeName: typeName); + } + + return this; + } + } + + public enum ReportLevel + { + Validation, + Info, + Warning, + Error, + InternalError, + } + + [Serializable] + public partial class ErrorLog + { + [SerializeField] internal List referencedObjects; + [SerializeField] internal ReportLevel reportLevel; + internal Assembly messageAssembly; + [SerializeField] internal string messageAssemblyName; + [SerializeField] internal string messageCode; + [SerializeField] internal string[] substitutions; + [SerializeField] internal string stacktrace; + + [CanBeNull] + internal Assembly MessageAssembly + { + get + { + if (messageAssembly == null) + { + try + { + messageAssembly = Assembly.Load(messageAssemblyName); + } + catch + { + // ignored + } + } + + return messageAssembly; + } + } + + public ErrorLog(ReportLevel level, string code, string[] strings, object[] args, Assembly callerAssembly) + { + reportLevel = level; + messageAssembly = callerAssembly; + messageAssemblyName = messageAssembly.GetName().Name; + + 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; + } + + public ErrorLog(ReportLevel level, string code, string[] strings, params object[] args) + : this(level, code, strings, args, Assembly.GetCallingAssembly()) + { + } + + public ErrorLog(ReportLevel level, string code, params object[] args) + : this(level, code, Array.Empty(), args, Assembly.GetCallingAssembly()) + { + } + + internal ErrorLog(Exception e, string additionalStackTrace = "") + : this(ReportLevel.InternalError, + "ErrorReporter:error.internal_error", + new [] {e.Message, e.TargetSite?.Name}, + Array.Empty(), + typeof(ErrorLog).Assembly) + { + stacktrace = e.ToString() + additionalStackTrace; + } + + public string ToString() + { + return "[" + reportLevel + "] " + messageCode + " " + "subst: " + string.Join(", ", substitutions); + } + } +} diff --git a/Internal/ErrorReporter/Editor/ErrorLog.cs.meta b/Internal/ErrorReporter/Editor/ErrorLog.cs.meta new file mode 100644 index 000000000..620ce84fb --- /dev/null +++ b/Internal/ErrorReporter/Editor/ErrorLog.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b4087c2d2b42442884386c60e0f2036d +timeCreated: 1682840642 diff --git a/Internal/ErrorReporter/Editor/ErrorReportUI.cs b/Internal/ErrorReporter/Editor/ErrorReportUI.cs new file mode 100644 index 000000000..a177d7abe --- /dev/null +++ b/Internal/ErrorReporter/Editor/ErrorReportUI.cs @@ -0,0 +1,308 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; +using UnityEngine.UIElements; + +namespace Anatawa12.AvatarOptimizer.ErrorReporting +{ + internal class ErrorReportUI : EditorWindow + { + internal static Action reloadErrorReport = () => { }; + + [MenuItem("Tools/Avatar Optimizer/Show error report", false, 100)] + public static void OpenErrorReportUI() + { + GetWindow().Show(); + } + + public static void MaybeOpenErrorReportUI() + { + if (BuildReport.CurrentReport.avatars.Any(av => av.logs.Count > 0)) + { + OpenErrorReportUI(); + } + } + + private Vector2 _avatarScrollPos, _errorScrollPos; + private int _selectedAvatar = -1; + private List