feat(error): integrate with NDMF error reporting framework

Note that as part of this, the pre-build validation system has been disabled for now.
It didn't work very well with other NDMF plugins in the first place, so it's probably
for the best...
This commit is contained in:
bd_ 2023-12-19 23:34:01 +09:00
parent fd44d244ec
commit 0a4036145e
26 changed files with 149 additions and 1123 deletions

View File

@ -1,5 +1,6 @@
using System.Collections.Generic;
using nadena.dev.modular_avatar.core;
using nadena.dev.ndmf;
#if MA_VRCSDK3_AVATARS
using nadena.dev.modular_avatar.core.menu;
@ -16,64 +17,61 @@ namespace nadena.dev.modular_avatar.editor.ErrorReporting
/// </summary>
/// <param name="tagComponent"></param>
/// <returns>Null if valid, otherwise a list of configuration errors</returns>
internal static List<ErrorLog> CheckComponent(this AvatarTagComponent tagComponent)
internal static void CheckComponent(this AvatarTagComponent tagComponent)
{
switch (tagComponent)
ErrorReport.WithContextObject(tagComponent, () =>
{
case ModularAvatarBlendshapeSync bs:
return CheckInternal(bs);
case ModularAvatarBoneProxy bp:
return CheckInternal(bp);
switch (tagComponent)
{
case ModularAvatarBlendshapeSync bs:
CheckInternal(bs);
break;
case ModularAvatarBoneProxy bp:
CheckInternal(bp);
break;
#if MA_VRCSDK3_AVATARS
case ModularAvatarMenuInstaller mi:
return CheckInternal(mi);
case ModularAvatarMergeAnimator obj:
return CheckInternal(obj);
case ModularAvatarMenuInstaller mi:
CheckInternal(mi);
break;
case ModularAvatarMergeAnimator obj:
CheckInternal(obj);
break;
#endif
case ModularAvatarMergeArmature obj:
return CheckInternal(obj);
default:
return null;
}
case ModularAvatarMergeArmature obj:
CheckInternal(obj);
break;
default:
return;
}
});
}
internal static List<ErrorLog> ValidateAll(GameObject root)
internal static void 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);
}
component.CheckComponent();
}
return logs;
}
private static List<ErrorLog> CheckInternal(ModularAvatarBlendshapeSync bs)
private static void 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)};
BuildReport.Log(ErrorSeverity.NonFatal, "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)};
BuildReport.Log(ErrorSeverity.NonFatal, "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)};
BuildReport.Log(ErrorSeverity.Information,"validation.blendshape_sync.no_bindings", bs);
}
List<ErrorLog> errorLogs = new List<ErrorLog>();
foreach (var binding in bs.Bindings)
{
var localShape = string.IsNullOrWhiteSpace(binding.LocalBlendshape)
@ -82,113 +80,82 @@ namespace nadena.dev.modular_avatar.editor.ErrorReporting
if (localMesh.sharedMesh.GetBlendShapeIndex(localShape) == -1)
{
errorLogs.Add(new ErrorLog(ReportLevel.Validation, "validation.blendshape_sync.missing_local_shape",
new string[] {localShape}, bs));
BuildReport.Log(ErrorSeverity.NonFatal, "validation.blendshape_sync.missing_local_shape",
localShape, bs);
}
var targetObj = binding.ReferenceMesh.Get(bs.transform);
if (targetObj == null)
{
errorLogs.Add(new ErrorLog(ReportLevel.Validation, "validation.blendshape_sync.no_target", bs));
BuildReport.Log(ErrorSeverity.NonFatal, "validation.blendshape_sync.no_target", bs);
continue;
}
var targetRenderer = targetObj.GetComponent<SkinnedMeshRenderer>();
if (targetRenderer == null)
{
errorLogs.Add(new ErrorLog(ReportLevel.Validation,
"validation.blendshape_sync.missing_target_renderer", bs, targetRenderer));
BuildReport.Log(ErrorSeverity.NonFatal,
"validation.blendshape_sync.missing_target_renderer", bs, targetRenderer);
continue;
}
var targetMesh = targetRenderer.sharedMesh;
if (targetMesh == null)
{
errorLogs.Add(new ErrorLog(ReportLevel.Validation, "validation.blendshape_sync.missing_target_mesh",
bs, targetRenderer));
BuildReport.Log(ErrorSeverity.NonFatal, "validation.blendshape_sync.missing_target_mesh",
bs, targetRenderer);
continue;
}
if (targetMesh.GetBlendShapeIndex(binding.Blendshape) == -1)
{
errorLogs.Add(new ErrorLog(ReportLevel.Validation,
"validation.blendshape_sync.missing_target_shape", new string[] {binding.Blendshape}, bs,
targetRenderer));
BuildReport.Log(ErrorSeverity.NonFatal,
"validation.blendshape_sync.missing_target_shape", binding.Blendshape, bs,
targetRenderer);
}
}
if (errorLogs.Count == 0)
{
return null;
}
else
{
return errorLogs;
}
}
private static List<ErrorLog> CheckInternal(ModularAvatarBoneProxy bp)
private static void CheckInternal(ModularAvatarBoneProxy bp)
{
if (bp.target == null)
{
return new List<ErrorLog>()
{
new ErrorLog(ReportLevel.Validation, "validation.bone_proxy.no_target", bp)
};
BuildReport.Log(ErrorSeverity.NonFatal, "validation.bone_proxy.no_target", bp);
}
return null;
}
#if MA_VRCSDK3_AVATARS
private static List<ErrorLog> CheckInternal(ModularAvatarMenuInstaller mi)
private static void CheckInternal(ModularAvatarMenuInstaller mi)
{
// TODO - check that target menu is in the avatar
if (mi.menuToAppend == null && mi.GetComponent<MenuSource>() == null)
{
return new List<ErrorLog>()
{
new ErrorLog(ReportLevel.Validation, "validation.menu_installer.no_menu", mi)
};
BuildReport.Log(ErrorSeverity.NonFatal, "validation.menu_installer.no_menu", mi);
}
return null;
}
private static List<ErrorLog> CheckInternal(ModularAvatarMergeAnimator ma)
private static void CheckInternal(ModularAvatarMergeAnimator ma)
{
if (ma.animator == null)
{
return new List<ErrorLog>()
{
new ErrorLog(ReportLevel.Validation, "validation.merge_animator.no_animator", ma)
};
BuildReport.Log(ErrorSeverity.NonFatal, "validation.merge_animator.no_animator", ma);
}
return null;
}
#endif
private static List<ErrorLog> CheckInternal(ModularAvatarMergeArmature ma)
private static void CheckInternal(ModularAvatarMergeArmature ma)
{
if (ma.mergeTargetObject == null)
{
return new List<ErrorLog>()
{
new ErrorLog(ReportLevel.Validation, "validation.merge_armature.no_target", ma)
};
BuildReport.Log(ErrorSeverity.NonFatal, "validation.merge_armature.no_target", ma);
return;
}
if (ma.mergeTargetObject == ma.gameObject || ma.mergeTargetObject.transform.IsChildOf(ma.transform))
{
return new List<ErrorLog>()
{
new ErrorLog(ReportLevel.Validation, "error.merge_armature.merge_into_self", ma,
ma.mergeTargetObject)
};
BuildReport.Log(ErrorSeverity.Error, "error.merge_armature.circular_dependency", ma,
ma.mergeTargetObject);
}
return null;
}
}
}

