diff --git a/Editor/API/Attributes/BuildPhase.cs b/Editor/API/Attributes/BuildPhase.cs index 7c3d31c8..0d90b307 100644 --- a/Editor/API/Attributes/BuildPhase.cs +++ b/Editor/API/Attributes/BuildPhase.cs @@ -6,7 +6,17 @@ namespace nadena.dev.ndmf { - public class BuildPhase + /// + /// Build Phases provide a coarse mechanism for grouping passes for execution. Each build phase has a recommended + /// usage to help avoid ordering conflicts without needing explicit constraints. + /// + /// Currently, the following phases are defined: + /// - Resolving + /// - Generating + /// - Transforming + /// - Optimizing + /// + public sealed class BuildPhase { public string Name { get; } @@ -16,11 +26,39 @@ internal BuildPhase(string name) Name = name; } + /// + /// The resolving phase is intended for use by passes which perform very early processing of components and + /// avatar state, before any large-scale changes have been made. For example, Modular Avatar uses this phase + /// to resolve string-serialized object passes to their destinations, and to clone animation controllers before + /// any changes are made to them. + /// + /// NDMF also has a built-in phase in Resolving, which removes EditorOnly objects. For more information, + /// see nadena.dev.ndmf.builtin.RemoveEditorOnlyPass. + /// + /// public static readonly BuildPhase Resolving = new BuildPhase("Resolving"); + + /// + /// The generating phase is intended for use by asses which generate components used by later plugins. For + /// example, if you want to generate components that will be used by Modular Avatar, this would be the place + /// to do it. + /// public static readonly BuildPhase Generating = new BuildPhase("Generating"); + + /// + /// The transforming phase is intended for general-purpose avatar transformations. Most of Modular Avatar's + /// logic runs here. + /// public static readonly BuildPhase Transforming = new BuildPhase("Transforming"); + + /// + /// The optimizing phase is intended for pure optimizations that need to run late in the build process. + /// public static readonly BuildPhase Optimizing = new BuildPhase("Optimizing"); + /// + /// This list contains all built-in phases in the order that they will be executed. + /// public static readonly ImmutableList BuiltInPhases = ImmutableList.Create(Resolving, Generating, Transforming, Optimizing); diff --git a/Editor/API/Attributes/ExportsPlugin.cs b/Editor/API/Attributes/ExportsPlugin.cs index 23e929e5..115befca 100644 --- a/Editor/API/Attributes/ExportsPlugin.cs +++ b/Editor/API/Attributes/ExportsPlugin.cs @@ -8,9 +8,13 @@ namespace nadena.dev.ndmf { /// /// This attribute declares a plugin to be registered with NDMF. - /// + /// /// /// [assembly: ExportsPlugin(typeof(MyPlugin))] + /// + /// class MyPlugin : Plugin<MyPlugin> { + /// // ... + /// } /// /// [AttributeUsage(AttributeTargets.Assembly)] diff --git a/Editor/API/Fluent/Sequence/Extensions.cs b/Editor/API/Fluent/Sequence/Extensions.cs index 857038bb..bec57509 100644 --- a/Editor/API/Fluent/Sequence/Extensions.cs +++ b/Editor/API/Fluent/Sequence/Extensions.cs @@ -119,6 +119,7 @@ public void WithCompatibleExtension(Type extension, Action action) /// sequence.WithRequiredExtensions(new[] {typeof(foo.bar.MyExtension)}, s => { /// s.Run(typeof(MyPass)); /// }); + /// /// /// The extension to request /// An action that will be invoked with the extensions marked required @@ -145,6 +146,7 @@ public void WithRequiredExtensions(IEnumerable extensions, Action { /// s.Run(typeof(MyPass)); /// }); + /// /// /// The extension to request /// An action that will be invoked with the extensions marked required diff --git a/Editor/API/Fluent/Sequence/Sequence.cs b/Editor/API/Fluent/Sequence/Sequence.cs index aa5e5f36..6e8361c6 100644 --- a/Editor/API/Fluent/Sequence/Sequence.cs +++ b/Editor/API/Fluent/Sequence/Sequence.cs @@ -1,5 +1,6 @@ #region +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using nadena.dev.ndmf.model; @@ -18,6 +19,7 @@ namespace nadena.dev.ndmf.fluent /// /// sequence.Run(typeof(MyPass)) // returns DeclaringPass /// .BeforePass(typeof(OtherPass)); // valid only on DeclaringPass + /// .Then.Run(typeof(OtherPass)); /// /// public sealed class DeclaringPass @@ -25,12 +27,27 @@ public sealed class DeclaringPass private readonly SolverContext _solverContext; private readonly BuildPhase _phase; private readonly SolverPass _pass; + private readonly Sequence _seq; - internal DeclaringPass(SolverPass pass, SolverContext solverContext, BuildPhase phase) + /// + /// Returns the original sequence that returned this DeclaringPass. This is useful for chaining multiple + /// pass declarations, like so: + /// + /// + /// InPhase(Generating) + /// .Run(typeof(PassOne)) + /// .Then.Run(typeof(PassTwo)); + /// + /// + [SuppressMessage("ReSharper", "ConvertToAutoProperty")] + public Sequence Then => _seq; + + internal DeclaringPass(SolverPass pass, SolverContext solverContext, BuildPhase phase, Sequence seq) { _pass = pass; _solverContext = solverContext; _phase = phase; + _seq = seq; } /// @@ -208,7 +225,7 @@ private DeclaringPass InternalRun(IPass pass, string sourceFile, int sourceLine) _priorPass = solverPass; OnNewPass(solverPass); - return new DeclaringPass(solverPass, _solverContext, _phase); + return new DeclaringPass(solverPass, _solverContext, _phase, this); } /// diff --git a/Editor/API/IExtensionContext.cs b/Editor/API/IExtensionContext.cs index 23305f51..23d67ec0 100644 --- a/Editor/API/IExtensionContext.cs +++ b/Editor/API/IExtensionContext.cs @@ -1,8 +1,20 @@ namespace nadena.dev.ndmf { + /// + /// The IExtensionContext is declared by custom extension contexts. + /// public interface IExtensionContext { + /// + /// Invoked when the extension is activated. + /// + /// void OnActivate(BuildContext context); + + /// + /// Invoked when the extension is deactivated. + /// + /// void OnDeactivate(BuildContext context); } } \ No newline at end of file diff --git a/Editor/API/Util/VisitAssets.cs b/Editor/API/Util/VisitAssets.cs index 19e6f1aa..6589d075 100644 --- a/Editor/API/Util/VisitAssets.cs +++ b/Editor/API/Util/VisitAssets.cs @@ -8,10 +8,22 @@ namespace nadena.dev.ndmf.util { + /// + /// This class provides helpers to traverse assets or asset properties referenced from a given root object. + /// public static class VisitAssets { public delegate bool AssetFilter(Object obj); + /// + /// Returns an enumerable of all assets referenced by the given root object. + /// + /// The asset to start traversal from + /// If false, traversal will not return assets that are saved + /// If false, scene assets will not be returned + /// If provided, this filter will be queried for each object encountered; if it + /// returns false, the selected object and all objects referenced from it will be ignored. + /// An enumerable of objects found public static IEnumerable ReferencedAssets( this Object root, bool traverseSaved = true, @@ -101,8 +113,18 @@ public static IEnumerable ReferencedAssets( } } + /// + /// Provides helpers to walk the properties of a SerializedObject + /// public static class WalkObjectProps { + /// + /// Returns an enumerable that will return _most_ of the properties of a SerializedObject. In particular, this + /// skips the contents of certain large arrays (e.g. the character contents of strings and the contents of + /// AnimationClip curves). + /// + /// The SerializedObject to traverse + /// The SerializedProperties found public static IEnumerable AllProperties(this SerializedObject obj) { var target = obj.targetObject; @@ -144,6 +166,11 @@ public static IEnumerable AllProperties(this SerializedObjec } } + /// + /// Returns all ObjectReference properties of a SerializedObject + /// + /// + /// public static IEnumerable ObjectProperties(this SerializedObject obj) { foreach (var prop in obj.AllProperties()) diff --git a/Editor/ApplyOnPlay.cs b/Editor/ApplyOnPlay.cs index 9149e8eb..c797be3c 100644 --- a/Editor/ApplyOnPlay.cs +++ b/Editor/ApplyOnPlay.cs @@ -26,6 +26,7 @@ #region +using nadena.dev.ndmf.config; using nadena.dev.ndmf.runtime; using UnityEditor; using UnityEngine; @@ -60,7 +61,7 @@ static ApplyOnPlay() private static void MaybeProcessAvatar(ApplyOnPlayGlobalActivator.OnDemandSource source, MonoBehaviour component) { - if (Settings.ApplyOnPlay && source == armedSource && component != null) + if (Config.ApplyOnPlay && source == armedSource && component != null) { var avatar = RuntimeUtil.FindAvatarInParents(component.transform); if (avatar == null) return; diff --git a/Editor/AvatarProcessor.cs b/Editor/AvatarProcessor.cs index 486acb02..fa645cfa 100644 --- a/Editor/AvatarProcessor.cs +++ b/Editor/AvatarProcessor.cs @@ -113,7 +113,7 @@ public static void ProcessAvatar(GameObject root) ProcessAvatar(buildContext, BuildPhase.Resolving, BuildPhase.Optimizing); buildContext.Finish(); - if (RuntimeUtil.isPlaying) + if (RuntimeUtil.IsPlaying) { root.AddComponent(); } diff --git a/Editor/Settings.cs b/Editor/Config.cs similarity index 81% rename from Editor/Settings.cs rename to Editor/Config.cs index 8a9b7246..f136cae3 100644 --- a/Editor/Settings.cs +++ b/Editor/Config.cs @@ -29,12 +29,16 @@ #endregion -namespace nadena.dev.ndmf +namespace nadena.dev.ndmf.config { - public static class Settings + public static class Config { + // Preserve apply-on-play config from pre-1.8 versions of Modular Avatar private const string PREFKEY_APPLY_ON_PLAY = "nadena.dev.modular-avatar.applyOnPlay"; + /// + /// Controls whether NDMF transformations will be applied at play time. + /// public static bool ApplyOnPlay { get => EditorPrefs.GetBool(PREFKEY_APPLY_ON_PLAY, true); @@ -45,7 +49,11 @@ public static bool ApplyOnPlay } } + /// + /// This event will be invoked when any config value changes. + /// public static event Action OnChange; + private static void NotifyChange() => OnChange?.Invoke(); } } \ No newline at end of file diff --git a/Editor/Settings.cs.meta b/Editor/Config.cs.meta similarity index 100% rename from Editor/Settings.cs.meta rename to Editor/Config.cs.meta diff --git a/Editor/GlobalInit.cs b/Editor/GlobalInit.cs index 9de7973c..4061db98 100644 --- a/Editor/GlobalInit.cs +++ b/Editor/GlobalInit.cs @@ -12,7 +12,7 @@ internal static class GlobalInit { static GlobalInit() { - RuntimeUtil.delayCall = call => { EditorApplication.delayCall += () => call(); }; + RuntimeUtil.DelayCall = call => { EditorApplication.delayCall += () => call(); }; } } } \ No newline at end of file diff --git a/Editor/UI/Menus.cs b/Editor/UI/Menus.cs index 3ce85e3a..2cee9a53 100644 --- a/Editor/UI/Menus.cs +++ b/Editor/UI/Menus.cs @@ -1,5 +1,6 @@ #region +using nadena.dev.ndmf.config; using UnityEditor; using UnityObject = UnityEngine.Object; @@ -16,7 +17,7 @@ internal static class Menus static void Init() { EditorApplication.delayCall += OnSettingsChanged; - Settings.OnChange += OnSettingsChanged; + Config.OnChange += OnSettingsChanged; } // Avoid cluttering the GameObject context menu with duplicate entries. Users are more familiar with MA anyway, @@ -38,12 +39,12 @@ public static void ApplyToCurrentAvatarGameobject() [MenuItem(APPLY_ON_PLAY_MENU_NAME, false, APPLY_ON_PLAY_PRIO)] private static void ApplyOnPlay() { - Settings.ApplyOnPlay = !Settings.ApplyOnPlay; + Config.ApplyOnPlay = !Config.ApplyOnPlay; } private static void OnSettingsChanged() { - Menu.SetChecked(APPLY_ON_PLAY_MENU_NAME, Settings.ApplyOnPlay); + Menu.SetChecked(APPLY_ON_PLAY_MENU_NAME, Config.ApplyOnPlay); } } } \ No newline at end of file diff --git a/Runtime/ApplyOnPlayGlobalActivator.cs b/Runtime/ApplyOnPlayGlobalActivator.cs index b3bf6b45..9de512de 100644 --- a/Runtime/ApplyOnPlayGlobalActivator.cs +++ b/Runtime/ApplyOnPlayGlobalActivator.cs @@ -48,7 +48,7 @@ internal enum OnDemandSource private void Awake() { - if (!RuntimeUtil.isPlaying || this == null) return; + if (!RuntimeUtil.IsPlaying || this == null) return; var scene = gameObject.scene; foreach (var root in scene.GetRootGameObjects()) @@ -117,13 +117,13 @@ public class AvatarActivator : MonoBehaviour { private void Awake() { - if (!RuntimeUtil.isPlaying || this == null) return; + if (!RuntimeUtil.IsPlaying || this == null) return; ApplyOnPlayGlobalActivator.OnDemandProcessAvatar(ApplyOnPlayGlobalActivator.OnDemandSource.Awake, this); } private void Start() { - if (!RuntimeUtil.isPlaying || this == null) return; + if (!RuntimeUtil.IsPlaying || this == null) return; ApplyOnPlayGlobalActivator.OnDemandProcessAvatar(ApplyOnPlayGlobalActivator.OnDemandSource.Start, this); } diff --git a/Runtime/GeneratedAssets.cs b/Runtime/GeneratedAssets.cs index a1eb8a94..a4c5136f 100644 --- a/Runtime/GeneratedAssets.cs +++ b/Runtime/GeneratedAssets.cs @@ -4,6 +4,9 @@ namespace nadena.dev.ndmf.runtime { + /// + /// This ScriptableObject is used as the root asset when storing generated assets. + /// [PreferBinarySerialization] public class GeneratedAssets : ScriptableObject { diff --git a/Runtime/RuntimeUtil.cs b/Runtime/RuntimeUtil.cs index cb9bee9d..381aa05c 100644 --- a/Runtime/RuntimeUtil.cs +++ b/Runtime/RuntimeUtil.cs @@ -15,11 +15,11 @@ public static class RuntimeUtil /// Invoke this function to register a callback with EditorApplication.delayCall from a context that cannot /// access EditorApplication. /// - public static Action delayCall { get; internal set; } + public static Action DelayCall { get; internal set; } static RuntimeUtil() { - delayCall = action => { throw new Exception("delayCall() cannot be called during static initialization"); }; + DelayCall = action => { throw new Exception("delayCall() cannot be called during static initialization"); }; } // Shadow the VRC-provided methods to avoid deprecation warnings @@ -39,9 +39,9 @@ internal static T GetOrAddComponent(this Component obj) where T : Component /// Returns whether the editor is in play mode. /// #if UNITY_EDITOR - public static bool isPlaying => UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode; + public static bool IsPlaying => UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode; #else - public static bool isPlaying => true; + public static bool IsPlaying => true; #endif /// diff --git a/docfx~/api/index.md b/docfx~/api/index.md index 78dc9c00..7eeececc 100644 --- a/docfx~/api/index.md +++ b/docfx~/api/index.md @@ -1,2 +1,3 @@ -# PLACEHOLDER -TODO: Add .NET projects to the *src* folder and run `docfx` to generate **REAL** *API Documentation*! +# API Documentation + +Click the namespace entries on the left to see detailed API documentation. \ No newline at end of file diff --git a/docfx~/articles/intro.md b/docfx~/articles/intro.md deleted file mode 100644 index c0478ced..00000000 --- a/docfx~/articles/intro.md +++ /dev/null @@ -1 +0,0 @@ -# Add your introductions here! diff --git a/docfx~/articles/toc.yml b/docfx~/articles/toc.yml deleted file mode 100644 index ff89ef1f..00000000 --- a/docfx~/articles/toc.yml +++ /dev/null @@ -1,2 +0,0 @@ -- name: Introduction - href: intro.md diff --git a/docfx~/docfx.json b/docfx~/docfx.json index 00be47ef..c0ecb72b 100644 --- a/docfx~/docfx.json +++ b/docfx~/docfx.json @@ -20,10 +20,10 @@ "includePrivateMembers": false, "disableGitFeatures": false, "disableDefaultFilter": false, - "noRestore": false, - "namespaceLayout": "flattened", + "noRestore": true, + "namespaceLayout": "nested", "memberLayout": "samePage", - "EnumSortOrder": "alphabetic", + "EnumSortOrder": "declaringOrder", "allowCompilationErrors": true } ], diff --git a/docfx~/execution-model.md b/docfx~/execution-model.md new file mode 100644 index 00000000..52a2dda7 --- /dev/null +++ b/docfx~/execution-model.md @@ -0,0 +1,126 @@ +# Execution Model + +NDMF supports a rich set of ordering constraints that can control what order processing occurs in. + +## High level model + +At a high level, there are a few main concepts behind NDMF's sequencing model. + +First, we have plugins. Plugins are intended to be the main end-user-visible unit of sequencing. Each plugin then +contains some number of _passes_, which are organized into _sequences_, which are further within _build phases_. + +A pass is simply a callback which is executed at a particular point in the build. A sequence is a collection of passes +that occur in a particular order. Finally, build phases provide a coarse way of grouping sequences together. + +## Build phases + +The following build phases are defined: + +#### Resolving + +The resolving phase is intended for use by passes which perform very early processing of components and +avatar state, before any large-scale changes have been made. For example, Modular Avatar uses this phase +to resolve string-serialized object passes to their destinations, and to clone animation controllers before +any changes are made to them. + +NDMF also has a built-in phase in Resolving, which removes EditorOnly objects. For more information, +see nadena.dev.ndmf.builtin.RemoveEditorOnlyPass. + +#### Generating + +The generating phase is intended for use by asses which generate components used by later plugins. For +example, if you want to generate components that will be used by Modular Avatar, this would be the place +to do it. + +#### Transforming + +The transforming phase is intended for general-purpose avatar transformations. Most of Modular Avatar's +logic runs here. + +#### Optimizing + +The optimizing phase is intended for pure optimizations that need to run late in the build process. + +## Sequences and pass constraints + +When declaring passes, you first create a sequence, then declare passes within that sequence. If necessary, +you can apply additional constraints, which let you inject additional passes at almost arbitrary points in the build +process. + +```csharp + +public class MyPlugin : Plugin +{ + public override string DisplayName => "Baby's first plugin"; + + protected override void Configure() + { + Sequence seq = InPhase(BuildPhase.Transforming); + seq + .AfterPass(typeof(SomePriorPass)) + .Run(typeof(Pass1)) + .BeforePass(typeof(SomeOtherPass)) + .Then.Run(typeof(Pass2)); + } +} +``` + +Sequences enforce that passes are executed in the order they are declared. Generally, NDMF will try to run passes in a +sequence right after each other, unless some constraints prevent that from happening. + +If you declare multiple sequences in the same build phase, those sequences might be executed in any order relative to +each other (or even interleaved!). + +### Constraint types + +There are several types of constraints that can be applied to passes: + +#### Before/AfterPlugin constraints. + +These declare that this particular _sequence_ runs, in its entirety, before or after _all_ processing by some other +plugin. Note that this does not enforce that the other plugin is loaded; it only enforces that if the other plugin is +loaded, it will run before or after this sequence. + +```csharp + +sequence.BeforePlugin("other.plugin.name"); +sequence.AfterPlugin(typeof(OtherPlugin)); + +``` + +Because this is intended to help resolve conflicts between optional dependencies, this accepts string names as well as +types. If you use a string name, it must be the fully-qualified name of the plugin (by default, this will be the +fully-qualified type name of the plugin). + +#### Before/AfterPass constraints. + +These declare that a single pass within a larger sequence runs before or after another pass. + +```csharp + +sequence.AfterPass(typeof(OtherPass)) + .Run(typeof(MyPass)) + .BeforePass(typeof(SomeOtherPass)); + +``` + +Note that the declaration order mirrors the order in which these passes will be executed. In the above example, +the execution order will be OtherPass, MyPass, SomeOtherPass - though, other passes might happen in between. + +#### WaitFor constraints + +The WaitFor constraint is similar to AfterPass, but NDMF will attempt to run the pass as soon as possible after the +specified pass. This is useful when you want to insert some processing between two passes within another plugin. + +```csharp + +sequence.WaitFor(typeof(OtherPass)) + .Run(typeof(MyPass)); + +``` + +Note that this does not guarantee that the pass will run _immediately_ after the specified pass. If you have other +dependencies on `MyPass` that are not yet satisfied, then `MyPass` will not run until those dependencies are satisfied. + +Additionally, there might be multiple `WaitFor` dependencies, in which case the order in which these are executed - +absent other constraints - is undefined. \ No newline at end of file diff --git a/docfx~/extension-context.md b/docfx~/extension-context.md new file mode 100644 index 00000000..d3b3a371 --- /dev/null +++ b/docfx~/extension-context.md @@ -0,0 +1,25 @@ +# Extension Contexts + +Extension contexts are designed to help improve build performance by amortizing some kind of processing across +multiple passes. For example, if you have multiple passes which need to do the same analysis across all animation +controllers, you can use an extension context to do that analysis once, then share the results with all passes. + +Passes declare that they either require or are compatible with specific extension context. When a pass declares that it +requires an extension context, the extension context will be "activated" before the pass is executed. The extension +context will then be "deactivated" when any pass that is not compatible with it is executed. This allows for any +deferred operations (e.g. updating animations after objects move around) to be performed. + +The compatibility declaration is important as - for example - there might be deferred work that needs to be performed +before a pass that is oblivious to the extension context can be allowed to execute. + +You can declare that a pass requires or is compatible with an extension context like so: + +```csharp + +sequence.WithCompatibleExtension("foo.bar.ExtensionClass", seq2 => { + seq2.WithRequiredExtension(typeof(OtherExtensionClass), seq3 => { + seq3.Run(typeof(MyPass)); + }); +}); + +``` \ No newline at end of file diff --git a/docfx~/index.md b/docfx~/index.md index 3ae25063..f61914c2 100644 --- a/docfx~/index.md +++ b/docfx~/index.md @@ -1,4 +1,44 @@ -# This is the **HOMEPAGE**. -Refer to [Markdown](http://daringfireball.net/projects/markdown/) for how to write markdown files. -## Quick Start Notes: -1. Add images to the *images* folder if the file is referencing an image. +# NDM Framework + +NFM Framework ("Nademof" for short) is a framework for running non-destructive build plugins when building avatars for +VRChat (and, eventually, for other VRSNS platforms). + +## Why is this needed? + +While the VRChat SDK has support for running callbacks at build time, it does not provide support for running callbacks +when entering play mode. If each plugin were to develop its own logic for running in play mode, this would lead to +compatibility issues (and, in fact, this has happened already!) + +NDM Framework's primary goal is to improve compatibility when multiple nondestructive plugins are loaded. It also aims +to make writing nondestructive build plugins easier. + +## How do I get started? + +Getting started is easy; a simple nondestructive plugin can look like this: + +```csharp + +[assembly: ExportsPlugin(typeof(MyPlugin))] + +public class MyPlugin : Plugin +{ + public override string DisplayName => "Baby's first plugin"; + + protected override void Configure() + { + InPhase(BuildPhase.Transforming).Run("Do the thing", ctx => + { + Debug.Log("Hello world!"); + }); + } +} + +``` + +NDM Framework supports more advanced features such as dependency ordering, but this is enough to get started. + +For more information, see the [API documentation](api/index.html) or the other articles linked from the sidebar. + +## Support model + +NDMF is currently in an alpha state. The API is not fully stable yet - hopefully soon, though! \ No newline at end of file diff --git a/docfx~/toc.yml b/docfx~/toc.yml index 59f80104..7daaf996 100644 --- a/docfx~/toc.yml +++ b/docfx~/toc.yml @@ -1,5 +1,11 @@ -- name: Articles - href: articles/ +- name: Introduction + href: index.md - name: Api Documentation href: api/ homepage: api/index.md +- name: Concepts + items: + - name: Execution Model + href: execution-model.md + - name: Extension Contexts + href: extension-context.md \ No newline at end of file