NEOLearn

Plugins and neo.toml

How NEO plugins work: descriptors, kinds, phases, states and game features, the neo.toml [plugins] section, the Plugins panel and plugins.* commands.

Updated October 17, 2026

On this page

Everything optional in NEO is a plugin: the engine core, the platform layer, the gameplay kits (character, input, camera, audio), the editor's tools and each game itself. They all use one mechanism, and a project turns them on and off and configures them in its neo.toml.

Each game and editor adds a single host plugin, which:

  1. collects the registered plugins;
  2. applies the project's neo.toml;
  3. resolves dependencies;
  4. builds the plugins in a fixed order;
  5. switches game features on and off;
  6. exposes everything as plugins.* commands, which the Studio's Plugins panel, the command line and AI agents all use.

The model#

ConceptWhat it is
DescriptorA [package.metadata.neo.plugin] table in the plugin crate's Cargo.toml: id, display name, description, category, kind, phase, dependencies (with version ranges), optional dependencies, platforms, enabled_by_default, game_feature, initial_state and command namespaces. The version is the crate's. The descriptor is checked when the code compiles, so a typo is a compile error with a "did you mean".
RegistrationOne register_neo_plugin! line next to the plugin. Plugins are linked into the game, not loaded from separate files.
KindWho may load it (below).
PhaseWhen it is built: platform, then core, default, post_default and editor. Within a phase, dependencies come first, then plugins in alphabetical order of id, so the order is the same on every run.
Project fileneo.toml at the project root: [plugins] and [plugin.<id>] sections (below).
Game featureA plugin that can be switched on and off while the game runs.
SettingsA typed settings struct per plugin, loaded from [plugin.<id>], validated, editable while running and saved back.
HealthEach plugin reports ok, degraded or failed, with a reason.

Kinds#

KindWhere it runs
runtimeEverywhere: it ships with the game.
dev_toolIn the editor and development builds; stripped from shipping builds.
editorIn the editor only. A game that links one fails to compile.
programHeadless tools.

A runtime plugin may only depend on runtime plugins, and a dev_tool plugin only on runtime or dev_tool ones, so nothing that ships can pull editor code in.

States#

Text
disabled   by neo.toml, by default, or by platform; never built, costs nothing
failed     a missing, mismatched or circular dependency, or its build failed

registered --load--> loaded --activate--> active      (game features)
                        ^                    |
                        +----deactivate------+

active                                                (every other plugin that was built)

How dependencies resolve#

  • A missing dependency fails the plugin, and every plugin that depends on it. Each message names the dependency, with a "did you mean".
  • A version mismatch (requiring neo_core@^0.2 when the build has 0.1.0) fails the plugin.
  • A cycle fails every plugin in it, and the message prints the path (a -> b -> c -> a).
  • A dependency that is disabled by default is turned on for the plugin that needs it, and reported as enabled by that dependency. A dependency the project explicitly disabled is an error naming both plugins.
  • A plugin can't depend on one in a later phase.
  • A plugin that is always on can't require a game feature, because the feature may be inactive. Use an optional dependency instead; optional dependencies only affect the build order.
  • Games stop on any error and print the full report. The editor keeps running: errors show in the Plugins panel, and a plugin whose build fails takes down only itself and its dependents.

Game features#

A game feature is a plugin you can switch on and off at runtime without restarting:

  • While it isn't active, its systems don't run and its commands refuse to run.
  • Activating it first activates the game features it requires, then runs its activation hook.
  • Deactivating it first deactivates the active features that require it, runs its deactivation hook, then removes every object the feature owns.
  • A feature in the registered state costs nothing until it is first activated; its code is then loaded at the end of that frame.

Startup code of a feature only runs if the feature is active when the game starts. Put setup that should happen on every activation in the activation hook instead.

neo.toml#

A project's neo.toml holds the plugin choices and settings. The game compiles it in and reads it as is; the Studio edits it in place and keeps your comments.

TOML
[plugins]
# the gameplay kits
neo_character = true
neo_input_kit = true
neo_gameplay_camera = true
neo_audio_kit = true

[plugin.dash]
speed = 6.0
SectionWhat it does
[plugins]<id> = true or false turns a plugin on or off. Plugins not listed follow their descriptor's enabled_by_default.
[plugin.<id>]That plugin's settings. Only values that differ from the defaults need to be written.

