diff --git a/python/mujoco/codegen/generate_spec_bindings.py b/python/mujoco/codegen/generate_spec_bindings.py
index ef4aedf4..50b40d1b 100644
--- a/python/mujoco/codegen/generate_spec_bindings.py
+++ b/python/mujoco/codegen/generate_spec_bindings.py
@@ -39,8 +39,10 @@ def _value_binding_code(
fullvarname = 'ptr->' + varname
if field.name.startswith('mjs'): # all other mjs are raw structs
fulltype = field.name.replace('mjs', 'raw::Mjs')
- if field.name != 'mjsPlugin' and field.name != 'mjsOrientation':
- fulltype = fulltype + '*' # plugin and orientation are pointers
+ if field.name == 'mjsPlugin' or field.name == 'mjsOrientation':
+ fulltype = fulltype + '&' # plugin and orientation are not pointers
+ else:
+ fulltype = fulltype + '*'
def_property_args = (
f'"{varname}"',
diff --git a/python/mujoco/specs.cc b/python/mujoco/specs.cc
index c280911e..0f4ab874 100644
--- a/python/mujoco/specs.cc
+++ b/python/mujoco/specs.cc
@@ -1017,9 +1017,12 @@ PYBIND11_MODULE(_specs, m) {
mjsTuple.def("delete", [](raw::MjsTuple& self) { mjs_delete(self.element); });
// ============================= MJSPLUGIN ===================================
- mjsPlugin.def_property_readonly("id", [](raw::MjsPlugin& self) -> int {
- return mjs_getId(self.instance);
- });
+ mjsPlugin.def_property(
+ "id",
+ [](raw::MjsPlugin& self) -> int { return mjs_getId(self.instance); },
+ [](raw::MjsPlugin& self, raw::MjsPlugin* other) {
+ self.instance = other->instance;
+ });
mjsPlugin.def("delete",
[](raw::MjsPlugin& self) { mjs_delete(self.instance); });
diff --git a/python/mujoco/specs_test.py b/python/mujoco/specs_test.py
index ad068f91..a860f443 100644
--- a/python/mujoco/specs_test.py
+++ b/python/mujoco/specs_test.py
@@ -322,5 +322,35 @@ class SpecsTest(absltest.TestCase):
self.assertEqual(model.nsite, 10)
self.assertEqual(model.nsensor, 9)
+ def test_plugin(self):
+ xml = """
+
+
+
+
+
+ """
+
+ spec = mujoco.MjSpec()
+ spec.from_string(xml)
+ self.assertIsNotNone(spec.worldbody)
+
+ body = spec.worldbody.add_body()
+ body.plugin.name = 'mujoco.elasticity.cable'
+ body.plugin.id = spec.add_plugin()
+ body.plugin.active = True
+ self.assertEqual(body.plugin.id, 0)
+
+ geom = body.add_geom()
+ geom.type = mujoco.mjtGeom.mjGEOM_BOX
+ geom.size[0] = 1
+ geom.size[1] = 1
+ geom.size[2] = 1
+
+ model = spec.compile()
+ self.assertIsNotNone(model)
+ self.assertEqual(model.nplugin, 1)
+ self.assertEqual(model.body_plugin[1], 0)
+
if __name__ == '__main__':
absltest.main()
diff --git a/src/user/user_api.cc b/src/user/user_api.cc
index bf3a98c6..4743f83b 100644
--- a/src/user/user_api.cc
+++ b/src/user/user_api.cc
@@ -606,6 +606,9 @@ const char* mjs_resolveOrientation(double quat[4], mjtByte degree, const char* s
// get id
int mjs_getId(mjsElement* element) {
+ if (!element) {
+ return -1;
+ }
return static_cast(element)->id;
}
diff --git a/test/user/user_api_test.cc b/test/user/user_api_test.cc
index 5eb68356..346f179d 100644
--- a/test/user/user_api_test.cc
+++ b/test/user/user_api_test.cc
@@ -20,6 +20,7 @@
#include
#include
#include
+#include
#include
#include
@@ -104,6 +105,41 @@ TEST_F(MujocoTest, TreeTraversal) {
mj_deleteSpec(spec);
}
+TEST_F(PluginTest, ActivatePlugin) {
+ std::string plugin_name = "mujoco.elasticity.cable";
+ mjSpec* spec = mj_makeSpec();
+
+ // get slot of requested plugin
+ int plugin_slot = -1;
+ const mjpPlugin* plugin = mjp_getPlugin(plugin_name.c_str(), &plugin_slot);
+ EXPECT_THAT(plugin, NotNull());
+
+ // activated plugin in the slot
+ std::vector> active_plugins;
+ active_plugins.emplace_back(std::make_pair(plugin, plugin_slot));
+ mjs_setActivePlugins(spec, &active_plugins);
+
+ // associate plugin to body
+ mjsBody* body = mjs_addBody(mjs_findBody(spec, "world"), 0);
+ mjs_setString(body->plugin.name, plugin_name.c_str());
+ body->plugin.instance = mjs_addPlugin(spec)->instance;
+ body->plugin.active = true;
+ mjsGeom* geom = mjs_addGeom(body, 0);
+ geom->type = mjGEOM_BOX;
+ geom->size[0] = 1;
+ geom->size[1] = 1;
+ geom->size[2] = 1;
+
+ // compile and check that the plugin is present
+ mjModel* model = mj_compile(spec, NULL);
+ EXPECT_THAT(model, NotNull());
+ EXPECT_THAT(model->nplugin, 1);
+ EXPECT_THAT(model->body_plugin[1], 0);
+
+ mj_deleteSpec(spec);
+ mj_deleteModel(model);
+}
+
// ------------------- test recompilation multiple files -----------------------
TEST_F(PluginTest, RecompileCompare) {
mjtNum tol = 0;