mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2024-12-28 10:15:06 +08:00
feat(error): Integrate with NDMF error reporting and localization system (#570)
* chore(i18n): initial integration with NDMF localization system * 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... * chore: fix u2019 build errors
This commit is contained in:
parent
a143b1edf7
commit
d297cf1cad
@ -4,7 +4,7 @@
|
||||
"version": "3.4.2"
|
||||
},
|
||||
"nadena.dev.ndmf": {
|
||||
"version": "0.3.0"
|
||||
"version": "1.3.0-alpha.0"
|
||||
}
|
||||
},
|
||||
"locked": {
|
||||
@ -17,6 +17,9 @@
|
||||
"com.vrchat.base": {
|
||||
"version": "3.4.2",
|
||||
"dependencies": {}
|
||||
},
|
||||
"nadena.dev.ndmf": {
|
||||
"version": "1.3.0-alpha.0"
|
||||
}
|
||||
}
|
||||
}
|
5
.github/ProjectRoot/vpm-manifest-2022.json
vendored
5
.github/ProjectRoot/vpm-manifest-2022.json
vendored
@ -4,7 +4,7 @@
|
||||
"version": "3.5.0"
|
||||
},
|
||||
"nadena.dev.ndmf": {
|
||||
"version": "0.3.0"
|
||||
"version": "1.3.0-alpha.0"
|
||||
}
|
||||
},
|
||||
"locked": {
|
||||
@ -17,6 +17,9 @@
|
||||
"com.vrchat.base": {
|
||||
"version": "3.5.0",
|
||||
"dependencies": {}
|
||||
},
|
||||
"nadena.dev.ndmf": {
|
||||
"version": "1.3.0-alpha.0"
|
||||
}
|
||||
}
|
||||
}
|
22
.github/ProjectRoot/vpm-manifest.json
vendored
22
.github/ProjectRoot/vpm-manifest.json
vendored
@ -1,22 +0,0 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"com.vrchat.avatars": {
|
||||
"version": "3.4.2"
|
||||
},
|
||||
"nadena.dev.ndmf": {
|
||||
"version": "0.3.0"
|
||||
}
|
||||
},
|
||||
"locked": {
|
||||
"com.vrchat.avatars": {
|
||||
"version": "3.4.2",
|
||||
"dependencies": {
|
||||
"com.vrchat.base": "3.4.2"
|
||||
}
|
||||
},
|
||||
"com.vrchat.base": {
|
||||
"version": "3.4.2",
|
||||
"dependencies": {}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a534edd7151c4cd49fe07919ae526004
|
||||
timeCreated: 1674132977
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c6dd917ee1894d58a0fa63c5edd9134d
|
||||
timeCreated: 1674042526
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 152d780db95c4e408240ec3cd4dad60e
|
||||
timeCreated: 1676979798
|
@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c9de7a0b7769b954ea5ca239afd0d4c5
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -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%;
|
||||
}
|
@ -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
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d5b79370f6c94a3eb05376006f255790
|
||||
timeCreated: 1674134609
|
@ -3,6 +3,8 @@ using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using nadena.dev.ndmf.localization;
|
||||
using nadena.dev.ndmf.ui;
|
||||
using Newtonsoft.Json;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
@ -19,13 +21,13 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
private static ImmutableDictionary<string, string> SupportedLanguageDisplayNames
|
||||
= ImmutableDictionary<string, string>.Empty
|
||||
.Add("en", "English")
|
||||
.Add("ja", "日本語")
|
||||
.Add("en-us", "English")
|
||||
.Add("ja-jp", "日本語")
|
||||
.Add("zh-hans", "简体中文")
|
||||
.Add("ko", "한국어");
|
||||
.Add("ko-kr", "한국어");
|
||||
|
||||
private static ImmutableList<string>
|
||||
SupportedLanguages = new string[] {"en", "ja", "zh-hans", "ko"}.ToImmutableList();
|
||||
SupportedLanguages = new string[] {"en-us", "ja-jp", "zh-hans", "ko-kr"}.ToImmutableList();
|
||||
|
||||
private static string[] DisplayNames = SupportedLanguages.Select(l =>
|
||||
{
|
||||
@ -37,51 +39,58 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
internal static string OverrideLanguage { get; set; } = null;
|
||||
|
||||
[MenuItem("Tools/Modular Avatar/Reload localizations")]
|
||||
public static void Reload()
|
||||
public static Localizer L { get; private set; }
|
||||
|
||||
static Localization()
|
||||
{
|
||||
Cache.Clear();
|
||||
OnLangChange?.Invoke();
|
||||
Localizer localizer = new Localizer(SupportedLanguages[0], () =>
|
||||
{
|
||||
List<(string, Func<string, string>)> languages = new List<(string, Func<string, string>)>();
|
||||
|
||||
foreach (var lang in SupportedLanguages)
|
||||
{
|
||||
languages.Add((lang, LanguageLookup(lang)));
|
||||
}
|
||||
|
||||
return languages;
|
||||
});
|
||||
|
||||
L = localizer;
|
||||
|
||||
LanguagePrefs.RegisterLanguageChangeCallback(typeof(Localization), _ => OnLangChange?.Invoke());
|
||||
}
|
||||
|
||||
private static ImmutableDictionary<string, string> GetLocalization(string lang)
|
||||
private static Func<string,string> LanguageLookup(string lang)
|
||||
{
|
||||
if (Cache.TryGetValue(lang, out var info))
|
||||
{
|
||||
return info;
|
||||
}
|
||||
|
||||
var fallback = lang == FallbackLanguage
|
||||
? ImmutableDictionary<string, string>.Empty
|
||||
: GetLocalization(FallbackLanguage);
|
||||
|
||||
var filename = localizationPathRoot + "/" + lang + ".json";
|
||||
|
||||
try
|
||||
{
|
||||
var langData = File.ReadAllText(filename);
|
||||
var tmp = JsonConvert.DeserializeObject<Dictionary<string, string>>(langData);
|
||||
var langMap = JsonConvert.DeserializeObject<Dictionary<string, string>>(langData);
|
||||
|
||||
foreach (var kvp in fallback)
|
||||
// return langMap.GetValueOrDefault; - Unity 2019 doesn't have this extension method
|
||||
return key =>
|
||||
{
|
||||
if (!tmp.ContainsKey(kvp.Key))
|
||||
{
|
||||
tmp[kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
info = tmp.ToImmutableDictionary();
|
||||
Cache[lang] = info;
|
||||
return info;
|
||||
if (langMap.TryGetValue(key, out var val)) return val;
|
||||
else return null;
|
||||
};
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError("Failed to load language file " + filename);
|
||||
Debug.LogException(e);
|
||||
return ImmutableDictionary<string, string>.Empty;
|
||||
return (k) => null;
|
||||
}
|
||||
}
|
||||
|
||||
[MenuItem("Tools/Modular Avatar/Reload localizations")]
|
||||
public static void Reload()
|
||||
{
|
||||
Localizer.ReloadLocalizations();
|
||||
Cache.Clear();
|
||||
}
|
||||
|
||||
public static GUIContent G(string key)
|
||||
{
|
||||
var tooltip = S(key + ".tooltip", null);
|
||||
@ -99,7 +108,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
return string.Format(S(key, key), format);
|
||||
}
|
||||
catch (FormatException e)
|
||||
catch (FormatException)
|
||||
{
|
||||
return S(key, key) + "(" + string.Join(", ", format) + ")";
|
||||
}
|
||||
@ -107,11 +116,9 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
public static string S(string key, string defValue)
|
||||
{
|
||||
var info = GetLocalization(GetSelectedLocalization());
|
||||
|
||||
if (info.TryGetValue(key, out var value))
|
||||
if (L.TryGetLocalizedString(key, out var val))
|
||||
{
|
||||
return value;
|
||||
return val;
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -121,22 +128,14 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
public static string GetSelectedLocalization()
|
||||
{
|
||||
return OverrideLanguage ?? EditorPrefs.GetString("nadena.dev.modularavatar.lang", "en");
|
||||
return LanguagePrefs.Language;
|
||||
}
|
||||
|
||||
public static void ShowLanguageUI()
|
||||
{
|
||||
EditorGUILayout.Separator();
|
||||
|
||||
var curLang = GetSelectedLocalization();
|
||||
|
||||
var curIndex = SupportedLanguages.IndexOf(curLang);
|
||||
var newIndex = EditorGUILayout.Popup("Editor Language", curIndex, DisplayNames);
|
||||
if (newIndex != curIndex)
|
||||
{
|
||||
EditorPrefs.SetString("nadena.dev.modularavatar.lang", SupportedLanguages[newIndex]);
|
||||
Reload();
|
||||
}
|
||||
|
||||
LanguageSwitcher.DrawImmediate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
@ -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以下のオブジェクトの処理中に発生しました:",
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"test0.test_a": "replaced"
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a851b660ad5443bf92884fdf8e872c4a
|
||||
timeCreated: 1673953035
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f029beaecb7c47b2889ce683b71b219f
|
||||
timeCreated: 1673953287
|
@ -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]
|
||||
@ -119,15 +119,13 @@ 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
|
||||
|
@ -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>();
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,6 @@
|
||||
},
|
||||
"vpmDependencies": {
|
||||
"com.vrchat.avatars": ">=3.2.0",
|
||||
"nadena.dev.ndmf": ">=1.2.5 <2.0.0-a"
|
||||
"nadena.dev.ndmf": ">=1.3.0-alpha.0 <2.0.0-a"
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user