diff --git a/examples/worlds/camera_sensor.sdf b/examples/worlds/camera_sensor.sdf index 3d967d701e..0cee711b47 100644 --- a/examples/worlds/camera_sensor.sdf +++ b/examples/worlds/camera_sensor.sdf @@ -8,23 +8,11 @@ 0.001 1.0 - - ogre2 - - - - 1.0 1.0 1.0 @@ -32,103 +20,22 @@ true + + true + - 3D View false docked - ogre2 scene 0.4 0.4 0.4 0.8 0.8 0.8 - -6 0 6 0 0.5 0 - - - - - - floating - 5 - 5 - false - - - - - false - 5 - 5 - floating - false - - - - - false - 5 - 5 - floating - false - - - - - false - 5 - 5 - floating - false - - - - - - World control - false - false - 72 - 1 - - floating - - - - - - - true - true - true - true - - - - - - - World stats - false - false - 110 - 290 - 1 - - floating - - - - - - - true - true - true - true + -3 0 3 0 0.5 0 @@ -137,20 +44,6 @@ camera - - - - - docked - - - - - - - docked - - diff --git a/examples/worlds/contact_sensor.sdf b/examples/worlds/contact_sensor.sdf index c37f7f259b..0714c34bf7 100644 --- a/examples/worlds/contact_sensor.sdf +++ b/examples/worlds/contact_sensor.sdf @@ -9,22 +9,7 @@ Run the following to print out contacts, --> - - - - - - - - + false diff --git a/examples/worlds/mimic_fast_slow_pendulums_world.sdf b/examples/worlds/mimic_fast_slow_pendulums_world.sdf index b42c8bc73a..7386bd3dac 100644 --- a/examples/worlds/mimic_fast_slow_pendulums_world.sdf +++ b/examples/worlds/mimic_fast_slow_pendulums_world.sdf @@ -16,6 +16,10 @@ 1 + + gz-physics-bullet-featherstone-plugin + + true 0 0 10 0 0 0 diff --git a/examples/worlds/quadcopter.sdf b/examples/worlds/quadcopter.sdf index 4b00c86c15..1ec3eaa18e 100644 --- a/examples/worlds/quadcopter.sdf +++ b/examples/worlds/quadcopter.sdf @@ -17,18 +17,6 @@ 0.001 1.0 - - - - - - diff --git a/include/gz/sim/Constants.hh b/include/gz/sim/Constants.hh new file mode 100644 index 0000000000..96d281ccc7 --- /dev/null +++ b/include/gz/sim/Constants.hh @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +#ifndef GZ_SIM_CONSTANTS_HH_ +#define GZ_SIM_CONSTANTS_HH_ + +#include "gz/sim/config.hh" +#include + +namespace gz::sim +{ +// Inline bracket to help doxygen filtering. +inline namespace GZ_SIM_VERSION_NAMESPACE +{ + constexpr std::string_view kPoliciesTag{"gz:policies"}; +} +} // namespace gz::sim + +#endif diff --git a/src/ServerConfig.cc b/src/ServerConfig.cc index b800c541aa..4e75a68ea0 100644 --- a/src/ServerConfig.cc +++ b/src/ServerConfig.cc @@ -938,7 +938,7 @@ sim::loadPluginInfo(bool _isPlayback) gzwarn << kServerConfigPathEnv << " set but no plugins found\n"; } - gzdbg << "Loaded (" << ret.size() << ") plugins from file " << + gzdbg << "Loading (" << ret.size() << ") plugins from file " << "[" << envConfig << "]\n"; return ret; @@ -1018,7 +1018,7 @@ sim::loadPluginInfo(bool _isPlayback) << "], but no plugins found\n"; } - gzdbg << "Loaded (" << ret.size() << ") plugins from file " << + gzdbg << "Loading (" << ret.size() << ") plugins from file " << "[" << defaultConfig << "]\n"; return ret; diff --git a/src/Server_TEST.cc b/src/Server_TEST.cc index f2248cb121..5f7697eeab 100644 --- a/src/Server_TEST.cc +++ b/src/Server_TEST.cc @@ -442,8 +442,7 @@ TEST_P(ServerFixture, GZ_UTILS_TEST_DISABLED_ON_WIN32(ServerConfigLogRecord)) EXPECT_EQ(0u, *server.IterationCount()); EXPECT_EQ(3u, *server.EntityCount()); - // Only the log record system is needed and therefore loaded. - EXPECT_EQ(1u, *server.SystemCount()); + EXPECT_EQ(4u, *server.SystemCount()); EXPECT_TRUE(serverConfig.LogRecordTopics().empty()); serverConfig.AddLogRecordTopic("test_topic1"); @@ -483,8 +482,7 @@ TEST_P(ServerFixture, EXPECT_EQ(0u, *server.IterationCount()); EXPECT_EQ(3u, *server.EntityCount()); - // Only the log record system is needed and therefore loaded. - EXPECT_EQ(1u, *server.SystemCount()); + EXPECT_EQ(4u, *server.SystemCount()); } EXPECT_FALSE(common::exists(logFile)); diff --git a/src/SimulationRunner.cc b/src/SimulationRunner.cc index ceea22ad49..268e956cda 100644 --- a/src/SimulationRunner.cc +++ b/src/SimulationRunner.cc @@ -18,6 +18,7 @@ #include "SimulationRunner.hh" #include +#include #ifdef HAVE_PYBIND11 #include #endif @@ -36,6 +37,7 @@ #include #include "gz/common/Profiler.hh" +#include "gz/sim/Constants.hh" #include "gz/sim/components/Model.hh" #include "gz/sim/components/Name.hh" #include "gz/sim/components/Sensor.hh" @@ -50,6 +52,7 @@ #include "gz/sim/components/RenderEngineServerHeadless.hh" #include "gz/sim/components/RenderEngineServerPlugin.hh" #include "gz/sim/Events.hh" +#include "gz/sim/ServerConfig.hh" #include "gz/sim/SdfEntityCreator.hh" #include "gz/sim/Util.hh" #include "gz/transport/TopicUtils.hh" @@ -263,6 +266,18 @@ SimulationRunner::SimulationRunner(const sdf::World &_world, if (_world.Gui()) { this->guiMsg = convert(*_world.Gui()); + + auto worldElem = this->sdfWorld.Element(); + if (worldElem) + { + auto policies = worldElem->FindElement("gz:policies"); + if (policies) + { + auto headerData = this->guiMsg.mutable_header()->add_data(); + headerData->set_key("gz:policies"); + headerData->add_value(policies->ToString("")); + } + } } std::string infoService{"gui/info"}; @@ -1586,21 +1601,60 @@ void SimulationRunner::CreateEntities(const sdf::World &_world) // Load any additional plugins from the Server Configuration this->LoadServerPlugins(this->serverConfig.Plugins()); + auto loadedWorldPlugins = this->systemMgr->TotalByEntity(worldEntity); // If we have reached this point and no world systems have been loaded, then // load a default set of systems. - if (this->systemMgr->TotalByEntity(worldEntity).empty()) + + auto worldElem = this->sdfWorld.Element(); + bool includeServerConfigPlugins = true; + if (worldElem) { - gzmsg << "No systems loaded from SDF, loading defaults" << std::endl; - bool isPlayback = !this->serverConfig.LogPlaybackPath().empty(); - auto plugins = gz::sim::loadPluginInfo(isPlayback); - this->LoadServerPlugins(plugins); + auto policies = worldElem->FindElement(std::string(kPoliciesTag)); + if (policies) + { + includeServerConfigPlugins = + policies + ->Get("include_server_config_plugins", includeServerConfigPlugins) + .first; + } } + if (includeServerConfigPlugins || loadedWorldPlugins.empty()) + { + bool isPlayback = !this->serverConfig.LogPlaybackPath().empty(); + auto defaultPlugins = gz::sim::loadPluginInfo(isPlayback); + if (loadedWorldPlugins.empty()) + { + gzmsg << "No systems loaded from SDF, loading defaults" << std::endl; + } + else + { + std::unordered_set loadedWorldPluginFileNames; + for (const auto &pl : loadedWorldPlugins) + { + loadedWorldPluginFileNames.insert(pl.fname); + } + auto isPluginLoaded = + [&loadedWorldPluginFileNames](const ServerConfig::PluginInfo &_pl) + { + return loadedWorldPluginFileNames.count(_pl.Plugin().Filename()) != 0; + }; + + // Remove plugin if it's already loaded so as to not duplicate world + // plugins. + defaultPlugins.remove_if(isPluginLoaded); + + gzdbg << "Additional plugins to load:\n"; + for (const auto &plugin : defaultPlugins) + { + gzdbg << plugin.Plugin().Name() << " " << plugin.Plugin().Filename() + << "\n"; + } + } + + this->LoadServerPlugins(defaultPlugins); + }; + // Store the initial state of the ECM; this->initialEntityCompMgr.CopyFrom(this->entityCompMgr); - - // Publish empty GUI messages for worlds that have no GUI in the beginning. - // In the future, support modifying GUI from the server at runtime. - if (_world.Gui()) - this->guiMsg = convert(*_world.Gui()); } diff --git a/src/gui/Gui.cc b/src/gui/Gui.cc index 035d082299..2d6f4549f1 100644 --- a/src/gui/Gui.cc +++ b/src/gui/Gui.cc @@ -18,6 +18,7 @@ #include #include #include +#include #include @@ -29,7 +30,10 @@ #include #include #include +#include +#include +#include "gz/sim/Constants.hh" #include "gz/sim/InstallationDirectories.hh" #include "gz/sim/Util.hh" #include "gz/sim/config.hh" @@ -47,6 +51,119 @@ namespace sim { // Inline bracket to help doxygen filtering. inline namespace GZ_SIM_VERSION_NAMESPACE { +namespace { + +std::unique_ptr parseDefaultPlugins( + const std::string &_defaultConfig) +{ + const auto resolvedDefaultConfigPath = + gz::gui::App()->ResolveConfigFile(_defaultConfig); + auto pluginsDoc = std::make_unique(); + if (pluginsDoc->LoadFile(resolvedDefaultConfigPath.c_str()) == + tinyxml2::XML_SUCCESS) + { + // Remove everything that's not a plugin + for (auto elem = pluginsDoc->FirstChildElement(); elem != nullptr;) + { + if (std::strcmp("plugin", elem->Value()) != 0) + { + auto tmp = elem; + elem = elem->NextSiblingElement(); + pluginsDoc->DeleteChild(tmp); + } + else + { + elem = elem->NextSiblingElement(); + } + } + } + + return pluginsDoc; +} + +auto combineUserAndDefaultPlugins( + std::unique_ptr _userPlugins, + const tinyxml2::XMLDocument &_defaultPlugins, bool _includeDefaultPlugins) +{ + if (_includeDefaultPlugins) + { + auto combinedPlugins = std::make_unique(); + _defaultPlugins.DeepCopy(combinedPlugins.get()); + + std::set processedUserPlugins; + for (auto pluginElem = _userPlugins->FirstChildElement("plugin"); + pluginElem != nullptr; + pluginElem = pluginElem->NextSiblingElement("plugin")) + { + const char *pluginFilename = pluginElem->Attribute("filename"); + + bool replacedPlugin{false}; + for (auto elem = combinedPlugins->FirstChildElement("plugin"); + elem != nullptr && processedUserPlugins.count(elem) == 0; + elem = elem->NextSiblingElement("plugin")) + { + if (elem->Attribute("filename", pluginFilename)) + { + auto tmp = elem; + // Insert the replacement + auto clonedPlugin = pluginElem->DeepClone(combinedPlugins.get()); + elem = combinedPlugins->InsertAfterChild(elem, clonedPlugin) + ->ToElement(); + // Remove the original + combinedPlugins->DeleteNode(tmp); + replacedPlugin = true; + } + } + if (!replacedPlugin) + { + auto clonedPlugin = pluginElem->DeepClone(combinedPlugins.get()); + auto insertedElem = combinedPlugins->InsertEndChild(clonedPlugin); + processedUserPlugins.insert(insertedElem ); + } + } + + return combinedPlugins; + } + return _userPlugins; +} + +/// \brief Various policies that affect the behavior of the GUI +struct GuiPolicies +{ + /// \brief Whether to include default plugins + bool includeGuiDefaultPlugins{true}; + + /// \brief Parse policies from a GUI message + /// \param[in] _msg Input message + /// \return A GuiPolicies object populated from parsing the message. + static GuiPolicies ParsePolicies(const msgs::GUI &_msg) + { + GuiPolicies policies; + for (const auto &data : _msg.header().data()) + { + if (data.key() == "gz:policies") + { + tinyxml2::XMLDocument doc; + if (data.value_size() > 0) + { + if (doc.Parse(data.value(0).c_str()) == tinyxml2::XML_SUCCESS) + { + tinyxml2::XMLHandle handle(doc); + auto elem = handle.FirstChildElement(kPoliciesTag.data()) + .FirstChildElement("include_gui_default_plugins") + .ToElement(); + if (elem) + { + elem->QueryBoolText(&policies.includeGuiDefaultPlugins); + } + } + } + } + } + return policies; + } +}; +} namespace gui { /// \brief Get the path to the default config file. If the file doesn't exist @@ -434,6 +551,9 @@ std::unique_ptr createGui( // Load plugins after creating GuiRunner, so they can access worldName if (_loadPluginsFromSdf) { + const auto guiPolicies = GuiPolicies::ParsePolicies(res); + auto userPlugins = std::make_unique(); + std::string pluginsXml = ""; for (int p = 0; p < res.plugin_size(); ++p) { const auto &plugin = res.plugin(p); @@ -444,13 +564,13 @@ std::unique_ptr createGui( if (fileName == "GzScene3D") { std::vector extras{"GzSceneManager", - "InteractiveViewControl", - "CameraTracking", - "MarkerManager", - "SelectEntities", - "EntityContextMenuPlugin", - "Spawn", - "VisualizationCapabilities"}; + "InteractiveViewControl", + "CameraTracking", + "MarkerManager", + "SelectEntities", + "EntityContextMenuPlugin", + "Spawn", + "VisualizationCapabilities"}; std::string msg{ "The [GzScene3D] GUI plugin has been removed since Garden.\n" @@ -460,7 +580,7 @@ std::unique_ptr createGui( for (auto extra : extras) { - msg += "* " + extra + "\n"; + msg += "* " + extra + "\n"; auto newPlugin = res.add_plugin(); newPlugin->set_filename(extra); @@ -478,15 +598,23 @@ std::unique_ptr createGui( fileName = "MinimalScene"; } + pluginsXml += "" + + plugin.innerxml() + "\n"; + } + userPlugins->Parse(pluginsXml.c_str()); - std::string pluginStr = "" + - plugin.innerxml() + ""; - - tinyxml2::XMLDocument pluginDoc; - pluginDoc.Parse(pluginStr.c_str()); + const auto defaultPlugins = parseDefaultPlugins(defaultConfig); + auto pluginsToLoad = combineUserAndDefaultPlugins( + std::move(userPlugins), *defaultPlugins, + guiPolicies.includeGuiDefaultPlugins); - app->LoadPlugin(fileName, - pluginDoc.FirstChildElement("plugin")); + gzdbg << "Loading plugins:\n"; + for (auto pluginElem = pluginsToLoad->FirstChildElement("plugin"); + pluginElem != nullptr; + pluginElem = pluginElem->NextSiblingElement("plugin")) + { + app->LoadPlugin(pluginElem->Attribute("filename"), pluginElem); + gzdbg << pluginElem->Attribute("filename") << "\n"; } } } diff --git a/test/integration/reset_sensors.cc b/test/integration/reset_sensors.cc index 7b9c43d492..be9c3b6a38 100644 --- a/test/integration/reset_sensors.cc +++ b/test/integration/reset_sensors.cc @@ -23,6 +23,7 @@ #include #include +#include #include #include @@ -336,6 +337,7 @@ TEST_F(ResetFixture, GZ_UTILS_TEST_DISABLED_ON_MAC(HandleReset)) imageReceiver.msgReceived = false; server.Run(true, 2000 - server.IterationCount().value(), false); + std::this_thread::sleep_for(20ms); // Check iterator state EXPECT_EQ(2000u, server.IterationCount().value()); diff --git a/test/test_config.hh.in b/test/test_config.hh.in index b367ed89de..89118966c9 100644 --- a/test/test_config.hh.in +++ b/test/test_config.hh.in @@ -48,6 +48,9 @@ struct TestWorldSansPhysics static std::string world = std::string("" "" "" + "" + " false" + "" "" diff --git a/test/worlds/air_pressure.sdf b/test/worlds/air_pressure.sdf index 0679f9e280..d66cb0fccd 100644 --- a/test/worlds/air_pressure.sdf +++ b/test/worlds/air_pressure.sdf @@ -5,6 +5,9 @@ 0 + + false + diff --git a/test/worlds/air_speed.sdf b/test/worlds/air_speed.sdf index feb3347655..b23d7ee6d5 100644 --- a/test/worlds/air_speed.sdf +++ b/test/worlds/air_speed.sdf @@ -5,6 +5,9 @@ 0 + + false + diff --git a/test/worlds/reset_sensors.sdf b/test/worlds/reset_sensors.sdf index 733658fefc..7f8db14a7e 100644 --- a/test/worlds/reset_sensors.sdf +++ b/test/worlds/reset_sensors.sdf @@ -1,6 +1,9 @@ + + false + diff --git a/tutorials/gui_config.md b/tutorials/gui_config.md index 9cc42fa1e0..0304f1131a 100644 --- a/tutorials/gui_config.md +++ b/tutorials/gui_config.md @@ -16,10 +16,33 @@ There are a few places where the GUI configuration can come from: 3. The default configuration file at `$HOME/.gz/sim/<#>/gui.config` \*, where `<#>` is Gazebo Sim's major version. -Each of the items above takes precedence over the ones below it. For example, -if a user chooses a `--gui-config`, the SDF's `` element is ignored. And -the default configuration file is only loaded if no configuration is passed -through the command line or the SDF file. +If a configuration file is specified using `--gui-config`, Gazebo will +ignore both the `` element inside the SDF file and the default +configuration file. Otherwise, Gazebo will load plugins by combining +plugins in the `` element and the default configuration file. +How Gazebo combines these plugins is determined by the +`` policy set in ``: + +- `true`: Plugins + from the default configuration file merged with plugins from the SDF file. + Plugins from SDF files take precedence over plugins from + the default configuration file. This means, if a plugin is specified in + both places, by default, only the one specified in the SDF file will be + loaded. If replacement occurs, the replacement + plugin will take the position of the replaced plugin in the order of plugins. + If replacement does not occur, the plugin is appended to the end of the list. + + The main use case for this policy is for users to rely on + the default list of plugins and only add extra plugins they need for the + application. This policy is also useful for overriding the parameters of a small + subset of the default plugins. This is the default setting in + Gazebo Ionic and later. + +- `false`: If + there are any plugins specified in the SDF file, plugins from the default + configuration file are ignored. This allows the user to have complete + control over which plugins are loaded. This is the default setting in + Gazebo Harmonic and earlier. > \* For log-playback, the default file is > `$HOME/.gz/sim/<#>/playback_gui.config` @@ -28,10 +51,9 @@ through the command line or the SDF file. ### Default configuration -Let's try this in practice. First, let's open Gazebo without passing -any arguments: +Let's try this in practice. First, let's open the default Gazebo world: -`gz sim` +`gz sim default.sdf` You should see an empty world with several plugins loaded by default, such as the 3D Scene, the play/pause button, etc. @@ -57,7 +79,7 @@ Let's try customizing it: 3. Reload Gazebo: - `gz sim` + `gz sim default.sdf` Note how the UI is now in dark mode! @@ -75,7 +97,7 @@ will be created with default values: Let's try overriding the default configuration from an SDF file. Open your favorite editor and save this file as `fuel_preview.sdf`: -``` +```xml @@ -84,6 +106,10 @@ favorite editor and save this file as `fuel_preview.sdf`: name="gz::sim::systems::SceneBroadcaster"> + + false + + @@ -137,7 +163,7 @@ Now let's load this world: `gz sim /fuel_preview.sdf` -Notice how the application has only one GUI plugin loaded, the 3D scene, as defined +Notice how the application has only 3 GUI plugins loaded, as defined on the SDF file above. @image html files/gui_config/fuel_preview.png @@ -147,6 +173,75 @@ the same model loaded into the default GUI layout. @image html files/gui_config/fuel_preview_no_gui.png +Now, let's change the policy so that default plugins are included + +```xml + + + + + + + + true + + + + + + + 3D View + false + docked + + + ogre2 + scene + 1.0 1.0 1.0 + 0.4 0.6 1.0 + 8.3 7 7.8 0 0.5 -2.4 + + + + false + 5 + 5 + floating + false + + + + + false + 5 + 5 + floating + false + + + + + + + https://fuel.gazebosim.org/1.0/OpenRobotics/models/Sun + + + + https://fuel.gazebosim.org/1.0/OpenRobotics/models/Gazebo + + + + +``` + +You will now see the same model loaded in the default GUI layout +similar to when you deleted the `` element altogether. Note that this will also +be the behavior if we removed `` tag. + +@image html files/gui_config/fuel_preview_no_gui.png + ### Command line It's often inconvenient to embed your GUI layout directly into every SDF file. diff --git a/tutorials/server_config.md b/tutorials/server_config.md index bbab5f5110..650eafe0c4 100644 --- a/tutorials/server_config.md +++ b/tutorials/server_config.md @@ -17,11 +17,26 @@ There are a few places where the plugins can be defined: 3. The default configuration file at `$HOME/.gz/sim/<#>/server.config` \*, where `<#>` is Gazebo Sim's major version. -Each of the items above takes precedence over the ones below it. For example, -if a the SDF file has any `` elements, then the -`GZ_SIM_SERVER_CONFIG_PATH` variable is ignored. And the default configuration -file is only loaded if no plugins are passed through the SDF file or the -environment variable. +The behavior of Gazebo when loading these plugins depends on the +`` policy set in ``: + +- `true`: Plugins + in the SDF file are first loaded, followed by plugins from config files + (either `GZ_SIM_SERVER_CONFIG_PATH` or the default configuration file). + Plugins from SDF files take precedence over plugins from config files, this + means, if a plugin is specified in both places, only the one specified in the + SDF file will be loaded. The main use case for this is for users to rely on + the default list of plugins and only add extra plugins they need for the + application. This is the default setting in Gazebo Ionic and later. + +- `false`: If + there are any plugins specified in the SDF file, plugins from the config files + (either `GZ_SIM_SERVER_CONFIG_PATH` or the default configuration file) are + ignored. This allows the user to have complete control over which plugins are + loaded. This is the default setting in Gazebo Harmonic and earlier. + +In both policy settings, the default configuration file is only loaded if no +plugins are passed through the `GZ_SIM_SERVER_CONFIG_PATH` environment variable. > \* For log-playback, the default file is > `$HOME/.gz/sim/<#>/playback_server.config` @@ -85,7 +100,7 @@ will be created with default values: Let's try overriding the default configuration from an SDF file. Open your favorite editor and save this file as `fuel_preview.sdf`: -``` +```xml @@ -147,8 +162,84 @@ Now let's load this world: `gz sim -r /fuel_preview.sdf` -Notice how the application has only one system plugin loaded, the scene -broadcaster, as defined on the SDF file above. Physics is not loaded, so even +Notice how the application has loaded the scene +broadcaster, as defined on the SDF file above as well as the default plugins +`Physics` and `UserCommands`. Since `SceneBroadcaster` is loaded from the SDF file, +it's not loaded again. We see that the cone falls due to gravity since all the +necessary plugins are loaded. + +@image html files/server_config/from_sdf_no_plugins.gif + +Now, let's modify the SDF file to change the policy `false` + +```xml + + + + + + false + + + + + + + + + + + 3D View + false + docked + + + ogre2 + scene + 1.0 1.0 1.0 + 0.4 0.6 1.0 + 8.3 7 7.8 0 0.5 -2.4 + + + + false + 5 + 5 + floating + false + + + + + false + 5 + 5 + floating + false + + + + + + + https://fuel.gazebosim.org/1.0/OpenRobotics/models/Sun + + + + https://fuel.gazebosim.org/1.0/OpenRobotics/models/Construction Cone + + + + +``` +Let's load this world again: + +`gz sim -r /fuel_preview.sdf` + +Notice how the application has only one system plugin loaded, the `SceneBroadcaster`, +as defined on the SDF file above. `Physics` is not loaded, so even though the simulation is running (started with `-r`), the cone doesn't fall with gravity. @@ -299,3 +390,8 @@ the background color is the default grey, instead of the blue color set on the GUI `GzScene` plugin. @image html files/server_config/camera_env.gif + + +### Order of Execution of Plugins +The order of execution of plugins can be controlled by setting +the `` tag inside ``. See example in examples/plugin/priority_printer_plugin and the associated README.md file to learn more.