feat: Add error reporting UI

This commit is contained in:
bd_ 2023-01-19 21:32:44 +09:00
parent 2a8c2ec3ce
commit c2e6bb53cd
35 changed files with 1540 additions and 276 deletions

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
@ -61,10 +62,13 @@ namespace nadena.dev.modular_avatar.core.editor
{
if (!layer.isDefault && layer.animatorController is AnimatorController ac && Util.IsTemporaryAsset(ac))
{
foreach (var state in Util.States(ac))
BuildReport.ReportingObject(ac, () =>
{
RegisterState(state);
}
foreach (var state in Util.States(ac))
{
RegisterState(state);
}
});
}
}
}

View File

@ -25,6 +25,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
@ -77,8 +78,8 @@ namespace nadena.dev.modular_avatar.core.editor
{
if (acp.type != param.type)
{
throw new Exception(
$"Parameter {param.name} has different types in {basePath} and {controller.name}");
BuildReport.LogFatal("error.merge_animator.param_type_mismatch", param.name, acp.type,
param.type);
}
continue;

View File

@ -27,10 +27,13 @@ using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using UnityEditor;
using UnityEditor.Build.Reporting;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
using VRC.SDKBase.Editor.BuildPipeline;
using BuildReport = nadena.dev.modular_avatar.editor.ErrorReporting.BuildReport;
using Object = UnityEngine.Object;
[assembly: InternalsVisibleTo("Tests")]
@ -83,17 +86,25 @@ namespace nadena.dev.modular_avatar.core.editor
savePath = basePath + " " + (++extension);
}
string originalBasePath = RuntimeUtil.RelativePath(null, avatar);
avatar = Object.Instantiate(avatar);
string clonedBasePath = RuntimeUtil.RelativePath(null, avatar);
try
{
Util.OverridePath = savePath;
avatar = Object.Instantiate(avatar);
var original = avatar;
avatar.transform.position += Vector3.forward * 2;
BuildReport.Clear();
ProcessAvatar(avatar);
}
finally
{
Util.OverridePath = null;
BuildReport.RemapPaths(originalBasePath, clonedBasePath);
}
}
@ -114,6 +125,7 @@ namespace nadena.dev.modular_avatar.core.editor
{
try
{
BuildReport.Clear();
ProcessAvatar(avatarGameObject);
FixupAnimatorDebugData(avatarGameObject);
return true;
@ -129,71 +141,76 @@ namespace nadena.dev.modular_avatar.core.editor
{
if (nowProcessing) return;
try
var vrcAvatarDescriptor = avatarGameObject.GetComponent<VRCAvatarDescriptor>();
using (BuildReport.CurrentReport.ReportingOnAvatar(vrcAvatarDescriptor))
{
AssetDatabase.StartAssetEditing();
nowProcessing = true;
var vrcAvatarDescriptor = avatarGameObject.GetComponent<VRCAvatarDescriptor>();
BoneDatabase.ResetBones();
PathMappings.Init(vrcAvatarDescriptor.gameObject);
ClonedMenuMappings.Clear();
// Sometimes people like to nest one avatar in another, when transplanting clothing. To avoid issues
// with inconsistently determining the avatar root, we'll go ahead and remove the extra sub-avatars
// here.
foreach (Transform directChild in avatarGameObject.transform)
try
{
foreach (var component in directChild.GetComponentsInChildren<VRCAvatarDescriptor>(true))
AssetDatabase.StartAssetEditing();
nowProcessing = true;
BoneDatabase.ResetBones();
PathMappings.Init(vrcAvatarDescriptor.gameObject);
ClonedMenuMappings.Clear();
// Sometimes people like to nest one avatar in another, when transplanting clothing. To avoid issues
// with inconsistently determining the avatar root, we'll go ahead and remove the extra sub-avatars
// here.
foreach (Transform directChild in avatarGameObject.transform)
{
Object.DestroyImmediate(component);
foreach (var component in directChild.GetComponentsInChildren<VRCAvatarDescriptor>(true))
{
Object.DestroyImmediate(component);
}
foreach (var component in directChild.GetComponentsInChildren<PipelineSaver>(true))
{
Object.DestroyImmediate(component);
}
}
foreach (var component in directChild.GetComponentsInChildren<PipelineSaver>(true))
var context = new BuildContext(vrcAvatarDescriptor);
new RenameParametersHook().OnPreprocessAvatar(avatarGameObject, context);
new MergeAnimatorProcessor().OnPreprocessAvatar(avatarGameObject, context);
context.AnimationDatabase.Bootstrap(vrcAvatarDescriptor);
new MenuInstallHook().OnPreprocessAvatar(avatarGameObject, context);
new MergeArmatureHook().OnPreprocessAvatar(context, avatarGameObject);
new BoneProxyProcessor().OnPreprocessAvatar(avatarGameObject);
new VisibleHeadAccessoryProcessor(vrcAvatarDescriptor).Process(context);
new RemapAnimationPass(vrcAvatarDescriptor).Process(context.AnimationDatabase);
new BlendshapeSyncAnimationProcessor().OnPreprocessAvatar(avatarGameObject, context);
PhysboneBlockerPass.Process(avatarGameObject);
context.AnimationDatabase.Commit();
AfterProcessing?.Invoke(avatarGameObject);
}
finally
{
AssetDatabase.StopAssetEditing();
nowProcessing = false;
// Ensure that we clean up AvatarTagComponents after failed processing. This ensures we don't re-enter
// processing from the Awake method on the unprocessed AvatarTagComponents
foreach (var component in avatarGameObject.GetComponentsInChildren<AvatarTagComponent>(true))
{
Object.DestroyImmediate(component);
UnityEngine.Object.DestroyImmediate(component);
}
var activator = avatarGameObject.GetComponent<AvatarActivator>();
if (activator != null)
{
UnityEngine.Object.DestroyImmediate(activator);
}
ClonedMenuMappings.Clear();
ErrorReportUI.MaybeOpenErrorReportUI();
}
var context = new BuildContext(vrcAvatarDescriptor);
new RenameParametersHook().OnPreprocessAvatar(avatarGameObject, context);
new MergeAnimatorProcessor().OnPreprocessAvatar(avatarGameObject, context);
context.AnimationDatabase.Bootstrap(vrcAvatarDescriptor);
new MenuInstallHook().OnPreprocessAvatar(avatarGameObject, context);
new MergeArmatureHook().OnPreprocessAvatar(context, avatarGameObject);
new BoneProxyProcessor().OnPreprocessAvatar(avatarGameObject);
new VisibleHeadAccessoryProcessor(vrcAvatarDescriptor).Process(context);
new RemapAnimationPass(vrcAvatarDescriptor).Process(context.AnimationDatabase);
new BlendshapeSyncAnimationProcessor().OnPreprocessAvatar(avatarGameObject, context);
PhysboneBlockerPass.Process(avatarGameObject);
context.AnimationDatabase.Commit();
AfterProcessing?.Invoke(avatarGameObject);
}
finally
{
AssetDatabase.StopAssetEditing();
nowProcessing = false;
// Ensure that we clean up AvatarTagComponents after failed processing. This ensures we don't re-enter
// processing from the Awake method on the unprocessed AvatarTagComponents
foreach (var component in avatarGameObject.GetComponentsInChildren<AvatarTagComponent>(true))
{
UnityEngine.Object.DestroyImmediate(component);
}
var activator = avatarGameObject.GetComponent<AvatarActivator>();
if (activator != null)
{
UnityEngine.Object.DestroyImmediate(activator);
}
ClonedMenuMappings.Clear();
}
}

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
@ -78,31 +79,7 @@ namespace nadena.dev.modular_avatar.core.editor
foreach (var component in components)
{
var targetObj = RuntimeUtil.RelativePath(avatarDescriptor.gameObject, component.gameObject);
foreach (var binding in component.Bindings)
{
var refObj = binding.ReferenceMesh.Get(component);
if (refObj == null) continue;
var refSmr = refObj.GetComponent<SkinnedMeshRenderer>();
if (refSmr == null) continue;
var refPath = RuntimeUtil.RelativePath(avatarDescriptor.gameObject, refObj);
var srcBinding = new SummaryBinding(refPath, binding.Blendshape);
if (!_bindingMappings.TryGetValue(srcBinding, out var dstBindings))
{
dstBindings = new List<SummaryBinding>();
_bindingMappings[srcBinding] = dstBindings;
}
var targetBlendshapeName = string.IsNullOrWhiteSpace(binding.LocalBlendshape)
? binding.Blendshape
: binding.LocalBlendshape;
dstBindings.Add(new SummaryBinding(targetObj, targetBlendshapeName));
}
BuildReport.ReportingObject(component, () => ProcessComponent(avatarDescriptor, component));
}
// Walk and transform all clips
@ -110,11 +87,41 @@ namespace nadena.dev.modular_avatar.core.editor
{
if (clip.CurrentClip is AnimationClip anim)
{
clip.CurrentClip = TransformMotion(anim);
BuildReport.ReportingObject(clip.CurrentClip,
() => { clip.CurrentClip = TransformMotion(anim); });
}
});
}
private void ProcessComponent(VRCAvatarDescriptor avatarDescriptor, ModularAvatarBlendshapeSync component)
{
var targetObj = RuntimeUtil.RelativePath(avatarDescriptor.gameObject, component.gameObject);
foreach (var binding in component.Bindings)
{
var refObj = binding.ReferenceMesh.Get(component);
if (refObj == null) continue;
var refSmr = refObj.GetComponent<SkinnedMeshRenderer>();
if (refSmr == null) continue;
var refPath = RuntimeUtil.RelativePath(avatarDescriptor.gameObject, refObj);
var srcBinding = new SummaryBinding(refPath, binding.Blendshape);
if (!_bindingMappings.TryGetValue(srcBinding, out var dstBindings))
{
dstBindings = new List<SummaryBinding>();
_bindingMappings[srcBinding] = dstBindings;
}
var targetBlendshapeName = string.IsNullOrWhiteSpace(binding.LocalBlendshape)
? binding.Blendshape
: binding.LocalBlendshape;
dstBindings.Add(new SummaryBinding(targetObj, targetBlendshapeName));
}
}
Motion TransformMotion(Motion motion)
{
if (motion == null) return null;

View File

@ -22,6 +22,7 @@
* SOFTWARE.
*/
using nadena.dev.modular_avatar.editor.ErrorReporting;
using UnityEngine;
namespace nadena.dev.modular_avatar.core.editor
@ -41,39 +42,44 @@ namespace nadena.dev.modular_avatar.core.editor
foreach (var proxy in boneProxies)
{
if (proxy.target != null && ValidateTarget(avatarGameObject, proxy.target) == ValidationResult.OK)
BuildReport.ReportingObject(proxy, () => ProcessProxy(avatarGameObject, proxy));
}
}
private void ProcessProxy(GameObject avatarGameObject, ModularAvatarBoneProxy proxy)
{
if (proxy.target != null && ValidateTarget(avatarGameObject, proxy.target) == ValidationResult.OK)
{
var oldPath = RuntimeUtil.AvatarRootPath(proxy.gameObject);
Transform transform = proxy.transform;
transform.SetParent(proxy.target, true);
bool keepPos, keepRot;
switch (proxy.attachmentMode)
{
var oldPath = RuntimeUtil.AvatarRootPath(proxy.gameObject);
Transform transform = proxy.transform;
transform.SetParent(proxy.target, true);
bool keepPos, keepRot;
switch (proxy.attachmentMode)
{
default:
case BoneProxyAttachmentMode.Unset:
case BoneProxyAttachmentMode.AsChildAtRoot:
keepPos = keepRot = false;
break;
case BoneProxyAttachmentMode.AsChildKeepWorldPose:
keepPos = keepRot = true;
break;
case BoneProxyAttachmentMode.AsChildKeepPosition:
keepPos = true;
keepRot = false;
break;
case BoneProxyAttachmentMode.AsChildKeepRotation:
keepRot = true;
keepPos = false;
break;
}
if (!keepPos) transform.localPosition = Vector3.zero;
if (!keepRot) transform.localRotation = Quaternion.identity;
default:
case BoneProxyAttachmentMode.Unset:
case BoneProxyAttachmentMode.AsChildAtRoot:
keepPos = keepRot = false;
break;
case BoneProxyAttachmentMode.AsChildKeepWorldPose:
keepPos = keepRot = true;
break;
case BoneProxyAttachmentMode.AsChildKeepPosition:
keepPos = true;
keepRot = false;
break;
case BoneProxyAttachmentMode.AsChildKeepRotation:
keepRot = true;
keepPos = false;
break;
}
Object.DestroyImmediate(proxy);
if (!keepPos) transform.localPosition = Vector3.zero;
if (!keepRot) transform.localRotation = Quaternion.identity;
}
Object.DestroyImmediate(proxy);
}
internal static ValidationResult ValidateTarget(GameObject avatarGameObject, Transform proxyTarget)

