Skip to content

Commit

Permalink
Add localization and error reporting UI framework (#65)
Browse files Browse the repository at this point in the history
* ui: add the start of a localization framework

This also adds a skeleton error report window to exercise the new
framework.

* feat(error): Implement core of error reporting framework

* feat(error): show source object for error in NDMF error report

* error: track source of error, fail VRChat build on fatal errors

* error: stack trace rendering

* feat(error): better handling for displaying errors on avatar build failures

* feat: lazy-load language mappings to allow custom lookups to be reloaded

* chore(i18n): minor improvements to localization UI

* feat(error): lots of additional changes to get ready for MA to port over

* Group error display by source plugin
* Various bugfixes, code reorgs, etc...

* chore(error): remove debugging NDMF test error

* fix(error): fixing error reporting system-related test issues

* chore: update CHANGELOG

* chore(error): remove default impl for unity 2019 compatibility

* ui(error): add avatar selection UI to error report window

* chore(error): API and docs refinements
  • Loading branch information
bdunderscore authored Dec 21, 2023
1 parent 684f7c9 commit 652ec4f
Show file tree
Hide file tree
Showing 76 changed files with 2,710 additions and 158 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [unreleased]

### Added
- New localization framework
- New error reporting framework

### Fixed
- Play mode processing fails when installed via UPM (#89)
Expand Down
246 changes: 167 additions & 79 deletions Editor/API/BuildContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using nadena.dev.ndmf.localization;
using nadena.dev.ndmf.reporting;
using nadena.dev.ndmf.runtime;
using nadena.dev.ndmf.ui;
using nadena.dev.ndmf.util;
using UnityEditor;
using UnityEngine;
Expand All @@ -20,6 +22,24 @@

namespace nadena.dev.ndmf
{
internal sealed class ExecutionScope : IDisposable
{
private readonly ErrorReportScope _errorReportScope;
private readonly RegistryScope _registryScope;

public ExecutionScope(BuildContext ctx)
{
_errorReportScope = new ErrorReportScope(ctx._report);
_registryScope = new RegistryScope(ctx._registry);
}

public void Dispose()
{
_errorReportScope.Dispose();
_registryScope.Dispose();
}
}

/// <summary>
/// The BuildContext is passed to all plugins during the build process. It provides access to the avatar being
/// built, as well as various other context information.
Expand All @@ -30,8 +50,12 @@ public sealed partial class BuildContext
private readonly Transform _avatarRootTransform;

private Stopwatch sw = new Stopwatch();
internal readonly ObjectRegistry _registry;
internal readonly ErrorReport _report;


public ObjectRegistry ObjectRegistry => _registry;
public ErrorReport ErrorReport => _report;

/// <summary>
/// The root GameObject of the avatar being built.
/// </summary>
Expand All @@ -49,6 +73,8 @@ public sealed partial class BuildContext
/// </summary>
public UnityObject AssetContainer { get; private set; }

public bool Successful => !_report.Errors.Any(e => e.TheError.Severity >= ErrorSeverity.Error);

private Dictionary<Type, object> _state = new Dictionary<Type, object>();
private Dictionary<Type, IExtensionContext> _extensions = new Dictionary<Type, IExtensionContext>();
private Dictionary<Type, IExtensionContext> _activeExtensions = new Dictionary<Type, IExtensionContext>();
Expand All @@ -75,9 +101,11 @@ public T Extension<T>() where T : IExtensionContext
return (T) value;
}

public BuildContext(GameObject obj, string assetRootPath)
public BuildContext(GameObject obj, string assetRootPath, bool isClone = true)
{
BuildEvent.Dispatch(new BuildEvent.BuildStarted(obj));
_registry = new ObjectRegistry(obj.transform);
_report = ErrorReport.Create(obj, isClone);

Debug.Log("Starting processing for avatar: " + obj.name);
sw.Start();
Expand Down Expand Up @@ -118,6 +146,20 @@ public BuildContext(GameObject obj, string assetRootPath)
}

sw.Stop();

// Register all initially-existing GameObjects and Components
using (new RegistryScope(_registry))
{
foreach (Transform xform in _avatarRootTransform.GetComponentsInChildren<Transform>(true))
{
ObjectRegistry.GetReference(xform.gameObject);

foreach (Component c in xform.gameObject.GetComponents<Component>())
{
ObjectRegistry.GetReference(c);
}
}
}
}

private static readonly Regex WindowsReservedFileNames = new Regex(
Expand Down Expand Up @@ -227,70 +269,86 @@ public void DeactivateExtensionContext<T>() where T : IExtensionContext

public void DeactivateExtensionContext(Type t)
{
if (_activeExtensions.ContainsKey(t))
{
var ctx = _activeExtensions[t];
Profiler.BeginSample("NDMF Deactivate: " + t);
using (new ExecutionScope(this))
using (_report.WithExtensionContextTrace(t))
try
{
ctx.OnDeactivate(this);
if (_activeExtensions.ContainsKey(t))
{
var ctx = _activeExtensions[t];
Profiler.BeginSample("NDMF Deactivate: " + t);
try
{
ctx.OnDeactivate(this);
}
finally
{
Profiler.EndSample();
}

_activeExtensions.Remove(t);
}
}
finally
catch (Exception e)
{
Profiler.EndSample();
ErrorReport.ReportException(e);
}
_activeExtensions.Remove(t);
}
}

internal void RunPass(ConcretePass pass)
{
sw.Start();
using (new ExecutionScope(this))
using (_report.WithContext(pass.Plugin as PluginBase))
using (_report.WithContextPassName(pass.Description))
{
sw.Start();

ImmutableDictionary<Type, double> deactivationTimes = ImmutableDictionary<Type, double>.Empty;
ImmutableDictionary<Type, double> deactivationTimes = ImmutableDictionary<Type, double>.Empty;

foreach (var ty in pass.DeactivatePlugins)
{
Stopwatch sw2 = new Stopwatch();
sw2.Start();
DeactivateExtensionContext(ty);
deactivationTimes = deactivationTimes.Add(ty, sw2.Elapsed.TotalMilliseconds);
}
foreach (var ty in pass.DeactivatePlugins)
{
Stopwatch sw2 = new Stopwatch();
sw2.Start();
DeactivateExtensionContext(ty);
deactivationTimes = deactivationTimes.Add(ty, sw2.Elapsed.TotalMilliseconds);
}

ImmutableDictionary<Type, double> activationTimes = ImmutableDictionary<Type, double>.Empty;
foreach (var ty in pass.ActivatePlugins)
{
Stopwatch sw2 = new Stopwatch();
sw2.Start();
ActivateExtensionContext(ty);
activationTimes = activationTimes.Add(ty, sw2.Elapsed.TotalMilliseconds);
}
ImmutableDictionary<Type, double> activationTimes = ImmutableDictionary<Type, double>.Empty;
foreach (var ty in pass.ActivatePlugins)
{
Stopwatch sw2 = new Stopwatch();
sw2.Start();
ActivateExtensionContext(ty);
activationTimes = activationTimes.Add(ty, sw2.Elapsed.TotalMilliseconds);
}

Stopwatch passTimer = new Stopwatch();
passTimer.Start();
Profiler.BeginSample(pass.Description);
try
{
pass.Execute(this);
}
catch (Exception e)
{
pass.Plugin.OnUnhandledException(e);
}
finally
{
Profiler.EndSample();
passTimer.Stop();
}

BuildEvent.Dispatch(new BuildEvent.PassExecuted(
pass.InstantiatedPass.QualifiedName,
passTimer.Elapsed.TotalMilliseconds,
activationTimes,
deactivationTimes
));
Stopwatch passTimer = new Stopwatch();
passTimer.Start();
Profiler.BeginSample(pass.Description);
try
{
pass.Execute(this);
}
catch (Exception e)
{
pass.Plugin.OnUnhandledException(e);
ErrorReport.ReportException(e);
}
finally
{
Profiler.EndSample();
passTimer.Stop();
}

sw.Stop();
BuildEvent.Dispatch(new BuildEvent.PassExecuted(
pass.InstantiatedPass.QualifiedName,
passTimer.Elapsed.TotalMilliseconds,
activationTimes,
deactivationTimes
));

sw.Stop();
}
}

public T ActivateExtensionContext<T>() where T : IExtensionContext
Expand All @@ -300,47 +358,77 @@ public T ActivateExtensionContext<T>() where T : IExtensionContext

public IExtensionContext ActivateExtensionContext(Type ty)
{
if (!_extensions.TryGetValue(ty, out var ctx))
{
ctx = (IExtensionContext) ty.GetConstructor(Type.EmptyTypes).Invoke(Array.Empty<object>());
}

if (!_activeExtensions.ContainsKey(ty))
{
Profiler.BeginSample("NDMF Activate: " + ty);
using (new ExecutionScope(this))
using (_report.WithExtensionContextTrace(ty))
try
{
ctx.OnActivate(this);
if (!_extensions.TryGetValue(ty, out var ctx))
{
ctx = (IExtensionContext)ty.GetConstructor(Type.EmptyTypes).Invoke(Array.Empty<object>());
}

if (!_activeExtensions.ContainsKey(ty))
{
Profiler.BeginSample("NDMF Activate: " + ty);
try
{
ctx.OnActivate(this);
}
finally
{
Profiler.EndSample();
}

_activeExtensions.Add(ty, ctx);
}

return _activeExtensions[ty];
}
finally
catch (Exception e)
{
Profiler.EndSample();
ErrorReport.ReportException(e);
return null;
}

_activeExtensions.Add(ty, ctx);
}

return _activeExtensions[ty];
}

internal void Finish()
{
sw.Start();
foreach (var kvp in _activeExtensions.ToList())
using (new ExecutionScope(this))
{
kvp.Value.OnDeactivate(this);
if (kvp.Value is IDisposable d)
sw.Start();
foreach (var kvp in _activeExtensions.ToList())
{
d.Dispose();
using (_report.WithExtensionContextTrace(kvp.Key))
{
try
{
kvp.Value.OnDeactivate(this);

// ReSharper disable once SuspiciousTypeConversion.Global
if (kvp.Value is IDisposable d)
{
d.Dispose();
}
}
catch (Exception e)
{
ErrorReport.ReportException(e);
}
}

_activeExtensions.Remove(kvp.Key);
}

_activeExtensions.Remove(kvp.Key);
}

Serialize();
sw.Stop();
Serialize();
sw.Stop();

BuildEvent.Dispatch(new BuildEvent.BuildEnded(sw.ElapsedMilliseconds, true));
BuildEvent.Dispatch(new BuildEvent.BuildEnded(sw.ElapsedMilliseconds, true));

if (!Application.isBatchMode && _report.Errors.Count > 0)
{
ErrorReportWindow.ShowReport(_report);
}
}
}
}
}
23 changes: 17 additions & 6 deletions Editor/API/Fluent/Plugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

