mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2025-01-30 18:22:52 +08:00
feat: Add error reporting UI
This commit is contained in:
parent
2a8c2ec3ce
commit
c2e6bb53cd
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
@ -0,0 +1,4 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0fb1980d53a548a996bdfca622d468ca
|
||||
timeCreated: 1674039787
|
||||
folderAsset: yes
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3b500f7714e4401aa090eef81c0bab01
|
||||
timeCreated: 1675855192
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a534edd7151c4cd49fe07919ae526004
|
||||
timeCreated: 1674132977
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 08d5f5d12365416d94e2d97970a24f5d
|
||||
timeCreated: 1674039799
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c6dd917ee1894d58a0fa63c5edd9134d
|
||||
timeCreated: 1674042526
|
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c9de7a0b7769b954ea5ca239afd0d4c5
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -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%;
|
||||
}
|
@ -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
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d5b79370f6c94a3eb05376006f255790
|
||||
timeCreated: 1674134609
|
@ -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);
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
}
|
@ -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": "ターゲットオブジェクトが未設定、もしくは存在しません。"
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user