View File

@ -1,104 +0,0 @@
using System;
using nadena.dev.modular_avatar.core.editor;
using UnityEditor;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UIElements;
namespace nadena.dev.modular_avatar.editor.ErrorReporting
{
internal class ErrorElement : Box
{
private readonly ErrorLog log;
Texture2D GetIcon()
{
switch (log.reportLevel)
{
case ReportLevel.Info:
return EditorGUIUtility.FindTexture("d_console.infoicon");
case ReportLevel.Warning:
return EditorGUIUtility.FindTexture("d_console.warnicon");
default:
return EditorGUIUtility.FindTexture("d_console.erroricon");
}
}
public ErrorElement(ErrorLog log, ObjectRefLookupCache cache)
{
this.log = log;
AddToClassList("ErrorElement");
var tex = GetIcon();
if (tex != null)
{
var image = new Image();
image.image = tex;
Add(image);
}
var inner = new Box();
Add(inner);
var label = new Label(GetLabelText());
inner.Add(label);
foreach (var obj in log.referencedObjects)
{
var referenced = obj.Lookup(cache);
if (referenced != null)
{
inner.Add(new SelectionButton(obj.typeName, referenced));
}
}
if (!string.IsNullOrWhiteSpace(log.stacktrace))
{
var foldout = new Foldout();
foldout.text = Localization.S("error.stack_trace");
var field = new TextField();
field.value = log.stacktrace;
field.isReadOnly = true;
field.multiline = true;
foldout.Add(field);
foldout.value = false;
inner.Add(foldout);
}
}
private static GameObject FindObject(string path)
{
var scene = SceneManager.GetActiveScene();
foreach (var root in scene.GetRootGameObjects())
{
if (root.name == path) return root;
if (path.StartsWith(root.name + "/"))
{
return root.transform.Find(path.Substring(root.name.Length + 1))?.gameObject;
}
}
return null;
}
private string GetLabelText()
{
var objArray = new object[log.substitutions.Length];
for (int i = 0; i < log.substitutions.Length; i++)
{
objArray[i] = log.substitutions[i];
}
try
{
return string.Format(Localization.S(log.messageCode), objArray);
}
catch (FormatException e)
{
Debug.LogError("Error formatting message code: " + log.messageCode);
Debug.LogException(e);
return log.messageCode + "\n" + string.Join("\n", objArray);
}
}
}
}