View File

@ -0,0 +1,4 @@
fileFormatVersion: 2
guid: 0fb1980d53a548a996bdfca622d468ca
timeCreated: 1674039787
folderAsset: yes

View File

@ -0,0 +1,186 @@
using System.Collections.Generic;
using nadena.dev.modular_avatar.core;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
namespace nadena.dev.modular_avatar.editor.ErrorReporting
{
internal static class ComponentValidation
{
/// <summary>
/// Validates the provided tag component.
/// </summary>
/// <param name="tagComponent"></param>
/// <returns>Null if valid, otherwise a list of configuration errors</returns>
internal static List<ErrorLog> CheckComponent(this AvatarTagComponent tagComponent)
{
switch (tagComponent)
{
case ModularAvatarBlendshapeSync bs:
return CheckInternal(bs);
case ModularAvatarBoneProxy bp:
return CheckInternal(bp);
case ModularAvatarMenuInstaller mi:
return CheckInternal(mi);
case ModularAvatarMergeAnimator obj:
return CheckInternal(obj);
case ModularAvatarMergeArmature obj:
return CheckInternal(obj);
default:
return null;
}
}
internal static List<ErrorLog> ValidateAll(GameObject root)
{
List<ErrorLog> logs = new List<ErrorLog>();
foreach (var component in root.GetComponentsInChildren<AvatarTagComponent>(true))
{
var componentLogs = component.CheckComponent();
if (componentLogs != null)
{
logs.AddRange(componentLogs);
}
}
return logs;
}
private static List<ErrorLog> CheckInternal(ModularAvatarBlendshapeSync bs)
{
var localMesh = bs.GetComponent<SkinnedMeshRenderer>();
if (localMesh == null)
{
return new List<ErrorLog>
{new ErrorLog(ReportLevel.Validation, "validation.blendshape_sync.no_local_renderer", bs)};
}
if (localMesh.sharedMesh == null)
{
return new List<ErrorLog>
{new ErrorLog(ReportLevel.Validation, "validation.blendshape_sync.no_local_mesh", bs)};
}
if (bs.Bindings == null || bs.Bindings.Count == 0)
{
return new List<ErrorLog>
{new ErrorLog(ReportLevel.Validation, "validation.blendshape_sync.no_bindings", bs)};
}
List<ErrorLog> errorLogs = new List<ErrorLog>();
foreach (var binding in bs.Bindings)
{
var localShape = string.IsNullOrWhiteSpace(binding.LocalBlendshape)
? binding.Blendshape
: binding.LocalBlendshape;
if (localMesh.sharedMesh.GetBlendShapeIndex(localShape) == -1)
{
errorLogs.Add(new ErrorLog(ReportLevel.Validation, "validation.blendshape_sync.missing_local_shape",
new string[] {localShape}, bs));
}
var targetObj = binding.ReferenceMesh.Get(bs.transform);
if (targetObj == null)
{
errorLogs.Add(new ErrorLog(ReportLevel.Validation, "validation.blendshape_sync.no_target", bs));
continue;
}
var targetRenderer = targetObj.GetComponent<SkinnedMeshRenderer>();
if (targetRenderer == null)
{
errorLogs.Add(new ErrorLog(ReportLevel.Validation,
"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));
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));
}
}
if (errorLogs.Count == 0)
{
return null;
}
else
{
return errorLogs;
}
}
private static List<ErrorLog> CheckInternal(ModularAvatarBoneProxy bp)
{
if (bp.target == null)
{
return new List<ErrorLog>()
{
new ErrorLog(ReportLevel.Validation, "validation.bone_proxy.no_target", bp)
};
}
return null;
}
private static List<ErrorLog> CheckInternal(ModularAvatarMenuInstaller mi)
{
// TODO - check that target menu is in the avatar
if (mi.menuToAppend == null)
{
return new List<ErrorLog>()
{
new ErrorLog(ReportLevel.Validation, "validation.menu_installer.no_menu", mi)
};
}
return null;
}
private static List<ErrorLog> CheckInternal(ModularAvatarMergeAnimator ma)
{
if (ma.animator == null)
{
return new List<ErrorLog>()
{
new ErrorLog(ReportLevel.Validation, "validation.merge_animator.no_animator", ma)
};
}
return null;
}
private static List<ErrorLog> CheckInternal(ModularAvatarMergeArmature ma)
{
if (ma.mergeTargetObject == null)
{
return new List<ErrorLog>()
{
new ErrorLog(ReportLevel.Validation, "validation.merge_armature.no_target", ma)
};
}
if (ma.mergeTargetObject == ma.gameObject || ma.mergeTargetObject.transform.IsChildOf(ma.transform))
{
return new List<ErrorLog>()
{
new ErrorLog(ReportLevel.Validation, "error.merge_armature.merge_into_self", ma,
ma.mergeTargetObject)
};
}
return null;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 3b500f7714e4401aa090eef81c0bab01
timeCreated: 1675855192

View File

@ -0,0 +1,105 @@
using System;
using System.Linq;
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);
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a534edd7151c4cd49fe07919ae526004
timeCreated: 1674132977

View File

@ -0,0 +1,388 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using nadena.dev.modular_avatar.core;
using Newtonsoft.Json;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
using UnityEditor;
using UnityEngine.SceneManagement;
using UnityEngine.Serialization;
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<ErrorLog> logs = new List<ErrorLog>();
}
internal class ObjectRefLookupCache
{
private Dictionary<string, Dictionary<long, UnityEngine.Object>> _cache =
new Dictionary<string, Dictionary<long, Object>>();
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<long, Object>(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<ObjectRef> 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<string>(), 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 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<UnityEngine.Object> _references = new Stack<Object>();
[JsonProperty] internal List<AvatarReport> Avatars = new List<AvatarReport>();
internal AvatarReport CurrentAvatar => _currentAvatar;
public 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 JsonConvert.DeserializeObject<BuildReport>(data);
}
catch (Exception e)
{
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(VRCAvatarDescriptor descriptor)
{
if (descriptor != null)
{
AvatarReport report = new AvatarReport();
report.objectRef = new ObjectRef(descriptor.gameObject);
Avatars.Add(report);
_currentAvatar = report;
_currentAvatar.successful = true;
_currentAvatar.logs.AddRange(ComponentValidation.ValidateAll(descriptor.gameObject));
}
return new AvatarReportScope();
}
internal static void Log(ReportLevel level, string code, params object[] objects)
{
ErrorLog errorLog = new ErrorLog(level, code, 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, params object[] objects)
{
Log(ReportLevel.Error, code, 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));
}
}
internal static T ReportingObject<T>(UnityEngine.Object obj, Func<T> action)
{
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
{
CurrentReport._references.Pop();
}
}
internal static void ReportingObject(UnityEngine.Object obj, Action action)
{
ReportingObject(obj, () =>
{
action();
return true;
});
}
internal IEnumerable<ObjectRef> GetActiveReferences()
{
return _references.Select(o => new ObjectRef(o));
}
public static void Clear()
{
_report = new BuildReport();
}
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();
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 08d5f5d12365416d94e2d97970a24f5d
timeCreated: 1674039799

View File

@ -0,0 +1,306 @@
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<ErrorReportUI>().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<Button> _avatarButtons = new List<Button>();
private Box selectAvatar;
private void OnEnable()
{
titleContent = new GUIContent("Error Report");
rootVisualElement.styleSheets.Add(Resources.Load<StyleSheet>("ModularAvatarErrorReport"));
RenderContent();
reloadErrorReport = RenderContent;
Selection.selectionChanged += ScheduleRender;
EditorApplication.hierarchyChanged += ScheduleRender;
AvatarTagComponent.OnChangeAction += ScheduleRender;
Localization.OnLangChange += RenderContent;
}
private void OnDisable()
{
reloadErrorReport = () => { };
Selection.selectionChanged -= ScheduleRender;
EditorApplication.hierarchyChanged -= ScheduleRender;
AvatarTagComponent.OnChangeAction -= ScheduleRender;
Localization.OnLangChange -= RenderContent;
}
private readonly int RefreshDelayTime = 500;
private Stopwatch DelayTimer = new Stopwatch();
private bool RenderPending = false;
private void ScheduleRender()
{
if (RenderPending) return;
RenderPending = true;
DelayTimer.Restart();
EditorApplication.delayCall += StartRenderTimer;
}
private async void StartRenderTimer()
{
while (DelayTimer.ElapsedMilliseconds < RefreshDelayTime)
{
long remaining = RefreshDelayTime - DelayTimer.ElapsedMilliseconds;
if (remaining > 0)
{
await Task.Delay((int) remaining);
}
}
RenderPending = false;
RenderContent();
Repaint();
}
private void RenderContent()
{
rootVisualElement.Clear();
var root = new Box();
root.Clear();
root.name = "Root";
rootVisualElement.Add(root);
root.Add(CreateLogo());
var box = new ScrollView();
var lookupCache = new ObjectRefLookupCache();
int reported = 0;
AvatarReport activeAvatar = null;
GameObject activeAvatarObject = null;
if (Selection.gameObjects.Length == 1)
{
activeAvatarObject = RuntimeUtil.FindAvatarInParents(Selection.activeGameObject.transform)?.gameObject;
activeAvatar = BuildReport.CurrentReport.Avatars.FirstOrDefault(av =>
av.objectRef.path == RuntimeUtil.RelativePath(null, activeAvatarObject));
if (activeAvatar == null)
{
activeAvatar = new AvatarReport();
activeAvatar.objectRef = new ObjectRef(activeAvatarObject);
}
}
if (activeAvatar == null)
{
activeAvatar = BuildReport.CurrentReport.Avatars.LastOrDefault();
}
if (activeAvatar != null)
{
reported++;
var avBox = new Box();
avBox.AddToClassList("avatarBox");
var header = new Box();
header.Add(new Label("Error report for " + activeAvatar.objectRef.name));
header.AddToClassList("avatarHeader");
avBox.Add(header);
List<ErrorLog> errorLogs = activeAvatar.logs
.Where(l => activeAvatarObject == null || l.reportLevel != ReportLevel.Validation).ToList();
if (activeAvatarObject != null)
{
activeAvatar.logs = errorLogs;
activeAvatar.logs.AddRange(ComponentValidation.ValidateAll(activeAvatarObject));
}
foreach (var ev in activeAvatar.logs)
{
avBox.Add(new ErrorElement(ev, lookupCache));
}
activeAvatar.logs.Sort((a, b) => a.reportLevel.CompareTo(b.reportLevel));
box.Add(avBox);
root.Add(box);
}
/*
if (reported == 0)
{
var container = new Box();
container.name = "no-errors";
container.Add(new Label("Nothing to report!"));
root.Add(container);
}
*/
}
private VisualElement CreateLogo()
{
var img = new Image();
img.image = LogoDisplay.LOGO_ASSET;
// I've given up trying to get USS to resize proportionally for now :|
float height = 64;
img.style.height = new StyleLength(new Length(height, LengthUnit.Pixel));
img.style.width = new StyleLength(new Length(LogoDisplay.ImageWidth(height), LengthUnit.Pixel));
var box = new Box();
box.name = "logo";
box.Add(img);
return box;
}
private VisualElement BuildErrorBox()
{
return new Box();
}
private VisualElement BuildSelectAvatarBox()
{
if (selectAvatar == null) selectAvatar = new Box();
selectAvatar.Clear();
_avatarButtons.Clear();
var avatars = BuildReport.CurrentReport.Avatars;
for (int i = 0; i < avatars.Count; i++)
{
var btn = new Button(() => SelectAvatar(i));
btn.text = avatars[i].objectRef.name;
_avatarButtons.Add(btn);
selectAvatar.Add(btn);
}
SelectAvatar(_selectedAvatar);
return selectAvatar;
}
private void SelectAvatar(int idx)
{
_selectedAvatar = idx;
for (int i = 0; i < _avatarButtons.Count; i++)
{
if (_selectedAvatar == i)
{
_avatarButtons[i].AddToClassList("selected");
}
else
{
_avatarButtons[i].RemoveFromClassList("selected");
}
}
}
private void OnGUI___()
{
var report = BuildReport.CurrentReport;
AvatarReport selected = null;
EditorGUILayout.BeginVertical(GUILayout.MaxHeight(150), GUILayout.Width(position.width));
if (report.Avatars.Count == 0)
{
GUILayout.Label("<no build messages>");
}
else
{
_avatarScrollPos = EditorGUILayout.BeginScrollView(_avatarScrollPos, false, true);
for (int i = 0; i < report.Avatars.Count; i++)
{
var avatarReport = report.Avatars[i];
EditorGUILayout.Space();
if (GUILayout.Toggle(_selectedAvatar == i, avatarReport.objectRef.name, EditorStyles.toggle))
{
_selectedAvatar = i;
}
}
EditorGUILayout.EndScrollView();
}
EditorGUILayout.EndVertical();
EditorGUILayout.Space();
var rect = EditorGUILayout.BeginVertical(GUILayout.Width(position.width));
_errorScrollPos = EditorGUILayout.BeginScrollView(_errorScrollPos, false, true);
EditorGUILayout.BeginVertical(
GUILayout.Width(rect.width
- GUI.skin.scrollView.margin.horizontal
- GUI.skin.scrollView.padding.horizontal),
GUILayout.ExpandWidth(false));
if (_selectedAvatar >= 0 && _selectedAvatar < BuildReport.CurrentReport.Avatars.Count)
{
foreach (var logEntry in BuildReport.CurrentReport.Avatars[_selectedAvatar].logs)
{
imguiRenderLogEntry(logEntry);
}
}
EditorGUILayout.EndVertical();
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
}
private static void imguiRenderLogEntry(ErrorLog logEntry)
{
MessageType ty = MessageType.Error;
switch (logEntry.reportLevel)
{
case ReportLevel.InternalError:
case ReportLevel.Error:
ty = MessageType.Error;
break;
case ReportLevel.Warning:
ty = MessageType.Warning;
break;
case ReportLevel.Info:
ty = MessageType.Info;
break;
}
EditorGUILayout.HelpBox(logEntry.ToString(), ty);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c6dd917ee1894d58a0fa63c5edd9134d
timeCreated: 1674042526

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c9de7a0b7769b954ea5ca239afd0d4c5
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,91 @@
VisualElement {
}
.avatarHeader {
display: flex;
flex-direction: row;
justify-content: center;
font-size: 200%;
margin-top: 10px;
margin-bottom: 10px;
border-left-width: 0;
border-right-width: 0;
border-top-width: 0;
border-bottom-width: 0;
}
.ErrorElement {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
padding: 2px;
margin: 2px;
}
.ErrorElement > Image {
width: 32px;
height: 32px;
}
.ErrorElement Box {
flex: 1 auto;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
padding: 2px;
margin: 2px;
border-width: 0 0 0 0;
}
#logo {
width: 100%;
align-items: center;
flex-direction: column;
flex-shrink: 0;
padding-top: 10px;
padding-bottom: 10px;
border-width: 0;
border-bottom-width: 1px;
}
.ErrorElement Box Label {
white-space: normal; /* word wrap??? */
}
.selection-button {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
border-width: 0 0 0 0;
}
.selection-button > Image {
width: 16px;
height: 16px;
}
#no-errors {
flex-grow: 1;
align-content: center;
justify-content: center;
border-width: 0;
}
#no-errors Label {
align-self: center;
}
.avatarBox {
border-width: 0;
min-height: 100%;
}
#Root {
border-width: 0;
width: 100%;
height: 100%;
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f11231cfb768b5a4da3f8965eeef0775
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
disableValidation: 0

