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.
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:
- collects the registered plugins;
- applies the project's
neo.toml; - resolves dependencies;
- builds the plugins in a fixed order;
- switches game features on and off;
- exposes everything as
plugins.*commands, which the Studio's Plugins panel, the command line and AI agents all use.
The model#
| Concept | What it is |
|---|---|
| Descriptor | A [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". |
| Registration | One register_neo_plugin! line next to the plugin. Plugins are linked into the game, not loaded from separate files. |
| Kind | Who may load it (below). |
| Phase | When 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 file | neo.toml at the project root: [plugins] and [plugin.<id>] sections (below). |
| Game feature | A plugin that can be switched on and off while the game runs. |
| Settings | A typed settings struct per plugin, loaded from [plugin.<id>], validated, editable while running and saved back. |
| Health | Each plugin reports ok, degraded or failed, with a reason. |
Kinds#
| Kind | Where it runs |
|---|---|
runtime | Everywhere: it ships with the game. |
dev_tool | In the editor and development builds; stripped from shipping builds. |
editor | In the editor only. A game that links one fails to compile. |
program | Headless 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#
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.2when 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
registeredstate 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.
[plugins]
# the gameplay kits
neo_character = true
neo_input_kit = true
neo_gameplay_camera = true
neo_audio_kit = true
[plugin.dash]
speed = 6.0| Section | What 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#
| Command | Arguments | Returns |
|---|---|---|
plugins.list | kind?, state? | Each plugin's id, name, version, kind, phase, state and reason, health, build time and whether it has settings. |
plugins.info | id | The descriptor, why it is enabled, its resolved dependencies, dependents, load position, the systems, resources and commands it added, issues, settings and build time. |
plugins.graph | format? (json or dot) | The load order, nodes, edges, errors and warnings, or a Graphviz graph. |
plugins.activate | id | The changes made (dependencies first) and the new state. |
plugins.deactivate | id | The changes made (dependents first, owned objects removed). |
plugins.enable | id, enabled | Writes [plugins]. Returns restart_required and would_fail, the plugins that need it. |
plugins.settings.get | id | The settings' type, fields, current values and defaults. |
plugins.settings.set | id, 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:
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 | activeWrite the plugin and register it:
Rustuse 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).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.