The same file also holds the game's [script] manifest; see The [script] manifest. Turning a plugin on or off takes effect the next time the game or editor starts. Settings changed from the Plugins panel or plugins.settings.set apply right away.

The Plugins panel#

In the Studio, open Project ▾ ▸ Plugins…. The panel lists every plugin in the build:

  • search by name, id, description or category, a category list, and a filter: All plugins, Running, Game features, Problems and Disabled;
  • each plugin as a card with its name, version, kind, phase, description, state, health, build time and dependencies;
  • Enable and Disable for the next start, written to neo.toml. Disabling lists the plugins that would fail without it and asks first. A banner says when a restart is needed, with a Restart NEO button;
  • Activate and Deactivate for game features, live;
  • a details pane with what the plugin added (commands, systems, resources), its issues and its Settings as editable fields. Invalid values show their error; valid ones apply live and are saved to neo.toml.

Commands#

CommandArgumentsReturns
plugins.listkind?, state?Each plugin's id, name, version, kind, phase, state and reason, health, build time and whether it has settings.
plugins.infoidThe descriptor, why it is enabled, its resolved dependencies, dependents, load position, the systems, resources and commands it added, issues, settings and build time.
plugins.graphformat? (json or dot)The load order, nodes, edges, errors and warnings, or a Graphviz graph.
plugins.activateidThe changes made (dependencies first) and the new state.
plugins.deactivateidThe changes made (dependents first, owned objects removed).
plugins.enableid, enabledWrites [plugins]. Returns restart_required and would_fail, the plugins that need it.
plugins.settings.getidThe settings' type, fields, current values and defaults.
plugins.settings.setid, values, persist?Validates and applies new values, and writes the ones that differ from the defaults to neo.toml.

A plugin's own commands are named <namespace>.<command>. The namespace is the plugin's id unless its descriptor lists other namespaces; the core, for example, owns play.*.

Writing a plugin#

Plugins are Rust crates. To make one:

  1. Add the descriptor to the crate's Cargo.toml:

    TOML
    [package.metadata.neo.plugin]
    display_name = "Dash"
    category = "Gameplay"
    kind = "runtime"                 # runtime | dev_tool | editor | program
    phase = "default"                # platform | core | default | post_default | editor
    dependencies = ["neo_core@^0.1"]
    game_feature = true              # optional: switchable at runtime
    initial_state = "loaded"         # registered | loaded | active
  2. Write the plugin and register it:

    Rust
    use bevy::prelude::*;
    use neo_plugin::{FeatureOwned, NeoPlugin, PluginAppExt, PluginSettings};
    use serde::{Deserialize, Serialize};
    
    #[derive(Resource, Reflect, Clone, Serialize, Deserialize)]
    #[serde(default)]
    pub struct DashSettings { pub speed: f32 }
    impl Default for DashSettings { fn default() -> Self { Self { speed: 6.0 } } }
    impl PluginSettings for DashSettings {
        fn validate(&self) -> Result<(), String> {
            if self.speed > 0.0 { Ok(()) } else { Err("speed must be > 0".into()) }
        }
    }
    
    #[derive(Default)]
    pub struct DashPlugin;
    impl NeoPlugin for DashPlugin {
        fn build(&self, app: &mut App) {
            app.register_plugin_settings::<DashSettings>()        // [plugin.dash] in neo.toml
                .register_plugin_command("boost", "Dash now", serde_json::json!({}), |_w, _| Ok(serde_json::Value::Null))
                .add_systems(Update, dash);                       // only runs while active
        }
        fn on_activate(&self, world: &mut World) {
            world.spawn((Name::new("dash trail"), FeatureOwned("dash")));
        }
    }
    fn dash(settings: Res<DashSettings>) { /* ... */ }
    
    neo_plugin::register_neo_plugin!(plugin = DashPlugin);

    An existing Bevy plugin can be adopted as is with register_neo_plugin!(bevy_plugin = T).

  3. Add the crate as a dependency of your game crate. Nothing else needs registering.

Objects spawned with FeatureOwned("<id>") are removed when the feature is deactivated.

Next steps#