View File

@ -0,0 +1,32 @@
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace nadena.dev.modular_avatar.editor.ErrorReporting
{
internal class SelectionButton : Box
{
private UnityEngine.Object target;
internal SelectionButton(string typeName, UnityEngine.Object target)
{
this.target = target;
AddToClassList("selection-button");
var tex = EditorGUIUtility.FindTexture("d_Search Icon");
var icon = new Image {image = tex};
Add(icon);
var button = new Button(() =>
{
Selection.activeObject = target;
EditorGUIUtility.PingObject(target);
});
//button.Add(new Label("[" + typeName + "] " + target.name));
button.text = "[" + typeName + "] " + target.name;
Add(button);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d5b79370f6c94a3eb05376006f255790
timeCreated: 1674134609

View File

@ -6,9 +6,14 @@ namespace nadena.dev.modular_avatar.core.editor
{
internal static class LogoDisplay
{
private static Texture2D LOGO_ASSET;
internal static readonly Texture2D LOGO_ASSET;
private static float TARGET_HEIGHT => EditorStyles.label.lineHeight * 3;
internal static float ImageWidth(float height)
{
return (height / (float) LOGO_ASSET.height) * LOGO_ASSET.width;
}
private static GUIStyle STYLE => new GUIStyle()
{
fixedHeight = TARGET_HEIGHT,
@ -35,7 +40,7 @@ namespace nadena.dev.modular_avatar.core.editor
if (LOGO_ASSET == null) return;
var height = TARGET_HEIGHT;
var width = (height / (float) LOGO_ASSET.height) * LOGO_ASSET.width;
var width = ImageWidth(height);
var rect = GUILayoutUtility.GetRect(width, height);
GUI.DrawTexture(rect, LOGO_ASSET, ScaleMode.ScaleToFit);

View File

@ -11,6 +11,7 @@ namespace nadena.dev.modular_avatar.core.editor
{
internal static class Localization
{
public static event Action OnLangChange;
private const string FallbackLanguage = "en";
private const string localizationPathGuid = "488c994003974b3ab2796371cf627bca";
@ -22,7 +23,8 @@ namespace nadena.dev.modular_avatar.core.editor
.Add("ja", "日本語")
.Add("zh-hans", "简体中文");
private static ImmutableList<string> SupportedLanguages = new string[] {"en", "ja", "zh-hans"}.ToImmutableList();
private static ImmutableList<string>
SupportedLanguages = new string[] {"en", "ja", "zh-hans"}.ToImmutableList();
private static string[] DisplayNames = SupportedLanguages.Select(l =>
{
@ -38,6 +40,7 @@ namespace nadena.dev.modular_avatar.core.editor
public static void Reload()
{
Cache.Clear();
OnLangChange?.Invoke();
}
private static ImmutableDictionary<string, string> GetLocalization(string lang)

View File

@ -60,5 +60,23 @@
"boneproxy.attachment.AsChildKeepPosition": "As child; keep position",
"boneproxy.attachment.AsChildKeepRotation": "As child; keep rotation",
"pb_blocker.help": "This object will not be affected by PhysBones attached to parents.",
"hint.bad_vrcsdk": "Incompatible version of VRCSDK detected.\n\nPlease try upgrading your VRCSDK; if this does not work, check for a newer version of Modular Avatar as well."
"hint.bad_vrcsdk": "Incompatible version of VRCSDK detected.\n\nPlease try upgrading your VRCSDK; if this does not work, check for a newer version of Modular Avatar as well.",
"error.stack_trace": "Stack trace (provide this when reporting bugs!)",
"error.merge_armature.merge_into_self": "Your Merge Armature component is referencing itself, or a child of itself, as the merge target. You should reference the avatar's armature instead. Do not put Merge Armature on the avatar's main armature.",
"error.internal_error": "An internal error has occurred: {0}\nwhen processing:",
"error.merge_animator.param_type_mismatch": "Parameter {0} has multiple types: {1} != {2}",
"error.rename_params.too_many_synced_params": "Too many synced parameters: Cost {0} > {1}",
"validation.blendshape_sync.no_local_renderer": "No renderer found on this object",
"validation.blendshape_sync.no_local_mesh": "No mesh found on the renderer on this object",
"validation.blendshape_sync.no_bindings": "No blendshape bindings found on this object",
"validation.blendshape_sync.missing_local_shape": "Missing local blendshape: {0}",
"validation.blendshape_sync.missing_target_shape": "Missing target blendshape: {0}",
"validation.blendshape_sync.no_target": "No target object specified",
"validation.blendshape_sync.missing_target_renderer": "No renderer found on the target object",
"validation.blendshape_sync.missing_target_mesh": "No mesh found on the renderer on the target object",
"validation.bone_proxy.no_target": "No target object specified (or target object not found)",
"validation.menu_installer.no_menu": "No menu to install specified",
"validation.merge_animator.no_animator": "No animator to merge specified",
"validation.merge_armature.no_target": "No merge target specified",
"validation.merge_armature.target_is_child": "Merge target cannot be a child of this object"
}

View File

@ -58,5 +58,22 @@
"boneproxy.attachment.AsChildKeepPosition": "子として・ワールド位置を維持",
"boneproxy.attachment.AsChildKeepRotation": "子として・ワールド向きを維持",
"pb_blocker.help": "このオブジェクトは親のPhysBoneから影響を受けなくなります。",
"hint.bad_vrcsdk": "使用中のVRCSDKのバージョンとは互換性がありません。\n\nVRCSDKを更新してみてください。それでもだめでしたら、Modular Avatarにも最新版が出てないかチェックしてください。"
"hint.bad_vrcsdk": "使用中のVRCSDKのバージョンとは互換性がありません。\n\nVRCSDKを更新してみてください。それでもだめでしたら、Modular Avatarにも最新版が出てないかチェックしてください。",
"error.stack_trace": "スタックトレース(バグを報告する時は必ず添付してください!)",
"error.merge_armature.merge_into_self": "Merge Armatureに自分自身のオブジェクト、もしくは自分の子をターゲットにしてしています。かわりにアバターのメインArmatureを指定してください。アバター自体のArmatureに追加しないでください。",
"error.internal_error": "内部エラーが発生しました:{0}\n以下のオブジェクトの処理中に発生しました",
"error.merge_animator.param_type_mismatch": "パラメーター {0} に複数の種別が設定されています: {1} != {2}",
"error.rename_params.too_many_synced_params": "同期パラメーターが多すぎます。コスト{0}が制限値の{1}を超えています。",
"validation.blendshape_sync.no_local_renderer": "このオブジェクトにはSkinnedMeshRendererがありません。",
"validation.blendshape_sync.no_local_mesh": "このオブジェクトにはSkinnedMeshRendererがありますが、メッシュがありません。",
"validation.blendshape_sync.no_bindings": "このBlendshapeSyncにはバインドが設定されていません。",
"validation.blendshape_sync.missing_local_shape": "同期先のメッシュに該当するブレンドシェープ「{0}」がありません。",
"validation.blendshape_sync.missing_target_shape": "同期元のメッシュに該当するブレンドシェープ「{0}」がありません。",
"validation.blendshape_sync.no_target": "このBlendshapeSyncには同期元が設定されていないバインドがあります。",
"validation.blendshape_sync.missing_target_renderer": "同期元のオブジェクトにはSkinnedMeshRendererがありません。",
"validation.blendshape_sync.missing_target_mesh": "同期元のオブジェクトにはSkinnedMeshRendererがありますが、メッシュがありません。",
"validation.bone_proxy.no_target": "ターゲットオブジェクトが未設定、もしくは存在しません。",
"validation.menu_installer.no_menu": "インストールするメニューがありません。",
"validation.merge_animator.no_animator": "Animator Controllerがありません。",
"validation.merge_armature.no_target": "ターゲットオブジェクトが未設定、もしくは存在しません。"
}

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using UnityEditor;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
@ -59,12 +60,12 @@ namespace nadena.dev.modular_avatar.core.editor
foreach (ModularAvatarMenuInstaller installer in menuInstallers)
{
_menuTree.TraverseMenuInstaller(installer);
BuildReport.ReportingObject(installer, () => _menuTree.TraverseMenuInstaller(installer));
}
foreach (MenuTree.ChildElement childElement in _menuTree.GetChildInstallers(null))
{
InstallMenu(childElement.installer);
BuildReport.ReportingObject(childElement.installer, () => InstallMenu(childElement.installer));
}
}

View File

@ -24,6 +24,7 @@
using System;
using System.Collections.Generic;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
@ -66,44 +67,50 @@ namespace nadena.dev.modular_avatar.core.editor
foreach (var merge in toMerge)
{
if (merge.animator == null) continue;
string basePath;
if (merge.pathMode == MergeAnimatorPathMode.Relative)
{
var relativePath = RuntimeUtil.RelativePath(avatarGameObject, merge.gameObject);
basePath = relativePath != "" ? relativePath + "/" : "";
}
else
{
basePath = "";
}
if (!mergeSessions.TryGetValue(merge.layerType, out var session))
{
session = new AnimatorCombiner(context);
mergeSessions[merge.layerType] = session;
if (defaultControllers_.ContainsKey(merge.layerType))
{
session.AddController("", defaultControllers_[merge.layerType], null);
}
}
bool? writeDefaults = merge.matchAvatarWriteDefaults ? writeDefaults_[merge.layerType] : null;
mergeSessions[merge.layerType]
.AddController(basePath, (AnimatorController) merge.animator, writeDefaults);
if (merge.deleteAttachedAnimator)
{
var animator = merge.GetComponent<Animator>();
if (animator != null) Object.DestroyImmediate(animator);
}
BuildReport.ReportingObject(merge, () => ProcessMergeAnimator(avatarGameObject, context, merge));
}
descriptor.baseAnimationLayers = FinishSessions(descriptor.baseAnimationLayers);
descriptor.specialAnimationLayers = FinishSessions(descriptor.specialAnimationLayers);
}
private void ProcessMergeAnimator(GameObject avatarGameObject, BuildContext context,
ModularAvatarMergeAnimator merge)
{
if (merge.animator == null) return;
string basePath;
if (merge.pathMode == MergeAnimatorPathMode.Relative)
{
var relativePath = RuntimeUtil.RelativePath(avatarGameObject, merge.gameObject);
basePath = relativePath != "" ? relativePath + "/" : "";
}
else
{
basePath = "";
}
if (!mergeSessions.TryGetValue(merge.layerType, out var session))
{
session = new AnimatorCombiner(context);
mergeSessions[merge.layerType] = session;
if (defaultControllers_.ContainsKey(merge.layerType))
{
session.AddController("", defaultControllers_[merge.layerType], null);
}
}
bool? writeDefaults = merge.matchAvatarWriteDefaults ? writeDefaults_[merge.layerType] : null;
mergeSessions[merge.layerType]
.AddController(basePath, (AnimatorController) merge.animator, writeDefaults);
if (merge.deleteAttachedAnimator)
{
var animator = merge.GetComponent<Animator>();
if (animator != null) Object.DestroyImmediate(animator);
}
}
private VRCAvatarDescriptor.CustomAnimLayer[] FinishSessions(
VRCAvatarDescriptor.CustomAnimLayer[] layers
)

View File

@ -25,6 +25,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using UnityEditor;
using UnityEngine;
using UnityEngine.Animations;
@ -49,17 +50,14 @@ namespace nadena.dev.modular_avatar.core.editor
foreach (var mergeArmature in mergeArmatures)
{
mergedObjects.Clear();
thisPassAdded.Clear();
MergeArmature(mergeArmature);
PruneDuplicatePhysBones();
UnityEngine.Object.DestroyImmediate(mergeArmature);
}
foreach (var renderer in avatarGameObject.transform.GetComponentsInChildren<SkinnedMeshRenderer>(true))
{
var bones = renderer.bones;
renderer.bones = bones;
BuildReport.ReportingObject(mergeArmature, () =>
{
mergedObjects.Clear();
thisPassAdded.Clear();
MergeArmature(mergeArmature);
PruneDuplicatePhysBones();
UnityEngine.Object.DestroyImmediate(mergeArmature);
});
}
foreach (var c in avatarGameObject.transform.GetComponentsInChildren<VRCPhysBone>(true))
@ -274,7 +272,8 @@ namespace nadena.dev.modular_avatar.core.editor
{
if (src == newParent)
{
throw new Exception("[ModularAvatar] Attempted to merge an armature into itself! Aborting build...");
// Error reported by validation framework
return;
}
if (zipMerge)

View File

@ -24,6 +24,7 @@
using System.Collections.Generic;
using System.Linq;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using UnityEditor;
using UnityEngine;
using VRC.SDKBase.Editor.BuildPipeline;
@ -85,23 +86,26 @@ namespace nadena.dev.modular_avatar.core.editor
foreach (var renderer in avatarGameObject.GetComponentsInChildren<SkinnedMeshRenderer>(true))
{
if (renderer.sharedMesh == null) continue;
bool isRetargetable = false;
foreach (var bone in renderer.bones)
BuildReport.ReportingObject(renderer, () =>
{
if (BoneDatabase.GetRetargetedBone(bone) != null)
if (renderer.sharedMesh == null) return;
bool isRetargetable = false;
foreach (var bone in renderer.bones)
{
isRetargetable = true;
break;
if (BoneDatabase.GetRetargetedBone(bone) != null)
{
isRetargetable = true;
break;
}
}
}
if (isRetargetable)
{
var newMesh = new MeshRetargeter(renderer).Retarget();
_context.SaveAsset(newMesh);
}
if (isRetargetable)
{
var newMesh = new MeshRetargeter(renderer).Retarget();
_context.SaveAsset(newMesh);
}
});
}
// Now remove retargeted bones

View File

@ -23,6 +23,7 @@
*/
using System.Collections.Generic;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using UnityEngine;
using VRC.SDK3.Dynamics.PhysBone.Components;
@ -43,28 +44,34 @@ namespace nadena.dev.modular_avatar.core.editor
var avatarTransform = avatarRoot.transform;
foreach (var tip in blockers)
{
var node = tip.transform;
// We deliberately skip the node itself to allow for a specific PhysBone to be attached here.
while (node != null && node != avatarTransform && node.parent != null)
BuildReport.ReportingObject(tip, () =>
{
node = node.parent;
if (!physBoneRootToIgnores.TryGetValue(node, out var parent))
var node = tip.transform;
// We deliberately skip the node itself to allow for a specific PhysBone to be attached here.
while (node != null && node != avatarTransform && node.parent != null)
{
parent = new List<Transform>();
physBoneRootToIgnores.Add(node, parent);
}
node = node.parent;
if (!physBoneRootToIgnores.TryGetValue(node, out var parent))
{
parent = new List<Transform>();
physBoneRootToIgnores.Add(node, parent);
}
parent.Add(tip.transform);
}
parent.Add(tip.transform);
}
});
}
foreach (var pb in physBones)
{
var root = pb.rootTransform != null ? pb.rootTransform : pb.transform;
if (physBoneRootToIgnores.TryGetValue(root, out var ignores))
BuildReport.ReportingObject(pb, () =>
{
pb.ignoreTransforms.AddRange(ignores);
}
var root = pb.rootTransform != null ? pb.rootTransform : pb.transform;
if (physBoneRootToIgnores.TryGetValue(root, out var ignores))
{
pb.ignoreTransforms.AddRange(ignores);
}
});
}
}
}

View File

@ -1,4 +1,5 @@
using UnityEditor;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
@ -22,10 +23,13 @@ namespace nadena.dev.modular_avatar.core.editor
PathMappings.ClearCache();
animDb.ForeachClip(clip =>
{
if (clip.CurrentClip is AnimationClip anim && !clip.IsProxyAnimation)
BuildReport.ReportingObject(clip.CurrentClip, () =>
{
clip.CurrentClip = MapMotion(anim);
}
if (clip.CurrentClip is AnimationClip anim && !clip.IsProxyAnimation)
{
clip.CurrentClip = MapMotion(anim);
}
});
});
}

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
@ -71,10 +72,8 @@ namespace nadena.dev.modular_avatar.core.editor
expParams.parameters = parameters.ToArray();
if (expParams.CalcTotalCost() > VRCExpressionParameters.MAX_PARAMETER_COST)
{
throw new Exception("Too many synced parameters: " +
"Cost " + expParams.CalcTotalCost() + " > "
+ VRCExpressionParameters.MAX_PARAMETER_COST
);
BuildReport.LogFatal("error.rename_params.too_many_synced_params", expParams.CalcTotalCost(),
VRCExpressionParameters.MAX_PARAMETER_COST);
}
avatar.expressionParameters = expParams;
@ -89,7 +88,7 @@ namespace nadena.dev.modular_avatar.core.editor
var p = obj.GetComponent<ModularAvatarParameters>();
if (p != null)
{
ApplyRemappings(p, ref remaps, ref prefixRemaps);
BuildReport.ReportingObject(p, () => ApplyRemappings(p, ref remaps, ref prefixRemaps));
}
var willPurgeAnimators = false;
@ -104,76 +103,79 @@ namespace nadena.dev.modular_avatar.core.editor
foreach (var component in obj.GetComponents<Component>())
{
switch (component)
BuildReport.ReportingObject(component, () =>
{
case VRCPhysBone bone:
switch (component)
{
if (bone.parameter != null && prefixRemaps.TryGetValue(bone.parameter, out var newVal))
case VRCPhysBone bone:
{
bone.parameter = newVal;
if (bone.parameter != null && prefixRemaps.TryGetValue(bone.parameter, out var newVal))
{
bone.parameter = newVal;
}
break;
}
break;
case VRCContactReceiver contact:
{
if (contact.parameter != null && remaps.TryGetValue(contact.parameter, out var newVal))
{
contact.parameter = newVal;
}
break;
}
case Animator anim:
{
if (willPurgeAnimators) break; // animator will be deleted in subsequent processing
// RuntimeAnimatorController may be AnimatorOverrideController, convert in case of AnimatorOverrideController
if (anim.runtimeAnimatorController is AnimatorOverrideController overrideController)
{
anim.runtimeAnimatorController = _context.ConvertAnimatorController(overrideController);
}
var controller = anim.runtimeAnimatorController as AnimatorController;
if (controller != null)
{
ProcessAnimator(ref controller, remaps);
anim.runtimeAnimatorController = controller;
}
break;
}
case ModularAvatarMergeAnimator merger:
{
// RuntimeAnimatorController may be AnimatorOverrideController, convert in case of AnimatorOverrideController
if (merger.animator is AnimatorOverrideController overrideController)
{
merger.animator = _context.ConvertAnimatorController(overrideController);
}
var controller = merger.animator as AnimatorController;
if (controller != null)
{
ProcessAnimator(ref controller, remaps);
merger.animator = controller;
}
break;
}
case ModularAvatarMenuInstaller installer:
{
if (installer.menuToAppend != null && installer.enabled)
{
ProcessMenu(ref installer.menuToAppend, remaps);
}
break;
}
}
case VRCContactReceiver contact:
{
if (contact.parameter != null && remaps.TryGetValue(contact.parameter, out var newVal))
{
contact.parameter = newVal;
}
break;
}
case Animator anim:
{
if (willPurgeAnimators) break; // animator will be deleted in subsequent processing
// RuntimeAnimatorController may be AnimatorOverrideController, convert in case of AnimatorOverrideController
if (anim.runtimeAnimatorController is AnimatorOverrideController overrideController)
{
anim.runtimeAnimatorController = _context.ConvertAnimatorController(overrideController);
}
var controller = anim.runtimeAnimatorController as AnimatorController;
if (controller != null)
{
ProcessAnimator(ref controller, remaps);
anim.runtimeAnimatorController = controller;
}
break;
}
case ModularAvatarMergeAnimator merger:
{
// RuntimeAnimatorController may be AnimatorOverrideController, convert in case of AnimatorOverrideController
if (merger.animator is AnimatorOverrideController overrideController)
{
merger.animator = _context.ConvertAnimatorController(overrideController);
}
var controller = merger.animator as AnimatorController;
if (controller != null)
{
ProcessAnimator(ref controller, remaps);
merger.animator = controller;
}
break;
}
case ModularAvatarMenuInstaller installer:
{
if (installer.menuToAppend != null && installer.enabled)
{
ProcessMenu(ref installer.menuToAppend, remaps);
}
break;
}
}
});
}
foreach (Transform child in obj.transform)

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using UnityEngine;
using UnityEngine.Animations;
using VRC.SDK3.Avatars.Components;
@ -61,7 +62,7 @@ namespace nadena.dev.modular_avatar.core.editor
foreach (var target in _avatar.GetComponentsInChildren<ModularAvatarVisibleHeadAccessory>(true))
{
var w = Process(target);
var w = BuildReport.ReportingObject(target, () => Process(target));
didWork = didWork || w;
}
@ -70,7 +71,8 @@ namespace nadena.dev.modular_avatar.core.editor
// Process meshes
foreach (var smr in _avatar.GetComponentsInChildren<SkinnedMeshRenderer>(true))
{
new VisibleHeadAccessoryMeshProcessor(smr, _visibleBones, _proxyHead).Retarget(context);
BuildReport.ReportingObject(smr,
() => new VisibleHeadAccessoryMeshProcessor(smr, _visibleBones, _proxyHead).Retarget(context));
}
}
}

