Non-Destructive Modular Framework (NDMF)

repository·main·Indexed 41 days ago

https://github.com/bdunderscore/ndmf

A framework for building non-destructive editor plugins for VRChat Avatar 3.0. It manages complex execution sequencing, asset generation, and transformation phases (Resolving, Generating, Transforming, and Optimizing) to ensure modularity without permanently altering source avatar assets. Version 1.14.8.

Tokens
7.9K
Snippets
21
Records
46
Agent score
99%

What's inside NDMF

  1. What is the NDM Framework?

    main

    The Non-Destructive Modular Framework ("なでもふ" or "Nademof") is a framework designed for running non-destructive build plugins when building avatars for VRChat and other VRSNS platforms.

    Its primary goals are:

    1. Improve Compatibility: It provides a unified way to run callbacks during both build time and play mode, preventing compatibility issues that arise when multiple plugins attempt to manage their own play-mode logic.
    2. Ease of Development: It simplifies the process of writing non-destructive build plugins by providing a structured execution model and dependency management.
  2. How extension contexts work

    main

    An Extension Context is a callback executed before and after a group of passes that require its services (e.g., TrackObjectRenamesContext for tracking renames and updating animation paths). This allows the framework to amortize the cost of the context across multiple passes.

    Declaring Context Requirements

    Passes can declare which contexts they need or are compatible with:

    • RequiredContexts: Instructs NDMF to "activate" the context before the pass. The context is deactivated before the next incompatible pass or when the build completes.
    • CompatibleContexts: Defines which contexts the pass can work alongside.

    Access an active context via BuildContext.Extension<ExtensionName>().

    abstract class MAPass : PluginPass
    {
        public override IImmutableSet<Type> RequiredContexts =>
            ImmutableHashSet<Type>.Empty.Add(typeof(ModularAvatarContext));
        
        public override IImmutableSet<object> CompatibleContexts =>
            ImmutableHashSet<Type>.Empty.Add(typeof(TrackObjectRenamesContext));
    
        protected BuildContext MAContext(build_framework.BuildContext context)
        {
            return context.Extension<ModularAvatarContext>().BuildContext;
        }
    }
  3. Understand the available Build Phases

    main

    NDMF provides four main build phases to categorize processing tasks:

    • Resolving: For early-stage processing, such as resolving string-serialized object passes or cloning animation controllers before large-scale changes occur. Includes a built-in pass nadena.dev.ndmf.builtin.RemoveEditorOnlyPass to remove EditorOnly objects.
    • Generating: For generating components that will be consumed by other plugins later in the build.
    • Transforming: For general-purpose avatar transformations (the phase where most Modular Avatar logic resides).
    • Optimizing: For pure optimizations that must run late in the build process.
  4. Configure platform-specific pass execution

    main

    NDMF supports multiplatform builds by allowing you to specify which platforms a pass should run on. By default, passes run only on VRChat SDK 3.0.

    You can control this using the following methods and attributes:

    • Methods: Sequence.OnAllPlatforms() and Sequence.OnPlatforms()
    • Attributes: [RunsOnAllPlatforms] and [RunsOnPlatform]
    • Helper Class: WellKnownPlatforms (introduced in 1.8.0-alpha.0)
    // Example usage of attributes for a pass class
    [RunsOnPlatform(WellKnownPlatforms.VRChat)]
    public class MyPlatformSpecificPass : IPass { ... }
  5. Use unique types for BuildContext state

    main
    When using BuildContext.GetState, always create a unique, dedicated state type (e.g., public class MyPluginState { ... }) rather than using generic types like Dictionary<string, object> or List<GameObject>. This prevents naming collisions and data conflicts with other plugins using the same state storage.
  6. How sequencing and constraints work in NDMF

    main

    Sequencing in NDMF determines the execution order of plugin passes. This is achieved through a combination of Phases and Constraints.

    The Execution Model

    Execution is organized as a tree where Phases are nodes and Passes are leaves. Each phase has its own independent dependency ordering.

    Constraint Types

    Constraints allow plugin authors to express ordering requirements. They are ranked by "binding strength":

    1. After (runs-soon-after): A strong constraint requiring a pass to run as soon as possible after another. Only one After constraint can be specified per target pass.
    2. Successor constraints: Internally defined by the order in which passes are declared within a phase. If no After constraint intervenes, the framework prefers to run the next declared pass immediately after the current one.
    3. NotBefore / NotAfter: Declares that one pass must run after/before another, but without a strong preference for how quickly it happens. These are converted into topological sort requirements.

    Constraint Scopes

    Constraints are phase-local. This means:

    • Pass/Phase targets: Must exist within the same parent phase.
    • Plugin targets: Constraints applied to a plugin (e.g., .AfterPlugin("name")) apply to all passes or phases belonging to that plugin within the same phase.
    • Successors: Only generated between passes in the same phase.

    If you need to enforce an order across different phases, you cannot use standard constraints; you must ensure the logic is handled via the phase hierarchy or by declaring a pseudopass that bridges the requirements.

    // Example of the hierarchy: 
    // Phases (Nodes) -> Passes (Leaves)
    // Constraints are applied within the scope of a Phase.
  7. How the NDMF execution model works

    main

    NDMF uses a hierarchy of Plugins and Passes.

    • Plugins: End-user-visible extensions (e.g., Modular Avatar).
    • Passes: Internal steps within a plugin. Breaking a plugin into multiple passes allows for fine-grained execution ordering.

    Passes are grouped into Phases, which execute in this specific order:

    1. Resolving: Run before any editor extensions modify the avatar. Use this to resolve serialized paths/strings to objects before transformations occur.
    2. Generating: Run before extensions that generate new objects/components for other systems.
    3. Transforming: The general-purpose phase where most avatar transformations occur.
    4. Optimizing: For optimization plugins that do not change the semantic meaning of the avatar.

    Note: Within a phase, passes execute in the order they are declared, unless dependency constraints are applied.

  8. Handle object modifications from other plugins

    main

    Do not assume that GameObjects, components, or assets will remain unchanged between your Sequences. Other plugins may run between your steps, even if you use Sequence.BeforePlugin or Sequence.AfterPlugin constraints.

    Key Rule: Do not cache assets retrieved from the avatar between Sequences. If you need to generate objects based on settings, perform the reading, generation, and assignment within the same Sequence to ensure consistency.

  9. How extension contexts work in NDMF

    main

    Extension contexts are used to improve build performance by amortizing expensive processing across multiple passes. Instead of every pass performing the same analysis (e.g., analyzing all animation controllers), an extension context performs the analysis once and shares the results with all participating passes.

    Lifecycle and Activation

    • Activation: An extension context is "activated" before a pass that declares it as a requirement is executed.
    • Deactivation: An extension context is "deactivated" when a pass is executed that is not compatible with it. This deactivation phase allows the context to perform deferred operations (such as updating animations after objects have moved).

    Compatibility vs. Requirement

    • Required Extension: A pass that needs the data/state provided by the context. The context is guaranteed to be active.
    • Compatible Extension: A pass that can work alongside the context without breaking it. This allows the context to remain active even if the pass doesn't explicitly use its data, preventing unnecessary deactivation/reactivation cycles.
    // Example of nesting requirements and compatibility
    sequence.WithCompatibleExtension("foo.bar.ExtensionClass", seq2 => {
        seq2.WithRequiredExtension(typeof(OtherExtensionClass), seq3 => {
            seq3.Run(typeof(MyPass));
        });
    });
  10. Understand NDMF versioning and breaking changes

    main

    NDMF follows Semantic Versioning 2.0.0 (MAJOR.MINOR.PATCH).

    • MAJOR version changes: Indicate non-backwards-compatible changes, such as removing API members or adding abstract methods to abstract classes.
    • MINOR version changes: Indicate backwards-compatible changes, such as adding new API members or new abstract classes. Note: Minor versions are also incremented when changes are made to execution order heuristics.
    • PATCH version changes: Indicate backwards-compatible bug fixes.

    If you encounter issues following a minor version update, it may be due to a change in how NDMF resolves execution order when constraints are insufficient.

  11. Create a simple non-destructive plugin

    main

    To create a basic plugin in the NDM Framework, define a class that inherits from Plugin<T> (where T is your plugin class) and decorate your assembly with the [assembly: ExportsPlugin(typeof(YourPlugin))] attribute.

    Inside the Configure() method, use InPhase(BuildPhase.PhaseName).Run(description, callback) to register tasks. This allows you to hook into specific stages of the build process, such as BuildPhase.Transforming.

    [assembly: ExportsPlugin(typeof(MyPlugin))]
    
    public class MyPlugin : Plugin<MyPlugin>
    {
        public override string DisplayName => "Baby's first plugin";
    
        protected override void Configure()
        {
            InPhase(BuildPhase.Transforming).Run("Do the thing", ctx =>
            {
                Debug.Log("Hello world!");
            });
        }
    }
  12. Enable experimental features via NDMF_EXPERIMENTAL

    main

    Certain experimental features (such as Resonite backend support, platform selection UI in the build window, and the NDMF Avatar Root component) are only accessible if you define NDMF_EXPERIMENTAL as a script define in your Unity project settings.

    Warning: Experimental features are subject to change in future releases and do not follow semantic versioning (semver).

    # No specific command, but requires adding 'NDMF_EXPERIMENTAL' to Unity Project Settings > Player > Script Compilation > Scripting Define Symbols