Skip to content

Conversation

alice-i-cecile
Copy link
Member

@alice-i-cecile alice-i-cecile commented Sep 18, 2025

Objective

Adding duplicate plugins is generally a mistake in user code, resulting in overridden settings, duplicate systems, doubled-up entities and more.

As a result, we plugin authors can set the is_unique attribute on their plugin, preventing it from being added multiple times.

This was added in #6411, and by default this flag is true. This is a good default! However, in that PR, we chose to make this failure mode panic. This is frustrating to users because:

  1. Plugins are not always added directly by the user.
  2. We do not report where the duplicate plugin was added very well.
  3. The standard is_plugin_added workaround doesn't work for plugin groups.

This was encountered in #21111, but has been widely complained about in #18909 and #15802.

Ultimately, this is not a critical failure: we can simply log a warning and not insert the second copy.

Fixes #21111, closes #18909.

Solution

In the spirit of #2337 and the blessed #14275, downgrade this failure mode to a warning and do the right thing.

Note that I cannot use the standard logging tools here, since bevy_log may not be initialized yet due to #1255.

Note to reviewers

Would I prefer to have #69 instead? Yes! But this patch still moves us in the correct direction and alleviates a major user pain point.

Question for reviewers

This behavior is completely harmless in the case of non-configurable plugins, but fairly concerning in the case of configurable plugins. Would you prefer a silently ignored / panic split based on the size of the plugin type?

@alice-i-cecile alice-i-cecile added C-Usability A targeted quality-of-life change that makes Bevy easier to use A-App Bevy apps and plugins S-Needs-Design This issue requires design work to think about how it would best be accomplished X-Controversial There is active debate or serious implications around merging this PR labels Sep 18, 2025
@alice-i-cecile
Copy link
Member Author

This behavior is completely harmless in the case of non-configurable plugins, but fairly concerning in the case of configurable plugins. Would you prefer a silently ignored / panic split based on the size of the plugin type?

After chewing on this a bit while writing this PR description, this is my preferred solution. I'll implement this tomorrow unless someone convinces me not to.

@hukasu
Copy link
Contributor

hukasu commented Sep 18, 2025

how would this deal with plugins that can take values?

app.add_plugin(
  MyPlugin {
    flag: true,
  }
);
app.add_plugin(
  MyPlugin {
    flag: false,
  }
);

@alice-i-cecile
Copy link
Member Author

how would this deal with plugins that can take values?

app.add_plugin(
  MyPlugin {
    flag: true,
  }
);
app.add_plugin(
  MyPlugin {
    flag: false,
  }
);

Yeah, this is why I'm leaning towards a branch on whether or not the plugin is a ZST: panicking in those cases is quite reasonable!

@janhohenheim
Copy link
Member

allowing ZSTs and keeping the old behavior otherwise sounds completely reasonable to me :)

@mockersf
Copy link
Member

mockersf commented Sep 18, 2025

This behavior is completely harmless in the case of non-configurable plugins, but fairly concerning in the case of configurable plugins.

I don't think this is true. Systems are added twice, pipelines are created twice, ...

If we want to remove the panic, we should make the api return a result

@janhohenheim
Copy link
Member

janhohenheim commented Sep 18, 2025

Can't we just skip the duplicate ZST plugin? I.e. not run its lifecycle?

Edit: yeah, that's what this PR is doing, isn't it? Just scope that to ZSTs and nothing should be added / registered twice

clippy::allow_attributes,
reason = "This lint only triggers sometimes, based on the features enabled."
)]
#[allow(unused_variables, reason = "plugin_name is only used with std enabled")]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than all of this "pomp and circumstance", can't we just name the bound field _plugin_name?

Copy link
Member

@janhohenheim janhohenheim Sep 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or add a let _ = plugin_name; to the end of the scope :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or even better, learned from @janis-bhm: #[allow(clippy::allow_attributes, unused_variables, reason = "...")]

// However, both `dbg!` and `eprintln!` are only available with the `std` feature.
// This is not a critical error, so we've chosen to simply ignore it in `no_std` environments.
#[cfg(feature = "std")]
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think changing behavior based on the number of fields / size of the typee is confusing / hard to document / hard to understand. Additionally, nothing is stopping a ZST plugin from behaving differently at different times, or feeding on data that might change.

I think we either need to embrace "unique plugins registered more than once ignore the configuration from the second plugin and warns" or "double-registering unique plugins aborts app execution".

I do think the warning makes sense, as plugins shouldn't need to coordinate / anticipate what other plugins might also register. Of course, the true fix is deferred plugin init, which would embrace the "only one instance, with shared config" approach.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imo that's fine in this case, since it's not behavior you have to actively think about. It's just an instance of one less panic in a specialized case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Panic at startup is very harmless compared to some during runtime, it is not like you are gonna miss it during development, it is annoying if you have to go to a library maintainer to ask them not to add external plugins, I won't deny, but this is something that is best dealt with the required plugins logic than with what is being proposed here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why bother though? Asking the other plugin to not add a ZST plugin has exactly the same effect in practice as what is proposed here. Imo we have a sensible and safe way to automate ecosystem tedium upstream with a tiny PR.

@hukasu
Copy link
Contributor

hukasu commented Sep 19, 2025

this is primarily so that libraries can add the plugins that they require without the need for testing if it is added previously, correct?
i propose something like this

app.require_plugin::<RenderPlugin>();
app.on_build_plugin_hook::<RenderPlugin>(run_post_render_plugin_build, true);
app.on_finish_plugin_hook::<RenderPlugin>(run_post_render_plugin_finish, false);

where

fn require_plugin<T: Plugin>();
fn on_[build|finish]_plugin_hook<T: Plugin>(system: fn(&mut App), required: bool);

Marking a plugin as required will cause the app to fail due to it missing.
Creating hooks that have required = true will also fail if the plugin is missing. The on build hook will run after the build or finish methods, if the on build hook is added to a plugin that is already added, then run it immediatly

@hukasu
Copy link
Contributor

hukasu commented Sep 19, 2025

a visual example of what cart said

struct MyPlugin;
impl Plugin for MyPlugin {
  fn build(app: &mut App) {
    if app.is_plugin_added::<SomeOtherPlugin>() {
      <do something else>
    }
  }
}

fn main() {
  let mut app = App::new();
  app.add_plugins(MyPlugin);
  app.add_plugins(SomeOtherPlugin);
  app.add_plugins(MyPlugin);
}

the first time the MyPlugin is added the if is false, but the second time it would've been true

@janhohenheim
Copy link
Member

ZST clearly can have divergent behavior, but is that a meaningful distinction? Is that something that actually happens in the wild?

@hukasu
Copy link
Contributor

hukasu commented Sep 20, 2025

Is that something that actually happens in the wild?

It only needs to happen once for people to get really mad, because this divergent behavior would be a pain to debug

@alice-i-cecile alice-i-cecile added this to the 0.18 milestone Sep 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-App Bevy apps and plugins C-Usability A targeted quality-of-life change that makes Bevy easier to use S-Needs-Design This issue requires design work to think about how it would best be accomplished X-Controversial There is active debate or serious implications around merging this PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Make Feathers only add InputDispatchPlugin when it's not already added Add try_add_plugins for apps a la try_insert for entities
5 participants