View File

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

View File

@ -3,6 +3,8 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using nadena.dev.modular_avatar.core;
using nadena.dev.modular_avatar.core.editor;
using nadena.dev.ndmf;
using Newtonsoft.Json;
using UnityEngine;
using UnityEditor;
@ -11,376 +13,38 @@ using Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.editor.ErrorReporting
{
internal class AvatarReport
{
[JsonProperty] internal ObjectRef objectRef;
[JsonProperty] internal bool successful;
[JsonProperty] internal List<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 override string ToString()
{
return "[" + reportLevel + "] " + messageCode + " " + "subst: " + string.Join(", ", substitutions);
}
}
internal class BuildReport
{
private const string Path = "Library/ModularAvatarBuildReport.json";
private static BuildReport _report;
private AvatarReport _currentAvatar;
private Stack<UnityEngine.Object> _references = new Stack<Object>();
[JsonProperty] internal List<AvatarReport> Avatars = new List<AvatarReport>();
internal AvatarReport CurrentAvatar => _currentAvatar;
public static BuildReport CurrentReport
internal static void Log(ErrorSeverity severity, string code, params object[] objects)
{
get
{
if (_report == null) _report = LoadReport() ?? new BuildReport();
return _report;
}
ErrorReport.ReportError(Localization.L, severity, code, objects);
}
static BuildReport()
internal static void LogFatal(string code, params object[] objects)
{
EditorApplication.playModeStateChanged += change =>
{
switch (change)
{
case PlayModeStateChange.ExitingEditMode:
// TODO - skip if we're doing a VRCSDK build
_report = new BuildReport();
break;
}
};
}
private static BuildReport LoadReport()
{
try
{
var data = File.ReadAllText(Path);
return JsonConvert.DeserializeObject<BuildReport>(data);
}
catch (Exception)
{
return null;
}
}
internal static void SaveReport()
{
var report = CurrentReport;
var json = JsonConvert.SerializeObject(report);
File.WriteAllText(Path, json);
ErrorReportUI.reloadErrorReport();
}
private class AvatarReportScope : IDisposable
{
public void Dispose()
{
var successful = CurrentReport._currentAvatar.successful;
CurrentReport._currentAvatar = null;
BuildReport.SaveReport();
if (!successful) throw new Exception("Avatar processing failed");
}
}
internal IDisposable ReportingOnAvatar(GameObject avatarGameObject)
{
if (avatarGameObject != null)
{
AvatarReport report = new AvatarReport();
report.objectRef = new ObjectRef(avatarGameObject);
Avatars.Add(report);
_currentAvatar = report;
_currentAvatar.successful = true;
_currentAvatar.logs.AddRange(ComponentValidation.ValidateAll(avatarGameObject));
}
return new AvatarReportScope();
}
internal static void Log(ReportLevel level, string code, object[] strings, params Object[] objects)
{
ErrorLog errorLog =
new ErrorLog(level, code, strings: strings.Select(s => s.ToString()).ToArray(), objects);
var avatarReport = CurrentReport._currentAvatar;
if (avatarReport == null)
{
Debug.LogWarning("Error logged when not processing an avatar: " + errorLog);
return;
}
avatarReport.logs.Add(errorLog);
}
internal static void LogFatal(string code, object[] strings, params Object[] objects)
{
Log(ReportLevel.Error, code, strings: strings, objects: objects);
if (CurrentReport._currentAvatar != null)
{
CurrentReport._currentAvatar.successful = false;
}
else
{
throw new Exception("Fatal error without error reporting scope");
}
ErrorReport.ReportError(Localization.L, ErrorSeverity.Error, code, objects);
}
internal static void LogException(Exception e, string additionalStackTrace = "")
{
var avatarReport = CurrentReport._currentAvatar;
if (avatarReport == null)
{
Debug.LogException(e);
return;
}
else
{
avatarReport.logs.Add(new ErrorLog(e, additionalStackTrace));
}
ErrorReport.ReportException(e, additionalStackTrace);
}
internal static T ReportingObject<T>(UnityEngine.Object obj, Func<T> action)
{
if (obj != null) CurrentReport._references.Push(obj);
try
{
return action();
}
catch (Exception e)
{
var additionalStackTrace = string.Join("\n", Environment.StackTrace.Split('\n').Skip(1)) + "\n";
LogException(e, additionalStackTrace);
return default;
}
finally
{
if (obj != null) CurrentReport._references.Pop();
}
return ErrorReport.WithContextObject(obj, action);
}
internal static void ReportingObject(UnityEngine.Object obj, Action action)
{
ReportingObject(obj, () =>
{
action();
return true;
});
}
internal IEnumerable<ObjectRef> GetActiveReferences()
{
return _references.Select(o => new ObjectRef(o));
}
public static void Clear()
{
_report = new BuildReport();
ErrorReport.WithContextObject(obj, action);
}
[Obsolete("Use NDMF's ObjectRegistry instead")]
public static void RemapPaths(string original, string cloned)
{
foreach (var av in CurrentReport.Avatars)
{
av.objectRef = av.objectRef.Remap(original, cloned);
foreach (var log in av.logs)
{
log.referencedObjects = log.referencedObjects.Select(o => o.Remap(original, cloned)).ToList();
}
}
ErrorReportUI.reloadErrorReport();
}
}
}