namespace nadena.dev.ndmf
{
internal interface IPlugin
internal interface IPluginInternal
{
string QualifiedName { get; }
string DisplayName { get; }
Expand All @@ -19,7 +19,18 @@ internal interface IPlugin
void OnUnhandledException(Exception e);
}

public abstract class Plugin<T> : IPlugin where T : Plugin<T>, new()
public abstract class PluginBase
{
public abstract string QualifiedName { get; }
public abstract string DisplayName { get; }
public virtual Texture2D LogoTexture => null;

internal PluginBase()
{
}
}

public abstract class Plugin<T> : PluginBase, IPluginInternal where T : Plugin<T>, new()
{
private static object _lock = new object();

Expand All @@ -30,10 +41,10 @@ internal interface IPlugin

private PluginInfo _info;

public virtual string QualifiedName => typeof(T).FullName;
public virtual string DisplayName => QualifiedName;
public override string QualifiedName => typeof(T).FullName;
public override string DisplayName => QualifiedName;

void IPlugin.Configure(PluginInfo info)
void IPluginInternal.Configure(PluginInfo info)
{
_info = info;
try
Expand Down Expand Up @@ -70,7 +81,7 @@ protected virtual void OnUnhandledException(Exception e)
Debug.LogException(e);
}

void IPlugin.OnUnhandledException(Exception e)
void IPluginInternal.OnUnhandledException(Exception e)
{
OnUnhandledException(e);
}
Expand Down
Loading

0 comments on commit 652ec4f

Please sign in to comment.