View File

@ -22,6 +22,7 @@
* SOFTWARE.
*/
using System;
using UnityEngine;
namespace nadena.dev.modular_avatar.core
@ -32,6 +33,8 @@ namespace nadena.dev.modular_avatar.core
[DefaultExecutionOrder(-9999)] // run before av3emu
public abstract class AvatarTagComponent : MonoBehaviour
{
internal static event Action OnChangeAction;
private void Awake()
{
if (!RuntimeUtil.isPlaying || this == null) return;
@ -44,7 +47,7 @@ namespace nadena.dev.modular_avatar.core
RuntimeUtil.OnDemandProcessAvatar(RuntimeUtil.OnDemandSource.Start, this);
}
private void OnValidate()
protected virtual void OnValidate()
{
if (RuntimeUtil.isPlaying) return;
@ -55,6 +58,13 @@ namespace nadena.dev.modular_avatar.core
Activator.CreateIfNotPresent(gameObject.scene);
#endif
});
OnChangeAction?.Invoke();
}
protected void OnDestroy()
{
OnChangeAction?.Invoke();
}
}
}

View File

@ -48,8 +48,10 @@ namespace nadena.dev.modular_avatar.core
private List<EditorBlendshapeBinding> _editorBindings;
private void OnValidate()
protected override void OnValidate()
{
base.OnValidate();
if (RuntimeUtil.isPlaying) return;
RuntimeUtil.delayCall(Rebind);
RuntimeUtil.OnHierarchyChanged -= Rebind;

View File

@ -58,8 +58,10 @@ namespace nadena.dev.modular_avatar.core
private List<BoneBinding> lockedBones;
void OnValidate()
protected override void OnValidate()
{
base.OnValidate();
RuntimeUtil.delayCall(() =>
{
if (this == null) return;

View File

@ -61,7 +61,7 @@ namespace nadena.dev.modular_avatar.core
child = child.transform.parent?.gameObject;
}
if (child == null) return null;
if (child == null && root != null) return null;
pathSegments.Reverse();
return String.Join("/", pathSegments);
@ -152,4 +152,4 @@ namespace nadena.dev.modular_avatar.core
OnHierarchyChanged?.Invoke();
}
}
}
}