View File

@ -1,308 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using nadena.dev.modular_avatar.core;
using nadena.dev.modular_avatar.core.editor;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace nadena.dev.modular_avatar.editor.ErrorReporting
{
internal class ErrorReportUI : EditorWindow
{
internal static Action reloadErrorReport = () => { };
[MenuItem("Tools/Modular Avatar/Show error report", false, 100)]
public static void OpenErrorReportUI()
{
GetWindow<ErrorReportUI>().Show();
}
public static void MaybeOpenErrorReportUI()
{
if (Application.isBatchMode) return; // headless unit tests
if (BuildReport.CurrentReport.Avatars.Any(av => av.logs.Count > 0))
{
OpenErrorReportUI();
}
}
private Vector2 _avatarScrollPos, _errorScrollPos;
private int _selectedAvatar = -1;
private List<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.FindAvatarTransformInParents(Selection.activeGameObject.transform)?.gameObject;
activeAvatar = BuildReport.CurrentReport.Avatars.FirstOrDefault(av =>
av.objectRef.path == RuntimeUtil.RelativePath(null, activeAvatarObject)
|| av.objectRef.path == RuntimeUtil.RelativePath(null, activeAvatarObject) + "(Clone)");
}
if (activeAvatar == null)
{
activeAvatar = BuildReport.CurrentReport.Avatars.LastOrDefault();
}
if (activeAvatar == null)
{
activeAvatar = new AvatarReport();
activeAvatar.objectRef = new ObjectRef(activeAvatarObject);
}
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;
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

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

View File

@ -1,28 +0,0 @@
using System;
using System.Runtime.Serialization;
using JetBrains.Annotations;
namespace nadena.dev.modular_avatar.editor.ErrorReporting
{
/// <summary>
/// These exceptions will not be logged in the error report.
/// </summary>
public class NominalException : Exception
{
public NominalException()
{
}
protected NominalException([NotNull] SerializationInfo info, StreamingContext context) : base(info, context)
{
}
public NominalException(string message) : base(message)
{
}
public NominalException(string message, Exception innerException) : base(message, innerException)
{
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 152d780db95c4e408240ec3cd4dad60e
timeCreated: 1676979798

View File

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

View File

@ -1,91 +0,0 @@
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

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

View File

@ -1,31 +0,0 @@
using UnityEditor;
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

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

View File

@ -100,24 +100,66 @@
"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.",
"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.merge_armature.physbone_on_humanoid_bone": "Some Humanoid bones in the armature to merge are controlled by PhysBones, and can't be merged properly because its position is different from the corresponding Humanoid bone in the merge target. You should remove the PhysBones on those Humanoid bones in the armature to merge.",
"error.merge_armature.circular_dependency": "[MA-0001] Circular reference in merge armature",
"error.merge_armature.circular_dependency:description": "Your Merge Armature component is referencing itself, or a child of itself, as the merge target.",
"error.merge_armature.circular_dependency:hint": "Merge Armature should typically specify the Armature object of the avatar itself under its Target field. Don't specify the outfit itself!",
"error.merge_armature.physbone_on_humanoid_bone": "[MA-0002] PhysBone component found on humanoid bone",
"error.merge_armature.physbone_on_humanoid_bone:hint": "Some Humanoid bones in the armature to merge are controlled by PhysBones, and can't be merged properly because its position is different from the corresponding Humanoid bone in the merge target. You should remove the PhysBones on those Humanoid bones in the armature to merge.",
"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",
"error.merge_animator.param_type_mismatch": "[MA-0003] Parameter type mismatch",
"error.merge_animator.param_type_mismatch:description": "Parameter {0} has multiple types: {1} != {2}",
"error.rename_params.too_many_synced_params": "[MA-0004] Too many synced parameters",
"error.rename_params.too_many_synced_params:description": "You have too many synced parameters in your avatar. You have assigned {0} bits worth of parameters, but the limit is {1}",
"error.replace_object.null_target": "[MA-0005] No target specified",
"error.replace_object.null_target:hint": "Replace object needs a target object to replace. Try setting one.",
"validation.blendshape_sync.no_local_renderer": "[MA-1000] No renderer found on this object",
"validation.blendshape_sync.no_local_renderer:hint": "Blendshape Sync acts on a Skinned Mesh Renderer on the same GameObject. Did you attach it to the right object?",
"validation.blendshape_sync.no_local_mesh": "[MA-1001] No mesh found on the renderer on this object",
"validation.blendshape_sync.no_local_mesh:hint": "It looks like the configuration for the Skinned Mesh Renderer on this object might be broken. Try recreating the object from its original prefab or FBX.",
"validation.blendshape_sync.no_bindings": "[MA-1002] No blendshape bindings found on this object",
"validation.blendshape_sync.no_bindings:hint": "Blendshape Sync needs to know which blendshapes to sync. Click the '+' button to add one.",
"validation.blendshape_sync.missing_local_shape": "[MA-1003] Missing local blendshape",
"validation.blendshape_sync.missing_local_shape:description": "Missing local blendshape: {0}",
"validation.blendshape_sync.missing_local_shape:hint": "The blendshape configured to 'receive' the value from the target object is missing. Try changing the blendshape name indicated in red.",
"validation.blendshape_sync.missing_target_shape": "[MA-1004] Missing target blendshape",
"validation.blendshape_sync.missing_target_shape:description": "Missing target blendshape: {0}",
"validation.blendshape_sync.missing_target_shape:hint": "The blendshape configured to 'send' the value to the local object is missing. Try changing the blendshape name indicated in red.",
"validation.blendshape_sync.no_target": "[MA-1005] No target object specified",
"validation.blendshape_sync.no_target:hint": "Blendshape Sync needs to know which object to sync blendshapes from. Try setting the Mesh field.",
"validation.blendshape_sync.missing_target_renderer": "[MA-1006] No renderer found on the target object",
"validation.blendshape_sync.missing_target_renderer:hint": "Blendshape Sync receives blendshape values from a Skinned Mesh Renderer on the target object. Did you attach it to the right object?",
"validation.blendshape_sync.missing_target_mesh": "[MA-1007] No mesh found on the renderer on the target object",
"validation.blendshape_sync.missing_target_mesh:hint": "It looks like the configuration for the Skinned Mesh Renderer on the target object might be broken. Try recreating the object from its original prefab or FBX.",
"validation.bone_proxy.no_target": "[MA-1100] No target object specified (or target object not found)",
"validation.bone_proxy.no_target:hint": "Bone Proxy needs to know which object to bind this Bone Proxy to. Try setting the target field to the object this object should follow.",
"validation.menu_installer.no_menu": "[MA-1200] No menu to install specified",
"validation.menu_installer.no_menu:hint": "Menu Installer needs to know which menu to install this prefab to. Try setting the 'Menu to install' field inside 'Prefab Developer Options', or attaching a MA Menu Item component.",
"validation.merge_animator.no_animator": "[MA-1300] No animator to merge specified",
"validation.merge_animator.no_animator:hint": "Merge Animator needs to know which animator to merge. Try setting the 'Animator to merge' field.",
"validation.merge_armature.no_target": "[MA-1400] No merge target specified",
"validation.merge_armature.no_target:hint": "Merge Armature needs to know which armature to merge. Try setting the 'Merge Target' field.",
"validation.merge_armature.target_is_child": "[MA-1500] Merge target cannot be a child of this object",
"validation.merge_armature.target_is_child:hint": "Merge Armature cannot merge an armature into itself. Try setting the 'Merge Target' field to a different object.",
"submenu_source.Children": "Children",
"submenu_source.MenuAsset": "Expressions Menu Asset",
"menuitem.showcontents": "Show menu contents",

View File

@ -99,6 +99,11 @@
"pb_blocker.help": "このオブジェクトは親のPhysBoneから影響を受けなくなります。",
"hint.bad_vrcsdk": "使用中のVRCSDKのバージョンとは互換性がありません。\n\nVRCSDKを更新してみてください。それでもだめでしたら、Modular Avatarにも最新版が出てないかチェックしてください。",
"error.stack_trace": "スタックトレース(バグを報告する時は必ず添付してください!)",
"error.merge_armature.circular_dependency": "[MA-0001] Merge Armatureに循環参照があります",
"error.merge_armature.merge_into_self": "Merge Armatureに自分自身のオブジェクト、もしくは自分の子をターゲットにしてしています。かわりにアバターのメインArmatureを指定してください。アバター自体のArmatureに追加しないでください。",
"error.merge_armature.physbone_on_humanoid_bone": "統合するArmatureのHumanoidボーンがPhysBoneによって制御されており、統合先の対応するHumanoidボーンと位置が異なるため正しく統合できません。統合するArmatureの該当HumanoidボーンからPhysBoneを削除してください。",
"error.internal_error": "内部エラーが発生しました:{0}\n以下のオブジェクトの処理中に発生しました",

View File

@ -1,3 +0,0 @@
{
"test0.test_a": "replaced"
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: a851b660ad5443bf92884fdf8e872c4a
timeCreated: 1673953035

View File

@ -145,7 +145,7 @@ namespace nadena.dev.modular_avatar.core.editor
if (visited.Contains(config)) return;
if (visitStack.Contains(config))
{
BuildReport.LogFatal("merge_armature.circular_dependency", new string[0], config);
BuildReport.LogFatal("error.merge_armature.circular_dependency", new string[0], config);
return;
}

View File

@ -107,7 +107,7 @@ namespace nadena.dev.modular_avatar.core.editor
// https://github.com/bdunderscore/modular-avatar/issues/308
// If we have duplicate Armature bones, retain them all in order to deal with some horrible hacks that are
// in use in the wild.
if (animator.isHuman)
if (animator != null && animator.isHuman)
{
try
{

View File

@ -15,29 +15,12 @@ namespace nadena.dev.modular_avatar.core.editor
{
BuildContext = new BuildContext(context);
}
toDispose = BuildReport.CurrentReport.ReportingOnAvatar(context.AvatarRootObject);
}
public void OnDeactivate(ndmf.BuildContext context)
{
try
{
toDispose?.Dispose();
toDispose = null;
if (BuildReport.CurrentReport?.CurrentAvatar?.successful == false)
{
// This is a bit of a temporary hack until we have a better way to report errors via NDMF
ErrorReportUI.OpenErrorReportUI();
throw new Exception("Errors occurred during modular avatar processing");
}
}
catch (Exception e)
{
ErrorReportUI.OpenErrorReportUI();
throw e;
}
toDispose?.Dispose();
toDispose = null;
}
}
}

View File

@ -15,6 +15,7 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
{
public override string QualifiedName => "nadena.dev.modular-avatar";
public override string DisplayName => "Modular Avatar";
public override Texture2D LogoTexture => LogoDisplay.LOGO_ASSET;
protected override void OnUnhandledException(Exception e)
{
@ -29,6 +30,8 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
seq.Run("Clone animators", AnimationUtil.CloneAllControllers);
seq = InPhase(BuildPhase.Transforming);
seq.Run("Validate configuration",
context => ComponentValidation.ValidateAll(context.AvatarRootObject));
seq.WithRequiredExtension(typeof(ModularAvatarContext), _s1 =>
{
seq.Run(ClearEditorOnlyTags.Instance);

View File

@ -47,7 +47,7 @@ namespace nadena.dev.modular_avatar.core.editor
if (targetObject == null)
{
BuildReport.LogFatal("replace_object.null_target", new string[0],
BuildReport.LogFatal("error.replace_object.null_target", new string[0],
component, targetObject);
UnityObject.DestroyImmediate(component.gameObject);
continue;
@ -55,7 +55,7 @@ namespace nadena.dev.modular_avatar.core.editor
if (component.transform.GetComponentsInParent<Transform>().Contains(targetObject.transform))
{
BuildReport.LogFatal("replace_object.parent_of_target", new string[0],
BuildReport.LogFatal("error.replace_object.parent_of_target", new string[0],
component, targetObject);
UnityObject.DestroyImmediate(component.gameObject);
continue;
@ -63,7 +63,7 @@ namespace nadena.dev.modular_avatar.core.editor
if (replacements.TryGetValue(targetObject, out var existingReplacement))
{
BuildReport.LogFatal("replace_object.replacing_replacement", new string[0],
BuildReport.LogFatal("error.replace_object.replacing_replacement", new string[0],
component, existingReplacement.Item1);
UnityObject.DestroyImmediate(component);
continue;

View File

@ -1,35 +0,0 @@
using nadena.dev.modular_avatar.core.editor;
using NUnit.Framework;
namespace modular_avatar_tests
{
public class LocalizationTest
{
[SetUp]
public void Setup()
{
Localization.OverrideLanguage = null;
Localization.Reload();
}
[TearDown]
public void Teardown()
{
Localization.OverrideLanguage = null;
}
[Test]
public void TestLanguageFallback()
{
Localization.OverrideLanguage = "test";
Assert.AreEqual(Localization.S("test0.test_a"), "replaced");
Assert.AreEqual(Localization.S("test0.test_b"), "test_b");
Assert.AreEqual(Localization.S("test0.test_c"), "test0.test_c");
Localization.OverrideLanguage = "en";
Assert.AreEqual(Localization.S("test0.test_a"), "test_a");
Assert.AreEqual(Localization.S("test0.test_b"), "test_b");
Assert.AreEqual(Localization.S("test0.test_c"), "test0.test_c");
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: f029beaecb7c47b2889ce683b71b219f
timeCreated: 1673953287

View File

@ -1,8 +1,10 @@
using System;
using System.Linq;
using nadena.dev.modular_avatar.animation;
using nadena.dev.modular_avatar.core;
using nadena.dev.modular_avatar.core.editor;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using nadena.dev.ndmf;
using NUnit.Framework;
using UnityEngine;
@ -100,14 +102,12 @@ namespace modular_avatar_tests.ReplaceObject
var replaceObject = replacement.AddComponent<ModularAvatarReplaceObject>();
replaceObject.targetObject.Set(root);
BuildReport.Clear();
Assert.Throws<Exception>(() =>
var errors = ErrorReport.CaptureErrors(() =>
{
using (BuildReport.CurrentReport.ReportingOnAvatar(root))
{
Process(root);
}
Process(root);
});
Assert.IsTrue(errors.Any(e => e.TheError.Severity == ErrorSeverity.Error));
}
[Test]
@ -120,14 +120,12 @@ namespace modular_avatar_tests.ReplaceObject
var replaceObject = replacement.AddComponent<ModularAvatarReplaceObject>();
replaceObject.targetObject.Set(null);
BuildReport.Clear();
Assert.Throws<Exception>(() =>
var errors = ErrorReport.CaptureErrors(() =>
{
using (BuildReport.CurrentReport.ReportingOnAvatar(root))
{
Process(root);
}
Process(root);
});
Assert.IsTrue(errors.Any(e => e.TheError.Severity == ErrorSeverity.Error));
}
// Test: child object handling

View File

@ -3,6 +3,7 @@ using System.IO;
using System.Linq;
using nadena.dev.modular_avatar.core.editor;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using nadena.dev.ndmf;
using NUnit.Framework;
using UnityEditor;
using UnityEditor.Animations;
@ -39,7 +40,7 @@ namespace modular_avatar_tests
}
}
BuildReport.Clear();
ErrorReport.Clear();
objects = new List<GameObject>();
}