diff --git a/Scripts/build_and_test.ps1 b/Scripts/build_and_test.ps1
index b986829..d2d139f 100644
--- a/Scripts/build_and_test.ps1
+++ b/Scripts/build_and_test.ps1
@@ -66,6 +66,23 @@ $cmd = Join-Path $Engine 'Engine\Binaries\Win64\UnrealEditor-Cmd.exe'
 if (-not (Test-Path $bat)) { Write-Error "Build.bat not found: $bat";          exit 3 }
 if (-not (Test-Path $cmd)) { Write-Error "UnrealEditor-Cmd not found: $cmd";   exit 3 }
 
+# --- Codegen drift gate ----------------------------------------------------
+# Refuse to build if codegen output drifted from the committed Source/, or
+# if any drift diagnostic (--strict) fires, or if the clang-AST introspect
+# snapshot is missing (--require-introspect). Skipped silently when Python
+# isn't on PATH — the gate is best-effort locally; CI enforces it strictly.
+$pluginRoot = Split-Path -Parent $PSScriptRoot
+$generator = Join-Path $pluginRoot 'Scripts/codegen/generate_ue_components.py'
+$py = (Get-Command python -ErrorAction SilentlyContinue)
+if ($py -and (Test-Path $generator)) {
+    Write-Host '>>> Codegen drift gate: python generate_ue_components.py --check --strict --require-introspect'
+    & $py.Source $generator --check --strict --require-introspect
+    if ($LASTEXITCODE -ne 0) {
+        Write-Error "Codegen drift detected (exit $LASTEXITCODE). Re-run 'python Scripts/codegen/generate_ue_components.py' to regenerate, then re-run this script."
+        exit 4
+    }
+}
+
 # Truncate the test log up-front so a build failure (or any early exit
 # before UnrealEditor-Cmd writes to it) doesn't leave the SHA-256 in the
 # summary pointing at a previous run's file.
diff --git a/Scripts/codegen/_codegen_checks.py b/Scripts/codegen/_codegen_checks.py
new file mode 100644
index 0000000..9dbf13b
--- /dev/null
+++ b/Scripts/codegen/_codegen_checks.py
@@ -0,0 +1,930 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""URLab codegen — drift checks.
+
+Every ``_check_*`` function that scans rules + snapshots for drift
+lives here. The orchestrator imports the dispatcher functions
+(``_emit_drift_diagnostics``, ``_check_*``) and wires them into
+``_phase_diagnostics``.
+
+Dependencies are strictly downward: imports from ``_codegen_core``
+only. No emit logic, no .cpp injection — drift checks read the
+rules + snapshots and route diagnostics through ``_diag_add``.
+"""
+
+from __future__ import annotations
+
+import os
+import re
+from typing import Any, Dict, List, Optional, Sequence, Set, Tuple
+
+from _codegen_core import (
+    _diag_add,
+    _UE_TYPE_INFO, _UE_TYPE_FAMILY, _ue_type_accepts_units_meta, _KNOWN_UE_UNITS,
+    _classify_c_type, _shapes_compatible, _VEC3_CONVERT_FMT,
+    schema_attrs, schema_subtype_attrs,
+    compute_canon_absorbed,
+    _resolve_mjs_field, _resolve_value_map, _value_map_pair,
+)
+
+
+# ---------------------------------------------------------------------------
+# Hand-enum scrape (regex fallback when the libclang scrape didn't run)
+# ---------------------------------------------------------------------------
+
+_HAND_ENUM_RE = re.compile(
+    r"enum\s+class\s+(EMj\w+)\s*:\s*uint8\s*\{([^}]*)\}",
+    re.DOTALL,
+)
+_HAND_ENUM_MEMBER_RE = re.compile(r"\A\s*(\w+)")
+
+
+def _scan_hand_enums(public_root: str) -> Dict[str, List[str]]:
+    """Walk ``public_root`` for every ``.h`` file declaring
+    ``enum class EMj* : uint8 { ... }`` and return
+    ``{enum_name: [members]}``. Members are stripped of inline
+    initialisers, UMETA macros, and comments."""
+    out: Dict[str, List[str]] = {}
+    for root, _dirs, files in os.walk(public_root):
+        for name in files:
+            if not name.endswith(".h"):
+                continue
+            with open(os.path.join(root, name), "r", encoding="utf-8") as f:
+                text = f.read()
+            for m in _HAND_ENUM_RE.finditer(text):
+                enum_name = m.group(1)
+                body = m.group(2)
+                body = re.sub(r"UMETA\([^)]*\)", "", body)
+                body = re.sub(r"//[^\n]*", "", body)
+                body = re.sub(r"/\*.*?\*/", "", body, flags=re.DOTALL)
+                members: List[str] = []
+                for line in body.split(","):
+                    mm = _HAND_ENUM_MEMBER_RE.match(line)
+                    if mm:
+                        members.append(mm.group(1))
+                if members:
+                    out[enum_name] = members
+    return out
+
+
+def _hand_enums_from_snapshot(mjspec: Optional[Dict[str, Any]]) -> Dict[str, List[str]]:
+    """Pull URLab's hand-rolled ``EMj*`` enums out of the merged
+    introspect snapshot when present. Snapshot path produces real
+    ENUM_CONSTANT_DECL members from libclang. Regex fallback covers
+    the local-dev case where the snapshot wasn't regenerated."""
+    if not mjspec:
+        return {}
+    introspect = mjspec.get("introspect", {}) or {}
+    hand_enums = introspect.get("hand_enums") or {}
+    out: Dict[str, List[str]] = {}
+    for name, entry in hand_enums.items():
+        if not isinstance(entry, dict):
+            continue
+        members = entry.get("members", {})
+        if isinstance(members, dict):
+            out[name] = list(members.keys())
+    return out
+
+
+# ---------------------------------------------------------------------------
+# Drift checks
+# ---------------------------------------------------------------------------
+
+
+def _check_hand_enum_drift(
+    schema: Dict[str, Any],
+    rules: Dict[str, Any],
+    mjspec: Optional[Dict[str, Any]],
+    public_root: str,
+) -> None:
+    """Cross-check rule-declared UE enum members against the actual
+    hand enums AND against MuJoCo's mjt* enums."""
+    hand_enums = _hand_enums_from_snapshot(mjspec)
+    if not hand_enums:
+        hand_enums = _scan_hand_enums(public_root)
+    if not hand_enums:
+        return
+
+    for cat_name, cat_rules in rules.get("categories", {}).items():
+        type_enum = cat_rules.get("type_enum_name")
+        if not type_enum or type_enum not in hand_enums:
+            continue
+        members = set(hand_enums[type_enum])
+        for sub in cat_rules.get("subtypes", []):
+            ev = sub.get("enum_value")
+            if ev and ev not in members:
+                _diag_add(
+                    f"[diagnostic] category '{cat_name}' subtype '{sub.get('key')}' "
+                    f"references enum_value '{type_enum}::{ev}' that does not "
+                    f"exist in {type_enum} members {sorted(members)}.",
+                    source="hand_enum_drift",
+                )
+
+    element_rules = rules.get("element_rules", {})
+    mjspec_enums = (mjspec or {}).get("enums", {})
+    intentional_skips = rules.get("intentionally_unmapped_mj_enum_values", {})
+    for elem, elem_rules in element_rules.items():
+        for attr, enum_def in elem_rules.get("xml_enum_attrs", {}).items():
+            ue_enum_type = enum_def.get("ue_enum_type") or enum_def.get("ue_enum")
+            value_map = enum_def.get("value_map", {})
+            if not ue_enum_type or ue_enum_type not in hand_enums:
+                continue
+            members = set(hand_enums[ue_enum_type])
+            for xml_val, mapping in value_map.items():
+                pair = _value_map_pair(mapping)
+                if pair is None:
+                    continue
+                ue_member, _ = pair
+                if ue_member not in members:
+                    _diag_add(
+                        f"[diagnostic] element '{elem}' xml_enum '{attr}' "
+                        f"value_map entry '{xml_val}' maps to "
+                        f"'{ue_enum_type}::{ue_member}' that does not exist "
+                        f"in members {sorted(members)}.",
+                        source="hand_enum_drift",
+                    )
+            from_mj_enum = enum_def.get("from_mj_enum")
+            if not from_mj_enum:
+                mjs_cast = enum_def.get("mjs_cast", "")
+                if mjs_cast.startswith("mjt"):
+                    from_mj_enum = mjs_cast
+            if from_mj_enum and from_mj_enum in mjspec_enums:
+                mj_values = mjspec_enums[from_mj_enum]
+                covered_mj: set = set()
+                for mapping in value_map.values():
+                    pair = _value_map_pair(mapping)
+                    if pair is not None:
+                        covered_mj.add(pair[1])
+                skips = set(intentional_skips.get(from_mj_enum, []))
+                per_elem = intentional_skips.get("__per_element__", {})
+                skips |= set(per_elem.get(f"{elem}.{attr}", []))
+                for mj_const in mj_values:
+                    if mj_const in covered_mj or mj_const in skips:
+                        continue
+                    _diag_add(
+                        f"[diagnostic] {from_mj_enum}::{mj_const} (from "
+                        f"mjspec snapshot) has no mapping in element "
+                        f"'{elem}' xml_enum_attrs.{attr}.value_map; new "
+                        f"MuJoCo enum values silently fall through to the "
+                        f"default branch at runtime. Add a value_map entry "
+                        f"or list under 'intentionally_unmapped_mj_enum_values"
+                        f".{from_mj_enum}'.",
+                        source="hand_enum_drift",
+                    )
+
+
+def _check_type_shape_drift(
+    schema: Dict[str, Any],
+    rules: Dict[str, Any],
+    mjspec: Optional[Dict[str, Any]],
+) -> None:
+    """Cross-check ``type_mappings[attr]`` against the actual C-type
+    shape of the corresponding mjs struct field."""
+    if not mjspec:
+        return
+    struct_table = (mjspec.get("introspect") or {}).get("structs", {})
+    if not struct_table:
+        return
+    type_map = rules.get("type_mappings", {})
+    unmodeled_mjs = rules.get("intentionally_unmodeled_mjs_fields", {})
+    element_rules = rules.get("element_rules", {})
+    for cat_name, cat_rules in rules.get("categories", {}).items():
+        mjs_struct = cat_rules.get("mjs_struct")
+        if not mjs_struct or mjs_struct not in struct_table:
+            continue
+        fields = {
+            f["name"]: (f.get("c_type", ""), f.get("array_dim"))
+            for f in struct_table[mjs_struct].get("fields", [])
+        }
+        skip_fields = set(unmodeled_mjs.get(mjs_struct, []))
+        skip_fields |= set(element_rules.get(cat_name, {}).get("exclude_attrs", []))
+        for attr, ue_type in type_map.items():
+            if attr not in fields or attr in skip_fields:
+                continue
+            ue_shape = _UE_TYPE_FAMILY.get(ue_type)
+            if not ue_shape:
+                continue
+            c_type, array_dim = fields[attr]
+            c_shape = _classify_c_type(c_type, array_dim)
+            if c_shape is None or c_shape[0] == "opaque":
+                continue
+            if not _shapes_compatible(ue_shape, c_shape):
+                _diag_add(
+                    f"[diagnostic] type_mappings['{attr}'] = '{ue_type}' "
+                    f"(category '{cat_name}') but {mjs_struct}.{attr} is "
+                    f"'{c_type}' (shape={c_shape}); rule expects "
+                    f"shape={ue_shape}. UE/C shape mismatch — emitted "
+                    f"code may overflow, truncate, or fail to compile.",
+                    source="type_shape_drift",
+                )
+
+
+def _check_new_attr_typing(
+    schema: Dict[str, Any],
+    rules: Dict[str, Any],
+) -> None:
+    """Every schema attr that lands as a UPROPERTY but has no explicit
+    type_mappings entry silently inherits ``default_type`` ('float').
+    Also surfaces type_mappings entries whose UE type does not resolve
+    to a known shape — the codegen would otherwise fall back to a
+    default emit and hide the typo (e.g. ``TArray<flot>``)."""
+    type_map = rules.get("type_mappings", {})
+    default_type = rules.get("default_type", "float")
+    global_excl = set(rules.get("global_exclusions", []))
+    intentional_float = set(rules.get("intentionally_default_typed_attrs", []))
+    element_rules = rules.get("element_rules", {})
+    canonicalizations = rules.get("canonicalizations", {})
+
+    def _ue_type_known(ue_type: str) -> bool:
+        if ue_type in _UE_TYPE_INFO:
+            return True
+        if ue_type.startswith("EMj"):
+            return True
+        if ue_type.startswith("TArray<") and ue_type.endswith(">"):
+            inner = ue_type[len("TArray<"):-1]
+            return inner in _UE_TYPE_INFO or inner.startswith("EMj") or inner.startswith("U")
+        return False
+
+    bad_typed: set = set()
+    for attr, ue_type in type_map.items():
+        if attr.startswith("_"):
+            continue
+        if not _ue_type_known(ue_type):
+            bad_typed.add(attr)
+            _diag_add(
+                f"[diagnostic] type_mappings['{attr}'] = '{ue_type}' is "
+                f"not a recognised UE type. The codegen will fall back "
+                f"to default_type ('{default_type}') for downstream "
+                f"emits, hiding the typo. Fix the type or add the entry "
+                f"to _UE_TYPE_INFO.",
+                source="new_attr_typing",
+            )
+
+    def _collect_attrs(cat_rules: Dict[str, Any]) -> set:
+        out: set = set(schema_attrs(schema, cat_rules.get("schema_common_block", "")))
+        out |= set(cat_rules.get("schema_common_extra_attrs", []))
+        sub_block = cat_rules.get("schema_subtypes_block")
+        if sub_block and sub_block in schema:
+            block = schema[sub_block]
+            if isinstance(block, dict):
+                for sub_attrs in block.values():
+                    if isinstance(sub_attrs, list):
+                        out |= set(sub_attrs)
+                    elif isinstance(sub_attrs, dict):
+                        out |= set(sub_attrs.get("attrs", []))
+            elif isinstance(block, list):
+                for entry in block:
+                    if isinstance(entry, dict):
+                        out |= set(entry.get("attrs", []))
+        return out
+
+    flagged: set = set()
+    for cat_name, cat_rules in rules.get("categories", {}).items():
+        elem_rules = element_rules.get(cat_name, {})
+        elem_excl = set(elem_rules.get("exclude_attrs", []))
+        xml_enum_attrs = set(elem_rules.get("xml_enum_attrs", {}).keys())
+        renames = set(elem_rules.get("property_renames", {}).keys())
+        collations = set(elem_rules.get("target_collations", {}).keys())
+        export_skip = set(elem_rules.get("export_skip_attrs", []))
+        common_imports = set()
+        for entry in elem_rules.get("common_imports", []):
+            if isinstance(entry, str):
+                common_imports.add(entry)
+            elif isinstance(entry, dict):
+                common_imports.add(entry.get("attr"))
+        canon_absorbed: set = compute_canon_absorbed(
+            elem_rules.get("applies_canonicalizations", []),
+            canonicalizations,
+        )
+        for attr in _collect_attrs(cat_rules):
+            if attr in global_excl or attr in elem_excl:
+                continue
+            if attr in xml_enum_attrs or attr in renames or attr in collations:
+                continue
+            if attr in canon_absorbed or attr in export_skip or attr in common_imports:
+                continue
+            if attr in type_map:
+                continue
+            if attr in flagged or attr in intentional_float:
+                continue
+            flagged.add(attr)
+            _diag_add(
+                f"[diagnostic] attr '{attr}' (used by {cat_name}) has no "
+                f"type_mappings entry and would fall back to default_type "
+                f"('{default_type}'). Pin its UE type in type_mappings, or "
+                f"list it under 'intentionally_default_typed_attrs' to "
+                f"acknowledge the fallback.",
+                source="new_attr_typing",
+            )
+
+
+_ATTR_NAME_RE = re.compile(r"^[a-z_][a-z0-9_]*$")
+
+
+def _all_schema_attrs(schema: Dict[str, Any], rules: Dict[str, Any]) -> set:
+    """Union of every schema attr observed anywhere in the snapshot."""
+    out: set = set()
+
+    def _walk(node: Any) -> None:
+        if isinstance(node, list):
+            for item in node:
+                if isinstance(item, str) and _ATTR_NAME_RE.match(item):
+                    out.add(item)
+                else:
+                    _walk(item)
+        elif isinstance(node, dict):
+            for k, v in node.items():
+                if k in ("obj_attr", "ref_attr") and isinstance(v, str):
+                    if _ATTR_NAME_RE.match(v):
+                        out.add(v)
+                _walk(v)
+
+    _walk(schema)
+    for cat_rules in rules.get("categories", {}).values():
+        out |= set(cat_rules.get("schema_common_extra_attrs", []))
+    return out
+
+
+def _check_orphan_rule_entries(
+    schema: Dict[str, Any],
+    rules: Dict[str, Any],
+) -> None:
+    """Diagnose rule entries that reference attrs no schema element
+    uses. Catches stale rules left over from MuJoCo renaming/removing
+    an attr."""
+    known_attrs = _all_schema_attrs(schema, rules)
+    if not known_attrs:
+        return
+    type_map = rules.get("type_mappings", {})
+    for attr in type_map.keys():
+        if attr not in known_attrs:
+            _diag_add(
+                f"[diagnostic] type_mappings['{attr}'] is set but no "
+                f"schema element exposes an attr by that name. Dead rule "
+                f"entry — remove, or check whether MuJoCo renamed/removed "
+                f"the attr.",
+                source="orphan_rule",
+            )
+    PER_ELEM_KEYS = (
+        "exclude_attrs",
+        "vec3_convert",
+        "property_renames",
+        "property_meta",
+        "property_defaults",
+        "export_skip_attrs",
+        "mjs_data_packed_attrs",
+    )
+    for elem, elem_rules in rules.get("element_rules", {}).items():
+        for key in PER_ELEM_KEYS:
+            block = elem_rules.get(key, {})
+            if isinstance(block, dict):
+                rule_attrs = list(block.keys())
+            elif isinstance(block, list):
+                rule_attrs = [
+                    a if isinstance(a, str) else a.get("attr")
+                    for a in block
+                ]
+            else:
+                continue
+            for attr in rule_attrs:
+                if not attr or attr in known_attrs:
+                    continue
+                _diag_add(
+                    f"[diagnostic] element_rules['{elem}']['{key}'] "
+                    f"references attr '{attr}', which is not present in "
+                    f"ANY schema element. Stale rule.",
+                    source="orphan_rule",
+                )
+
+
+def _check_mjxmacro_block_coverage(
+    rules: Dict[str, Any],
+    mjxmacro: Dict[str, Any],
+) -> None:
+    """Bi-directional check between mjxmacro snapshot blocks and
+    ``synthetic_categories`` entries. Forward catches new blocks
+    upstream URLab doesn't model; reverse catches rule entries
+    pointing at vanished blocks."""
+    blocks = mjxmacro.get("struct_fields", {})
+    synthetic = rules.get("synthetic_categories", {})
+    if not blocks and not synthetic:
+        return
+    block_to_rule: Dict[str, str] = {}
+    for key, entry in synthetic.items():
+        if not isinstance(entry, dict):
+            continue
+        mb = entry.get("mjxmacro_block")
+        if mb:
+            block_to_rule[mb] = key
+    for block in blocks:
+        if block not in block_to_rule:
+            _diag_add(
+                f"[diagnostic] mjxmacro struct_fields block '{block}' has "
+                f"no synthetic_categories rule entry; the {len(blocks[block])} "
+                f"field(s) it declares will not be emitted as a UE struct.",
+                source="mjxmacro_block_coverage",
+            )
+    for block, rule_key in block_to_rule.items():
+        if block not in blocks:
+            _diag_add(
+                f"[diagnostic] synthetic_categories '{rule_key}' references "
+                f"mjxmacro_block '{block}' that does not exist in the current "
+                f"mjxmacro snapshot — block renamed or removed upstream.",
+                source="mjxmacro_block_coverage",
+            )
+
+
+# Apply-mode values _emit_synth_apply_methods_cpp understands. Anything
+# else lands as a silent passthrough to vec3_y_flip / unconditional —
+# the diagnostic surfaces typos so a rule author's intent is real.
+_VALID_APPLY_MODES = (
+    {"unconditional", "guarded", "pos"}
+    | set(_VEC3_CONVERT_FMT.keys())
+    | {f"guarded_{k}" for k in _VEC3_CONVERT_FMT.keys()}
+    | {"guarded_pos"}
+)
+
+
+def _check_apply_mode_validity(rules: Dict[str, Any]) -> None:
+    """Diagnose ``field_apply_to_spec_mode`` values the codegen would
+    silently fall back on."""
+    synth = rules.get("synthetic_categories", {})
+    for cat_name, def_ in synth.items():
+        if cat_name.startswith("_") or not isinstance(def_, dict):
+            continue
+        modes = def_.get("field_apply_to_spec_mode", {})
+        if not isinstance(modes, dict):
+            continue
+        for field, mode in modes.items():
+            if mode not in _VALID_APPLY_MODES:
+                _diag_add(
+                    f"[diagnostic] synthetic_categories['{cat_name}']"
+                    f".field_apply_to_spec_mode['{field}'] = '{mode}' "
+                    f"is not a recognised apply mode. Expected one of "
+                    f"{sorted(_VALID_APPLY_MODES)}.",
+                    source="apply_mode",
+                )
+
+
+def _check_embedded_cpp_references(
+    rules: Dict[str, Any], mjspec: Optional[Dict[str, Any]],
+) -> None:
+    """Diagnose rule entries that name a MuJoCo C symbol the mjspec
+    snapshot doesn't carry."""
+    if not mjspec or "enums" not in mjspec:
+        return
+    known_consts: Set[str] = set()
+    for members in mjspec["enums"].values():
+        if isinstance(members, dict):
+            known_consts.update(members.keys())
+    if not known_consts:
+        return
+    for elem, elem_rules in rules.get("element_rules", {}).items():
+        for attr, enum_def in elem_rules.get("xml_enum_attrs", {}).items():
+            for xml_val, mapping in enum_def.get("value_map", {}).items():
+                pair = _value_map_pair(mapping)
+                if pair is None:
+                    continue
+                mj_const = pair[1]
+                if not isinstance(mj_const, str):
+                    continue
+                if not mj_const.startswith("mj"):
+                    continue
+                if mj_const not in known_consts:
+                    _diag_add(
+                        f"[diagnostic] element_rules['{elem}']"
+                        f".xml_enum_attrs['{attr}'].value_map['{xml_val}'][1]"
+                        f" = '{mj_const}' is not present in any mjspec enum. "
+                        f"Likely typo — UE compile would fail with "
+                        f"'undeclared identifier'.",
+                        source="embedded_cpp_refs",
+                    )
+    for cat_name, cat_rules in rules.get("categories", {}).items():
+        final = cat_rules.get("geom_final_type")
+        if not final:
+            continue
+        fallback = final.get("default_fallback")
+        if isinstance(fallback, str) and fallback.startswith("mj") and fallback not in known_consts:
+            _diag_add(
+                f"[diagnostic] categories['{cat_name}'].geom_final_type"
+                f".default_fallback = '{fallback}' is not present in any "
+                f"mjspec enum.",
+                source="embedded_cpp_refs",
+            )
+
+
+def _collect_every_schema_attr(schema: Dict[str, Any]) -> Set[str]:
+    """Walk the entire schema tree returning every attr name found.
+    Used by ``_check_allowlist_staleness``."""
+    out: Set[str] = set()
+
+    def walk(node: Any) -> None:
+        if isinstance(node, dict):
+            for v in node.values():
+                if isinstance(v, list) and all(isinstance(s, str) for s in v):
+                    out.update(v)
+                else:
+                    walk(v)
+        elif isinstance(node, list):
+            for entry in node:
+                walk(entry)
+
+    walk(schema)
+    return out
+
+
+def _check_allowlist_staleness(
+    schema: Dict[str, Any], rules: Dict[str, Any],
+    mjspec: Optional[Dict[str, Any]],
+) -> None:
+    """Surface allowlist entries the codebase no longer needs. Covers
+    4 of the 5 ``intentionally_*`` allowlists; the fifth
+    (intentionally_unmodeled_compiler_attrs) is policed indirectly via
+    _check_compiler_attrs_coverage."""
+    unmodeled_elems: Dict[str, str] = rules.get("intentionally_unmodeled_elements", {})
+    schema_keys = set(schema.keys())
+    for key in unmodeled_elems:
+        if key not in schema_keys:
+            _diag_add(
+                f"[diagnostic] intentionally_unmodeled_elements lists '{key}' "
+                f"but the schema no longer carries that top-level element. "
+                f"Drop the entry.",
+                source="allowlist_staleness",
+            )
+
+    unmodeled_fields: Dict[str, List[str]] = rules.get("intentionally_unmodeled_mjs_fields", {})
+    structs = (mjspec or {}).get("structs", {}) if mjspec else {}
+    if structs:
+        for struct, fields in unmodeled_fields.items():
+            if struct not in structs:
+                _diag_add(
+                    f"[diagnostic] intentionally_unmodeled_mjs_fields names "
+                    f"struct '{struct}' that is missing from the current "
+                    f"mjspec snapshot. Drop the entry.",
+                    source="allowlist_staleness",
+                )
+                continue
+            struct_field_set = set(structs[struct])
+            for field in fields or []:
+                if field not in struct_field_set:
+                    _diag_add(
+                        f"[diagnostic] intentionally_unmodeled_mjs_fields"
+                        f"['{struct}'] lists '{field}' but the field no "
+                        f"longer exists on the mjs struct. Drop the entry.",
+                        source="allowlist_staleness",
+                    )
+
+    default_typed: List[str] = rules.get("intentionally_default_typed_attrs", [])
+    every_attr: Set[str] = _collect_every_schema_attr(schema)
+    for attr in default_typed:
+        if attr not in every_attr:
+            _diag_add(
+                f"[diagnostic] intentionally_default_typed_attrs lists "
+                f"'{attr}' but no schema element carries that attr. "
+                f"Drop the entry.",
+                source="allowlist_staleness",
+            )
+
+    unmapped: Dict[str, Any] = rules.get("intentionally_unmapped_mj_enum_values", {})
+    mj_enums = (mjspec or {}).get("enums", {}) if mjspec else {}
+    if mj_enums:
+        for key in unmapped:
+            if key.startswith("_") or key == "__per_element__":
+                continue
+            if key not in mj_enums:
+                _diag_add(
+                    f"[diagnostic] intentionally_unmapped_mj_enum_values "
+                    f"names mjt enum '{key}' that is missing from the "
+                    f"current mjspec snapshot. Drop the entry.",
+                    source="allowlist_staleness",
+                )
+
+
+def _check_property_units_validity(
+    rules: Dict[str, Any],
+) -> None:
+    """Diagnose property_units entries that use an unrecognised UE
+    display unit, or target a UE type that UHT rejects Units meta on."""
+    for elem, elem_rules in rules.get("element_rules", {}).items():
+        units = elem_rules.get("property_units", {})
+        if not isinstance(units, dict):
+            continue
+        for attr, unit in units.items():
+            if unit not in _KNOWN_UE_UNITS:
+                _diag_add(
+                    f"[diagnostic] element_rules['{elem}'].property_units"
+                    f"['{attr}'] = '{unit}' is not a UE-recognised display "
+                    f"unit. Use one of {sorted(_KNOWN_UE_UNITS)} or extend "
+                    f"_KNOWN_UE_UNITS in _codegen_core.",
+                    source="property_units",
+                )
+
+
+def _check_compiler_attrs_coverage(
+    schema: Dict[str, Any],
+    rules: Dict[str, Any],
+) -> None:
+    """Diagnose any <compiler> attribute the MJCF schema lists that
+    URLab neither models in FMjCompilerSettings nor explicitly skips."""
+    compiler_attrs = schema.get("compiler", {}).get("attrs", [])
+    if not compiler_attrs:
+        return
+    mapped = set(rules.get("compiler_attr_field_map", {}).keys())
+    skipped = set(rules.get("intentionally_unmodeled_compiler_attrs", []))
+    for attr in compiler_attrs:
+        if attr in mapped or attr in skipped:
+            continue
+        _diag_add(
+            f"[diagnostic] <compiler {attr}='...'> is in the MJCF schema "
+            f"but URLab does not handle it. Either add a field to "
+            f"FMjCompilerSettings + an entry in 'compiler_attr_field_map', "
+            f"or add to 'intentionally_unmodeled_compiler_attrs' if the "
+            f"flag has no UE-side effect.",
+            source="compiler_coverage",
+        )
+
+
+def _check_value_map_stale_mj_consts(
+    rules: Dict[str, Any],
+    mjspec: Optional[Dict[str, Any]],
+) -> None:
+    """Every value_map[*][1] is a literal mj* C constant emitted into
+    the generated .cpp; the snapshot cross-check surfaces removed/
+    renamed constants before they break the C++ compile."""
+    if not mjspec:
+        return
+    enums = mjspec.get("enums", {})
+    if not enums:
+        return
+    known: set = set()
+    for members in enums.values():
+        if isinstance(members, dict):
+            known.update(members.keys())
+    if not known:
+        return
+    for elem, elem_rules in rules.get("element_rules", {}).items():
+        for attr, enum_def in elem_rules.get("xml_enum_attrs", {}).items():
+            value_map = enum_def.get("value_map", {})
+            for xml_val, mapping in value_map.items():
+                pair = _value_map_pair(mapping)
+                if pair is None:
+                    continue
+                mj_const = pair[1]
+                if not isinstance(mj_const, str) or not mj_const.startswith("mj"):
+                    continue
+                if mj_const not in known:
+                    _diag_add(
+                        f"[diagnostic] element '{elem}' xml_enum '{attr}' "
+                        f"value_map entry '{xml_val}' references "
+                        f"'{mj_const}', which is NOT in the mjspec "
+                        f"snapshot's enum members. The generated .cpp "
+                        f"will fail to compile when the MuJoCo headers "
+                        f"update.",
+                        source="value_map_stale",
+                    )
+
+
+def _check_top_level_elements_coverage(
+    schema: Dict[str, Any], rules: Dict[str, Any],
+) -> None:
+    """Flag top-level schema elements with no category in rules."""
+    categories = rules.get("categories", {})
+    known_blocks = {c.get("schema_common_block", "").split(".", 1)[0]
+                    for c in categories.values()}
+    known_blocks.discard("")
+    container_keys = {
+        "_meta", "actuator_common", "actuator_types", "sensor_common",
+        "sensor_types", "sensor_per_type", "sensor_extras",
+        "tendon", "equality", "contact",
+        "asset", "compiler", "option", "keyframe", "default",
+    }
+    unmodeled: Dict[str, str] = rules.get("intentionally_unmodeled_elements", {})
+    for key in schema:
+        if key.startswith("_") or key in container_keys or key in known_blocks:
+            continue
+        if key in unmodeled:
+            continue
+        _diag_add(
+            f"[diagnostic] schema has top-level element '{key}' but no "
+            f"category in codegen_rules.json (rules['categories']). "
+            f"Either add a category entry, list '{key}' in "
+            f"container_keys, or add an "
+            f"'intentionally_unmodeled_elements' entry explaining why "
+            f"it's skipped.",
+            source="top_level_coverage",
+        )
+
+
+def _check_actuator_subtypes_coverage(
+    schema: Dict[str, Any], rules: Dict[str, Any],
+) -> None:
+    act_cat = rules.get("categories", {}).get("actuator", {})
+    known_act_subs = {s.get("key") for s in act_cat.get("subtypes", [])}
+    for name in schema.get("actuator_types", {}):
+        if name not in known_act_subs:
+            _diag_add(
+                f"[diagnostic] schema actuator subtype '{name}' has no "
+                f"entry in categories.actuator.subtypes. Add a subtype "
+                f"+ subtype_setto rule pair so codegen emits the UMj"
+                f"{name.title()}Actuator class.",
+                source="actuator_subtypes_coverage",
+            )
+
+
+def _check_sensor_subtypes_coverage(
+    schema: Dict[str, Any], rules: Dict[str, Any],
+) -> None:
+    sens_cat = rules.get("categories", {}).get("sensor", {})
+    known_sens_subs = {s.get("key") for s in sens_cat.get("subtypes", [])}
+    for name in schema.get("sensor_types", []):
+        if name not in known_sens_subs:
+            _diag_add(
+                f"[diagnostic] schema sensor type '{name}' has no entry "
+                f"in categories.sensor.subtypes. Add it so the per-sensor "
+                f"UMjXSensor subclass is emitted.",
+                source="sensor_subtypes_coverage",
+            )
+
+
+def _check_setto_param_coverage(
+    schema: Dict[str, Any], rules: Dict[str, Any],
+    mjspec: Optional[Dict[str, Any]],
+) -> None:
+    """Flag mjs_setTo* parameters with no UE-side mapping AND no
+    explicit sentinel default."""
+    if not mjspec or "setto_functions" not in mjspec:
+        return
+    categories = rules.get("categories", {})
+    setto_defaults = rules.get("setto_param_defaults", {})
+    for cat_name, cat_rules in categories.items():
+        setto_block = cat_rules.get("subtype_setto") or {}
+        common_attrs = set(schema_attrs(
+            schema, cat_rules.get("schema_common_block", "")))
+        for sub_key, setto_def in setto_block.items():
+            if not isinstance(setto_def, dict):
+                continue
+            fn_name = setto_def.get("call")
+            if not fn_name or fn_name not in mjspec["setto_functions"]:
+                continue
+            sig = mjspec["setto_functions"][fn_name]
+            renames = setto_def.get("param_renames", {})
+            fn_defaults = set(setto_defaults.get(fn_name, {}).keys())
+            sub_attrs = set(schema_subtype_attrs(schema, cat_name, sub_key))
+            schema_attr_set = common_attrs | sub_attrs
+            for p in sig.get("params", []):
+                pname = p["name"]
+                ue_prop = renames.get(pname, pname)
+                if ue_prop in schema_attr_set or pname in fn_defaults:
+                    continue
+                _diag_add(
+                    f"[diagnostic] {fn_name} param '{pname}' is not "
+                    f"in the {cat_name}.{sub_key} schema attrs and "
+                    f"has no param_renames or setto_param_defaults "
+                    f"entry. Codegen passes the generic sentinel for "
+                    f"it. If MuJoCo added this param, decide whether "
+                    f"to expose it as a UE UPROPERTY (schema attr + "
+                    f"type_mapping) or pin its sentinel.",
+                    source="setto_param_coverage",
+                )
+
+
+def _check_mjs_struct_field_drift(
+    schema: Dict[str, Any], rules: Dict[str, Any],
+    mjspec: Optional[Dict[str, Any]],
+) -> None:
+    """Flag mjs struct fields with no schema-attr coverage or rule
+    binding."""
+    if not mjspec or "structs" not in mjspec:
+        return
+    categories = rules.get("categories", {})
+    unmodeled_by_struct: Dict[str, set] = {
+        struct: set(fields) for struct, fields in
+        rules.get("intentionally_unmodeled_mjs_fields", {}).items()
+    }
+    for cat_name, cat_rules in categories.items():
+        mjs_struct = cat_rules.get("mjs_struct")
+        if not mjs_struct or mjs_struct not in mjspec["structs"]:
+            continue
+        block = cat_rules.get("schema_common_block", "")
+        schema_attrs_set = set(schema_attrs(schema, block))
+        for sub in cat_rules.get("subtypes", []):
+            schema_attrs_set.update(
+                schema_subtype_attrs(schema, cat_name, sub.get("key", ""))
+            )
+        mjs_field_set = set(mjspec["structs"][mjs_struct])
+        elem_rules = rules.get("element_rules", {}).get(cat_name, {})
+        attr_to_mjs = elem_rules.get("attr_to_mjs_field", {})
+        mapped_fields = set(attr_to_mjs.values())
+        for coll_def in elem_rules.get("target_collations", {}).values():
+            if isinstance(coll_def, dict) and coll_def.get("mjs_field"):
+                mapped_fields.add(coll_def["mjs_field"])
+        for enum_def in elem_rules.get("xml_enum_attrs", {}).values():
+            if isinstance(enum_def, dict) and enum_def.get("mjs_field"):
+                mapped_fields.add(enum_def["mjs_field"])
+        for packed in elem_rules.get("mjs_data_packed_attrs", {}).values():
+            if isinstance(packed, dict):
+                mapped_fields.add(packed.get("target") or "data")
+        for da in elem_rules.get("mjs_double_vec_attrs", []):
+            mapped_fields.add(da)
+        if cat_rules.get("type_enum_name"):
+            mapped_fields.add("type")
+        mapped_fields.add("objtype")
+        for conv_list in elem_rules.get("unit_conversion", {}).values():
+            for entry in conv_list if isinstance(conv_list, list) else []:
+                if isinstance(entry, dict) and entry.get("mjs_field"):
+                    mapped_fields.add(entry["mjs_field"])
+        # Canonicalisation name -> set of mjs struct fields its export
+        # block writes. Lets the drift check treat canon-owned fields as
+        # covered. Inlined here because this is the only consumer; kept
+        # in lock-step with the inline canon emitters in
+        # generate_ue_components (_canon_export_*).
+        canon_mjs_fields = {
+            "body_sleep_policy":     {"sleep"},
+            "actuator_transmission": {"target", "trntype", "slidersite", "refsite"},
+            "fromto_decompose":      set(),
+            "orientation":           {"quat"},
+            "spatial_pose":          {"pos"},
+        }
+        for canon in elem_rules.get("applies_canonicalizations", []):
+            mapped_fields |= canon_mjs_fields.get(canon, set())
+        structural = {
+            "element", "info", "userdata", "user", "name", "classname",
+            "parent", "frame", "explicitinertial",
+        }
+        structural |= unmodeled_by_struct.get(mjs_struct, set())
+        structural |= mapped_fields
+        normalised_schema = {a.replace("_", "").lower()
+                             for a in schema_attrs_set}
+        for mjs_field in mjs_field_set:
+            if mjs_field in structural:
+                continue
+            candidate = mjs_field.replace("_", "").lower()
+            if candidate in normalised_schema:
+                continue
+            stripped = candidate.replace("name", "")
+            if stripped in normalised_schema:
+                continue
+            if candidate.startswith("act") and ("actuator" + candidate[3:]) in normalised_schema:
+                continue
+            _diag_add(
+                f"[diagnostic] {mjs_struct}.{mjs_field} (in mjspec_snapshot) "
+                f"has no corresponding schema attr in '{cat_name}'. Either "
+                f"add a schema canonicalisation, list it under "
+                f"intentionally_unmodeled_mjs_fields, or accept that codegen "
+                f"silently skips it.",
+                source="mjs_field_drift",
+            )
+
+
+def _check_generated_enum_coverage(
+    rules: Dict[str, Any], mjspec: Optional[Dict[str, Any]],
+) -> None:
+    """Flag generated_enums entries that map fewer mj enum members
+    than the snapshot carries."""
+    if not mjspec or "enums" not in mjspec:
+        return
+    for gen_name, gen_def in rules.get("generated_enums", {}).items():
+        if gen_name.startswith("_") or not isinstance(gen_def, dict):
+            continue
+        if gen_def.get("disabled"):
+            continue
+        for entry in gen_def.get("enums", []):
+            if entry.get("disabled"):
+                continue
+            mj_enum_name = entry.get("from_mj_enum")
+            ue_member_from_mj = entry.get("ue_member_from_mj", {})
+            exclude = set(entry.get("exclude_mj_members", []))
+            if not mj_enum_name:
+                continue
+            c_enum = mjspec["enums"].get(mj_enum_name, {})
+            if not c_enum:
+                _diag_add(
+                    f"[diagnostic] generated_enums['{gen_name}'].{entry.get('ue_name')}: "
+                    f"from_mj_enum '{mj_enum_name}' not found in the projected mjspec. "
+                    f"Rebuild the introspect snapshot, or correct the enum name.",
+                    source="enum_drift",
+                )
+                continue
+            mapped = set(ue_member_from_mj) | exclude
+            missing = [m for m in c_enum if m not in mapped]
+            if missing:
+                _diag_add(
+                    f"[diagnostic] generated_enums['{gen_name}'].{entry.get('ue_name')}: "
+                    f"C enum '{mj_enum_name}' has {len(missing)} member(s) with no UE "
+                    f"mapping: {missing[:5]}{'...' if len(missing) > 5 else ''}. Add to "
+                    f"ue_member_from_mj or exclude_mj_members.",
+                    source="enum_drift",
+                )
+
+
+def _emit_drift_diagnostics(schema: Dict[str, Any], rules: Dict[str, Any],
+                            mjspec: Optional[Dict[str, Any]]) -> None:
+    """Run every drift-coverage check. Each helper surfaces one class
+    of drift. All diagnostics route through the module-level
+    ``_DIAGS_BUFFER.pending`` collector and flush once at the end of
+    ``collect_all_writes``."""
+    _check_top_level_elements_coverage(schema, rules)
+    _check_actuator_subtypes_coverage(schema, rules)
+    _check_sensor_subtypes_coverage(schema, rules)
+    _check_setto_param_coverage(schema, rules, mjspec)
+    _check_mjs_struct_field_drift(schema, rules, mjspec)
+    _check_generated_enum_coverage(rules, mjspec)
diff --git a/Scripts/codegen/_codegen_core.py b/Scripts/codegen/_codegen_core.py
new file mode 100644
index 0000000..817331b
--- /dev/null
+++ b/Scripts/codegen/_codegen_core.py
@@ -0,0 +1,487 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""URLab codegen — core shared data + helpers.
+
+Dependency root. Owns:
+
+- ``DiagBuffer`` — the diagnostics buffer the orchestrator + checks
+  share.
+- ``_UE_TYPE_INFO`` — single source of truth for every UE type
+  codegen can emit; ``_UE_TYPE_FAMILY`` and
+  ``_UE_TYPES_ACCEPTING_UNITS_META`` are derived views.
+- Naming helpers, schema readers, canon-iteration helpers.
+- The 5-strategy ``_resolve_mjs_field`` chain — the bridge between
+  MJCF attr names and the C struct field names MuJoCo picks.
+- ``_resolve_value_map`` — the xml_enum value-map reader.
+- ``FileWrite`` — the data class every emitter produces.
+
+Read-only against rules / schema / snapshot. No file writes, no
+UPROPERTY emission, no .cpp injection. ``generate_ue_components``
+and ``_codegen_checks`` both import from here.
+"""
+
+from __future__ import annotations
+
+import re
+import sys
+from dataclasses import dataclass
+from typing import Any, Dict, Iterable, Iterator, List, Optional, Sequence, Set, Tuple
+
+
+# ---------------------------------------------------------------------------
+# Diagnostics
+# ---------------------------------------------------------------------------
+
+@dataclass
+class Diagnostic:
+    """A non-fatal codegen warning. Collected by ``_diag_add`` during a
+    run and flushed once at the end via ``_diag_flush``."""
+    message:  str
+    source:   str = ""    # short tag for grouping (e.g. "schema_drift")
+
+
+class DiagBuffer:
+    """Collects codegen diagnostics during a run. ``pending`` is the
+    queue of unflushed entries; ``fired_count`` survives the flush so
+    ``main()`` can check whether ``--strict`` should exit non-zero."""
+
+    def __init__(self) -> None:
+        self.pending: List[Diagnostic] = []
+        self.fired_count: int = 0
+
+    def add(self, message: str, *, source: str = "") -> None:
+        self.pending.append(Diagnostic(message=message, source=source))
+
+    def flush(self) -> None:
+        """Print collected diagnostics to stderr, advance ``fired_count``
+        by the number flushed, and clear the pending list."""
+        if not self.pending:
+            return
+        print(f"\n--- codegen diagnostics ({len(self.pending)}) ---",
+              file=sys.stderr)
+        for d in self.pending:
+            print(d.message, file=sys.stderr)
+        print(
+            "(diagnostics are non-fatal; the codegen has emitted what it "
+            "can — fix the rules to silence them.)",
+            file=sys.stderr,
+        )
+        self.fired_count += len(self.pending)
+        self.pending.clear()
+
+    def reset(self) -> None:
+        self.pending.clear()
+        self.fired_count = 0
+
+
+_DIAGS_BUFFER = DiagBuffer()
+
+
+def _diag_add(message: str, *, source: str = "") -> None:
+    _DIAGS_BUFFER.add(message, source=source)
+
+
+def _diag_flush() -> None:
+    _DIAGS_BUFFER.flush()
+
+
+def _reset_diags() -> None:
+    """Reset the diag buffer between runs so back-to-back invocations
+    in the same Python process (test runners, integration shells)
+    don't carry stale state."""
+    _DIAGS_BUFFER.reset()
+
+
+# ---------------------------------------------------------------------------
+# UE type registry
+# ---------------------------------------------------------------------------
+
+@dataclass(frozen=True)
+class _UEType:
+    """Single source of truth for a UE type codegen can emit. Every per-
+    type dispatch (default initializer, drift-check shape, UHT Units-meta
+    gate) reads this row instead of redeclaring the type universe."""
+    default_init: str                  # C++ initializer (e.g. ``"0.0f"``)
+    shape: Tuple[str, Any]             # drift-check shape (see _classify_c_type)
+    accepts_units_meta: bool           # UHT allows Units="..." on this type
+
+
+# Every UE type codegen can target. ``_attr_default_value``,
+# ``_UE_TYPE_FAMILY``, and ``_UE_TYPES_ACCEPTING_UNITS_META`` below are
+# thin views over this dict.
+_UE_TYPE_INFO: Dict[str, _UEType] = {
+    "float":           _UEType("0.0f",                  ("scalar", "num"),    True),
+    "double":          _UEType("0.0",                   ("scalar", "num"),    True),
+    "int32":           _UEType("0",                     ("scalar", "int"),    True),
+    "uint8":           _UEType("0",                     ("scalar", "int"),    True),  # synth-mirror only
+    "bool":            _UEType("false",                 ("scalar", "bool"),   False),
+    "FString":         _UEType('TEXT("")',              ("scalar", "string"), False),
+    "FVector":         _UEType("FVector::ZeroVector",   ("vec", 3),           True),
+    "FQuat":           _UEType("FQuat::Identity",       ("vec", 4),           False),
+    "FLinearColor":    _UEType("FLinearColor::White",   ("vec", 4),           False),
+    "TArray<float>":   _UEType("{}",                    ("array", None),      False),
+    "TArray<int32>":   _UEType("{}",                    ("array", None),      False),
+}
+
+
+def _attr_default_value(ue_type: str) -> str:
+    info = _UE_TYPE_INFO.get(ue_type)
+    if info is not None:
+        return info.default_init
+    # Unknown types fall through to the default-construct literal — same
+    # for novel TArray<...> entries (e.g. TArray<UObject*>) the registry
+    # doesn't enumerate.
+    return "{}"
+
+
+# UE type → shape for ``_check_type_shape_drift``. Derived from
+# ``_UE_TYPE_INFO`` so the type universe is declared once.
+_UE_TYPE_FAMILY = {k: v.shape for k, v in _UE_TYPE_INFO.items()}
+
+
+_UE_TYPES_ACCEPTING_UNITS_META = {
+    k for k, v in _UE_TYPE_INFO.items() if v.accepts_units_meta
+}
+
+
+def _ue_type_accepts_units_meta(ue_type: str) -> bool:
+    """UHT enforces 'Units' meta only on numeric scalars + numeric
+    structs. Returns False for TArray<...>, FString, FLinearColor,
+    FQuat — codegen silently drops the Units meta for those types so
+    the build succeeds."""
+    info = _UE_TYPE_INFO.get(ue_type)
+    return bool(info and info.accepts_units_meta)
+
+
+_DYNAMIC_VEC_TYPES = {
+    "mjFloatVec", "mjDoubleVec", "mjIntVec", "mjStringVec",
+    "mjFloatVecVec", "mjDoubleVecVec", "mjIntVecVec",
+}
+
+
+_KNOWN_UE_UNITS = {
+    # UE5 numeric units the editor knows how to render + (optionally)
+    # convert via Display Units preferences.
+    "m", "cm", "mm", "km",
+    "rad", "deg",
+}
+
+
+@dataclass
+class FileWrite:
+    """One file write the codegen would perform. Collected during the
+    phase walk; ``apply_writes`` either flushes them to disk, prints
+    diffs, or returns the count under ``--check``."""
+    path: str
+    content: str
+
+
+def override_toggle_name(prop_name: str) -> str:
+    """``bOverride_<X>`` is the codegen-emitted toggle field that
+    gates whether the corresponding UPROPERTY's value gets written
+    back to the mjs struct."""
+    return f"bOverride_{prop_name}"
+
+
+# ---------------------------------------------------------------------------
+# Rule-iteration helpers
+# ---------------------------------------------------------------------------
+
+
+def iter_canon_absorbed(elem_canons: Iterable[str],
+                        canonicalizations: Dict[str, Any]) -> Iterator[str]:
+    """Yield every attr absorbed by any canonicalisation in
+    ``elem_canons``. Skips unknown canon keys silently — diagnostics
+    for those live in the rule-validity drift checks."""
+    if not canonicalizations:
+        return
+    for c in elem_canons:
+        cd = canonicalizations.get(c)
+        if cd:
+            yield from cd.get("absorbs_attrs", ())
+
+
+def compute_canon_absorbed(elem_canons: Iterable[str],
+                           canonicalizations: Dict[str, Any]) -> Set[str]:
+    return set(iter_canon_absorbed(elem_canons, canonicalizations))
+
+
+def iter_category_attrs(attrs: Iterable[str], *,
+                        global_excl: Set[str],
+                        elem_excl: Set[str],
+                        canon_absorbed: Set[str],
+                        extra_excl: Optional[Set[str]] = None) -> Iterator[str]:
+    """Yield attrs that pass the standard codegen exclusion gates plus
+    any site-specific ``extra_excl`` set the caller assembles
+    (xml_enum_attrs, target-collation-absorbed, etc — these vary per
+    emission stage)."""
+    for attr in attrs:
+        if attr in global_excl or attr in elem_excl or attr in canon_absorbed:
+            continue
+        if extra_excl is not None and attr in extra_excl:
+            continue
+        yield attr
+
+
+# ---------------------------------------------------------------------------
+# Schema readers
+# ---------------------------------------------------------------------------
+
+
+def schema_attrs(schema: Dict[str, Any], path: str) -> List[str]:
+    """Read an attribute list at a dotted path, e.g.
+    ``actuator_common.attrs``. Returns [] if missing."""
+    cur: Any = schema
+    for piece in path.split("."):
+        if isinstance(cur, dict) and piece in cur:
+            cur = cur[piece]
+        else:
+            return []
+    if isinstance(cur, list):
+        if cur and isinstance(cur[0], str):
+            return list(cur)
+        attrs: List[str] = []
+        for entry in cur:
+            if isinstance(entry, dict):
+                if "attrs" in entry and isinstance(entry["attrs"], list):
+                    attrs.extend([str(x) for x in entry["attrs"]])
+                else:
+                    for v in entry.values():
+                        if isinstance(v, list):
+                            attrs.extend([str(x) for x in v])
+        seen = set()
+        out = []
+        for a in attrs:
+            if a not in seen:
+                seen.add(a)
+                out.append(a)
+        return out
+    if isinstance(cur, dict):
+        attrs2: List[str] = []
+        for v in cur.values():
+            if isinstance(v, list):
+                attrs2.extend([str(x) for x in v if isinstance(x, str)])
+        return attrs2
+    return []
+
+
+def schema_subtype_attrs(schema: Dict[str, Any], category: str,
+                         subtype_key: str) -> List[str]:
+    """Look up per-subtype attrs from schema['actuator_types'][key] /
+    schema['sensor_types'] (list-of-dict). Returns [] if not found."""
+    if category == "actuator":
+        block = schema.get("actuator_types", {})
+        if isinstance(block, dict):
+            entry = block.get(subtype_key)
+            if isinstance(entry, list):
+                return [str(x) for x in entry]
+            if isinstance(entry, dict) and "attrs" in entry:
+                return [str(x) for x in entry["attrs"]]
+        return []
+    if category == "sensor":
+        block = schema.get("sensor_types", [])
+        if isinstance(block, list):
+            for entry in block:
+                if isinstance(entry, dict) and entry.get("name") == subtype_key:
+                    attrs = entry.get("attrs", [])
+                    return [str(x) for x in attrs] if isinstance(attrs, list) else []
+                if isinstance(entry, str) and entry == subtype_key:
+                    return []
+        return []
+    return []
+
+
+# ---------------------------------------------------------------------------
+# C-type classifier (drift-check inputs)
+# ---------------------------------------------------------------------------
+
+
+def _classify_c_type(c_type: str, array_dim) -> Optional[tuple]:
+    """Reduce an introspect c_type + array_dim down to a comparable
+    (shape, dim_or_family) tuple. Shapes:
+      ``("scalar", fam)`` — single value, fam ∈ {num,int,bool,string}
+      ``("vec", N)``      — fixed-size C array of length N
+      ``("array", None)`` — dynamic-length C vec (mjFloatVec, etc.)
+      ``("opaque", _)``   — mjsElement* and other spec-internal
+                            pointers URLab never exposes; returning a
+                            distinct tag lets the check skip them
+                            without silently accepting "scalar"
+                            matches.
+    Returns None for genuinely unknown bases so the check stays quiet
+    on novel types instead of producing noise.
+    """
+    ct = c_type.strip()
+    base_no_arr = re.sub(r"\s*\[[^\]]*\]\s*", "", ct).strip()
+    base_no_ptr = base_no_arr.rstrip(" *").strip()
+    if base_no_ptr in _DYNAMIC_VEC_TYPES:
+        return ("array", None)
+    if base_no_ptr.startswith("mjsElement") or base_no_ptr.endswith("Element"):
+        return ("opaque", None)
+    if "*" in ct or "mjString" in ct or "std::string" in ct:
+        return ("scalar", "string")
+    is_array = array_dim is not None and array_dim != 1
+    base = base_no_arr
+    fam: Optional[str] = None
+    if base in ("mjtNum", "float", "double"):
+        fam = "num"
+    elif base in ("int", "mjtByte", "unsigned char", "uint8_t", "uchar"):
+        fam = "int"
+    elif base in ("bool", "_Bool"):
+        fam = "bool"
+    else:
+        return None
+    if is_array:
+        if array_dim in (3, 4):
+            return ("vec", array_dim)
+        return ("array", None)
+    return ("scalar", fam)
+
+
+def _shapes_compatible(ue_shape: tuple, c_shape: tuple) -> bool:
+    """True when ``ue_shape`` can legitimately back ``c_shape``.
+
+    A ``TArray<X>`` (array) is shape-compatible with a fixed-size C
+    array (vec) because the codegen writes ``prop.Num()`` elements
+    into the C array.
+    Vec-to-vec requires the dim to match exactly.
+    Scalar↔array always mismatches. Opaque C shapes are skipped."""
+    if c_shape[0] == "opaque":
+        return True
+    if ue_shape[0] == c_shape[0]:
+        if ue_shape[0] == "vec":
+            return ue_shape[1] == c_shape[1]
+        return True
+    if ue_shape[0] == "array" and c_shape[0] == "vec":
+        return True
+    return False
+
+
+# ---------------------------------------------------------------------------
+# Vec3 convert format table
+# ---------------------------------------------------------------------------
+#
+# Maps the per-field ``apply_to_spec_mode`` rule values for FVector
+# UPROPERTYs to the per-element write template the synth-struct emitter
+# uses. Each entry is a Python format string with two slots: ``{c}``
+# (the destination C-array expression) and ``{ue}`` (the source UE
+# FVector expression).
+
+_VEC3_CONVERT_FMT = {
+    "pos":         "{c}[0] =  {ue}.X / 100.0; {c}[1] = -{ue}.Y / 100.0; {c}[2] =  {ue}.Z / 100.0;",
+    "vec3_cm":     "{c}[0] =  {ue}.X / 100.0; {c}[1] = -{ue}.Y / 100.0; {c}[2] =  {ue}.Z / 100.0;",
+    "vec3_y_flip": "{c}[0] =  {ue}.X; {c}[1] = -{ue}.Y; {c}[2] =  {ue}.Z;",
+    "passthrough": "{c}[0] = {ue}.X; {c}[1] = {ue}.Y; {c}[2] = {ue}.Z;",
+}
+
+
+# ---------------------------------------------------------------------------
+# mjs-field resolver chain (the 5-strategy bridge)
+# ---------------------------------------------------------------------------
+
+
+def _resolve_direct(attr: str, mjs_fields: Set[str]) -> Optional[str]:
+    """``attr`` matches a struct field verbatim (condim -> condim)."""
+    return attr if attr in mjs_fields else None
+
+
+def _resolve_name_suffix(attr: str, mjs_fields: Set[str]) -> Optional[str]:
+    """``attr + "name"`` matches (mesh -> meshname). MuJoCo's name-
+    ref fields uniformly append ``name`` to the schema attr."""
+    cand = attr + "name"
+    return cand if cand in mjs_fields else None
+
+
+def _resolve_root_name_digits(attr: str, mjs_fields: Set[str]) -> Optional[str]:
+    """``<root><digits>`` -> ``<root>name<digits>`` (body1 ->
+    bodyname1)."""
+    m = re.match(r"^(.*?)(\d+)$", attr)
+    if not m:
+        return None
+    root, digits = m.group(1), m.group(2)
+    for candidate in (f"{root}name{digits}", f"{root}_name{digits}"):
+        if candidate in mjs_fields:
+            return candidate
+    return None
+
+
+def _resolve_underscore_norm(attr: str, mjs_fields: Set[str]) -> Optional[str]:
+    """Strip all underscores and look for matching letters
+    (solreflimit -> solref_limit). Lossy — only used when nothing
+    cleaner fits."""
+    norm = attr.replace("_", "")
+    for f in mjs_fields:
+        if f.replace("_", "") == norm:
+            return f
+    return None
+
+
+def _resolve_actuator_prefix(attr: str, mjs_fields: Set[str]) -> Optional[str]:
+    """Schema ``actuator*`` -> mjs ``act*`` (actuatorfrclimited ->
+    actfrclimited). MuJoCo abbreviates the prefix on its struct
+    fields even though the XML attr spells it out."""
+    if not attr.startswith("actuator"):
+        return None
+    cand = "act" + attr[len("actuator"):]
+    if cand in mjs_fields:
+        return cand
+    cand_norm = cand.replace("_", "")
+    for f in mjs_fields:
+        if f.replace("_", "") == cand_norm:
+            return f
+    return None
+
+
+_MJS_FIELD_RESOLVERS = (
+    _resolve_direct,
+    _resolve_name_suffix,
+    _resolve_root_name_digits,
+    _resolve_underscore_norm,
+    _resolve_actuator_prefix,
+)
+
+
+def _resolve_mjs_field(attr: str, mjs_fields: set,
+                       attr_to_mjs_field: Optional[Dict[str, str]] = None) -> str:
+    """Pick the correct C field name on a category's mjs struct for a
+    given schema attribute. Per-element ``attr_to_mjs_field``
+    overrides apply first; otherwise the ``_MJS_FIELD_RESOLVERS``
+    chain captures MuJoCo's own naming conventions in priority order.
+    Falls back to ``attr`` if nothing matches — the build then
+    surfaces the mismatch loudly when the export line tries to access
+    ``Element->attr``."""
+    if attr_to_mjs_field and attr in attr_to_mjs_field:
+        return attr_to_mjs_field[attr]
+    if not mjs_fields:
+        return attr
+    for resolver in _MJS_FIELD_RESOLVERS:
+        hit = resolver(attr, mjs_fields)
+        if hit is not None:
+            return hit
+    return attr
+
+
+# ---------------------------------------------------------------------------
+# xml_enum value-map resolver
+# ---------------------------------------------------------------------------
+
+
+def _resolve_value_map(attr: str, enum_def: Dict[str, Any]) -> Dict[str, Sequence[str]]:
+    """Return the ``{xml_val -> [UE_enum_member, mj_const]}`` table for
+    an xml_enum attr. The rule must carry an explicit ``value_map``."""
+    if "value_map" in enum_def:
+        return enum_def["value_map"]
+    raise RuntimeError(f"xml_enum {attr}: rule is missing value_map")
+
+
+def _value_map_pair(mapping: Any) -> Optional[Tuple[str, str]]:
+    """Unpack a ``value_map`` entry into ``(ue_member, mj_const)`` or
+    return None when the shape is malformed. The 4 check sites that
+    used to inline ``isinstance(mapping, (list, tuple)) and len(mapping)
+    >= 2`` route through here so one shape contract covers all of
+    them. The corresponding emit-side path raises (see
+    ``_emit_xml_enum_export``) — the rule shape contract test
+    (test_rule_shape_contract.py::test_xml_enum_attr_value_maps_have_
+    correct_shape) is the canonical gate; this helper is the
+    runtime fallback for the drift checks."""
+    if not isinstance(mapping, (list, tuple)) or len(mapping) < 2:
+        return None
+    return mapping[0], mapping[1]
diff --git a/Scripts/codegen/_codegen_inject.py b/Scripts/codegen/_codegen_inject.py
new file mode 100644
index 0000000..f11d70d
--- /dev/null
+++ b/Scripts/codegen/_codegen_inject.py
@@ -0,0 +1,196 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""URLab codegen — text-injection machinery.
+
+Pure text operations: locate ``// --- CODEGEN_<tag>_START/END ---``
+marker pairs in a source file, splice a body between them, and emit
+diagnostics when the body has unbalanced braces or the markers are
+missing.
+
+The brace-aware helpers (``_find_matching_brace``,
+``_strip_cpp_braces_in_strings``, ``_check_brace_balance``) live
+here too because the banner-overwrite audit + the brace-balance
+gate both consume them."""
+
+from __future__ import annotations
+
+import os
+import re
+from typing import Dict, List, Optional, Sequence, Tuple
+
+from _codegen_core import _diag_add, FileWrite
+
+
+def _make_tag_pair(tag: str) -> Tuple[str, str]:
+    return (f"// --- CODEGEN_{tag}_START ---", f"// --- CODEGEN_{tag}_END ---")
+
+
+def _strip_cpp_braces_in_strings(text: str) -> str:
+    """Remove characters that look like braces but live inside a
+    string/char literal or comment. Used by ``_check_brace_balance``
+    so a diagnostic message containing ``"{"`` doesn't trip the
+    balance gate."""
+    text = re.sub(r"//[^\n]*", "", text)
+    text = re.sub(r"/\*.*?\*/", "", text, flags=re.DOTALL)
+    text = re.sub(r'"(?:\\.|[^"\\])*"', '""', text)
+    text = re.sub(r"'(?:\\.|[^'\\])*'", "''", text)
+    return text
+
+
+def _find_matching_brace(text: str, open_idx: int) -> Optional[int]:
+    """Return the index of the ``}`` matching the ``{`` at
+    ``open_idx``. Tracks brace depth across string/char literals and
+    line/block comments so a nested ``{`` inside a TEXT(...) literal
+    doesn't shift the balance. Returns None on EOF / unmatched."""
+    if open_idx < 0 or open_idx >= len(text) or text[open_idx] != "{":
+        return None
+    depth = 1
+    i = open_idx + 1
+    in_str: Optional[str] = None
+    while i < len(text) and depth > 0:
+        ch = text[i]
+        if in_str is not None:
+            if ch == "\\" and i + 1 < len(text):
+                i += 2
+                continue
+            if ch == in_str:
+                in_str = None
+            i += 1
+            continue
+        if ch == "/" and i + 1 < len(text) and text[i + 1] == "/":
+            nl = text.find("\n", i)
+            i = nl if nl != -1 else len(text)
+            continue
+        if ch == "/" and i + 1 < len(text) and text[i + 1] == "*":
+            end = text.find("*/", i + 2)
+            i = end + 2 if end != -1 else len(text)
+            continue
+        if ch in ('"', "'"):
+            in_str = ch
+        elif ch == "{":
+            depth += 1
+        elif ch == "}":
+            depth -= 1
+            if depth == 0:
+                return i
+        i += 1
+    return None
+
+
+def _check_brace_balance(body: str, tag: str, source: str) -> None:
+    """Emit a diagnostic when an injected block has an obvious brace
+    imbalance."""
+    stripped = _strip_cpp_braces_in_strings(body)
+    opens = stripped.count("{")
+    closes = stripped.count("}")
+    if opens != closes:
+        _diag_add(
+            f"[diagnostic] {source}: injected block for tag "
+            f"CODEGEN_{tag} has unbalanced braces (open={opens}, "
+            f"close={closes}). The generated .cpp will not compile.",
+            source=source,
+        )
+
+
+_TAG_PATTERN_CACHE: Dict[str, "re.Pattern[str]"] = {}
+
+
+def _tag_pattern(tag: str) -> "re.Pattern[str]":
+    pat = _TAG_PATTERN_CACHE.get(tag)
+    if pat is None:
+        start, end = _make_tag_pair(tag)
+        pat = re.compile(
+            r"([ \t]*)(" + re.escape(start) + r")(.*?)([ \t]*)(" + re.escape(end) + r")",
+            re.DOTALL,
+        )
+        _TAG_PATTERN_CACHE[tag] = pat
+    return pat
+
+
+def inject_between_tags(file_text: str, tag: str, new_inner: str,
+                        *, balance_source: Optional[str] = None) -> Tuple[str, bool]:
+    """Replace the content between ``// --- CODEGEN_<tag>_START ---``
+    and ``// --- CODEGEN_<tag>_END ---`` with ``new_inner``. Returns
+    (new_text, was_found).
+
+    The END marker's leading whitespace is rewritten to match the
+    START marker's indent. When ``new_inner`` is empty or
+    whitespace-only the block collapses so START + END sit on
+    adjacent lines with no stray blank line between them.
+
+    When ``balance_source`` is set, emit a diagnostic if the injected
+    block has unbalanced braces."""
+    pattern = _tag_pattern(tag)
+    matched = [False]
+
+    def repl(m: re.Match) -> str:
+        matched[0] = True
+        start_indent = m.group(1)
+        trimmed = new_inner.rstrip()
+        if trimmed:
+            body = "\n" + trimmed + "\n" + start_indent
+        else:
+            # Empty body: collapse to one newline so START + END sit
+            # adjacent (no stray blank line between them).
+            body = "\n" + start_indent
+        return m.group(1) + m.group(2) + body + m.group(5)
+
+    new_text = pattern.sub(repl, file_text)
+    if matched[0] and balance_source:
+        _check_brace_balance(new_inner, tag, balance_source)
+    return new_text, matched[0]
+
+
+def _inject_tags_into_cpp(
+    cpp_path: str,
+    tagged_bodies: Sequence[Tuple[str, str]],
+    writes: List[FileWrite],
+    *,
+    diag_source: str,
+) -> None:
+    """Open ``cpp_path`` once, inject every (tag, body) pair, surface
+    a diagnostic for each missing marker pair, append (or update) a
+    single FileWrite if any tag actually changed the text.
+
+    Phase ordering: earlier phases (e.g. ``_phase_categories``) may have
+    queued a FileWrite for the same path. Reading from disk in that case
+    would be stale — the later phase would inject into the PRE-cleanup
+    text and the post-cleanup IMPORT/EXPORT block would be lost on
+    apply. Use the pending write's content when present, and replace
+    it in-place so duplicate FileWrites for the same path don't show
+    up in ``apply_writes``."""
+    pending_idx: Optional[int] = None
+    for i in range(len(writes) - 1, -1, -1):
+        if writes[i].path == cpp_path:
+            pending_idx = i
+            break
+
+    if pending_idx is not None:
+        text = writes[pending_idx].content
+    elif os.path.exists(cpp_path):
+        with open(cpp_path, "r", encoding="utf-8") as f:
+            text = f.read()
+    else:
+        _diag_add(
+            f"[diagnostic] {diag_source}: host .cpp '{cpp_path}' does "
+            f"not exist; no blocks injected.",
+            source=diag_source,
+        )
+        return
+
+    new_text = text
+    for tag, body in tagged_bodies:
+        new_text, ok = inject_between_tags(
+            new_text, tag, body, balance_source=diag_source,
+        )
+        if not ok:
+            _diag_add(
+                f"[diagnostic] {diag_source}: '{cpp_path}' is missing "
+                f"the CODEGEN_{tag}_START/END marker pair.",
+                source=diag_source,
+            )
+    if new_text == text:
+        return
+    if pending_idx is not None:
+        writes[pending_idx] = FileWrite(path=cpp_path, content=new_text)
+    else:
+        writes.append(FileWrite(path=cpp_path, content=new_text))
diff --git a/Scripts/codegen/_vendored/_VENDORED_FROM.md b/Scripts/codegen/_vendored/_VENDORED_FROM.md
new file mode 100644
index 0000000..e24f132
--- /dev/null
+++ b/Scripts/codegen/_vendored/_VENDORED_FROM.md
@@ -0,0 +1,17 @@
+# Vendored sources
+
+`sync_vendored.py` reads this manifest and rewrites it deterministically
+on every run (stable column order, sorted by `local_dest`). Re-running on
+the same upstream SHA produces zero diff — keeps the manifest out of
+merge-conflict territory.
+
+To bump a vendored file, edit its `upstream_sha` (or replace with `main`
+for an unpinned re-fetch), then run:
+
+```
+python Scripts/codegen/sync_vendored.py
+```
+
+| file | upstream_url | upstream_sha | retrieved_date | license | local_dest |
+| ---- | ------------ | ------------ | -------------- | ------- | ---------- |
+| mjspecmacro.h | https://raw.githubusercontent.com/google-deepmind/mujoco/{sha}/include/mujoco/mjspecmacro.h | main | 2026-05-29 | Apache-2.0 | Scripts/codegen/_vendored/mjspecmacro.h |
diff --git a/Scripts/codegen/_vendored/__init__.py b/Scripts/codegen/_vendored/__init__.py
new file mode 100644
index 0000000..388c395
--- /dev/null
+++ b/Scripts/codegen/_vendored/__init__.py
@@ -0,0 +1,12 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""
+Vendored third-party source files needed by URLab codegen.
+
+Files here are auto-fetched by ``Scripts/codegen/sync_vendored.py`` from
+the URLs listed in ``_VENDORED_FROM.md``. Edit those source URLs / SHAs
+in the manifest; the script downloads + writes them verbatim.
+
+DO NOT edit the .h / .cc files in this directory by hand — they get
+clobbered on the next ``sync_vendored.py`` run. URLab-side changes go
+in the codegen modules that consume them.
+"""
diff --git a/Scripts/codegen/_vendored/ast_nodes.py b/Scripts/codegen/_vendored/ast_nodes.py
new file mode 100644
index 0000000..f85cc9a
--- /dev/null
+++ b/Scripts/codegen/_vendored/ast_nodes.py
@@ -0,0 +1,154 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""
+Lightweight AST-node dataclasses used by ``build_introspect_snapshot.py``
+to project clang.cindex AST cursors into a serialisable shape.
+
+URLab-side rather than pulled from upstream because (a) the upstream
+``clang.cindex`` types don't pickle cleanly across libclang versions and
+(b) the snapshot only needs a small subset of node info. Keeping the
+projection in one place means the schema is auditable from JSON, and
+test fixtures can be hand-built without needing a real libclang install.
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field, asdict
+from typing import Any, Dict, List, Optional
+
+
+@dataclass
+class CFunctionParam:
+    name:       str
+    c_type:     str
+    array_dim:  Optional[int] = None       # None for scalar / pointer
+
+    def to_dict(self) -> Dict[str, Any]:
+        return asdict(self)
+
+
+@dataclass
+class CFunction:
+    name:        str
+    return_type: str
+    params:      List[CFunctionParam] = field(default_factory=list)
+    doc:         str = ""
+    file:        str = ""                  # source path
+    line:        int = 0
+
+    def to_dict(self) -> Dict[str, Any]:
+        return {
+            "name":        self.name,
+            "return_type": self.return_type,
+            "params":      [p.to_dict() for p in self.params],
+            "doc":         self.doc,
+            "file":        self.file,
+            "line":        self.line,
+        }
+
+
+@dataclass
+class CEnumMember:
+    name:  str
+    value: int
+    doc:   str = ""
+
+
+@dataclass
+class CEnum:
+    name:       str
+    members:    List[CEnumMember] = field(default_factory=list)
+    underlying_type: Optional[str] = None
+    doc:        str = ""
+    file:       str = ""
+    line:       int = 0
+
+    def to_dict(self) -> Dict[str, Any]:
+        return {
+            "name":            self.name,
+            "members":         {m.name: m.value for m in self.members},
+            "member_docs":     {m.name: m.doc for m in self.members if m.doc},
+            "underlying_type": self.underlying_type,
+            "doc":             self.doc,
+            "file":            self.file,
+            "line":            self.line,
+        }
+
+
+@dataclass
+class CStructField:
+    name:       str
+    c_type:     str
+    array_dim:  Optional[int] = None
+    is_pointer: bool = False
+    doc:        str = ""
+
+
+@dataclass
+class CStruct:
+    name:       str
+    fields:     List[CStructField] = field(default_factory=list)
+    doc:        str = ""
+    file:       str = ""
+    line:       int = 0
+
+    def to_dict(self) -> Dict[str, Any]:
+        return {
+            "name":   self.name,
+            "fields": [
+                {
+                    "name":       f.name,
+                    "c_type":     f.c_type,
+                    "array_dim":  f.array_dim,
+                    "is_pointer": f.is_pointer,
+                    "doc":        f.doc,
+                }
+                for f in self.fields
+            ],
+            "doc":  self.doc,
+            "file": self.file,
+            "line": self.line,
+        }
+
+
+@dataclass
+class CDefine:
+    name:  str
+    value: str
+    doc:   str = ""
+    file:  str = ""
+    line:  int = 0
+
+
+@dataclass
+class IntrospectSnapshot:
+    """Top-level container for the clang-AST scrape output. Serialised
+    to ``Scripts/introspect_snapshot.json``. snapshot_version bumps on
+    schema-incompatible changes.
+
+    ``hand_enums`` mirrors ``enums`` but covers URLab's hand-rolled
+    ``EMj*`` enums under ``Source/URLab/Public``. They live in a
+    separate field so the MuJoCo mjt* lookup (value_map.mj_const drift
+    checks) doesn't accidentally collide with a future EMj* name."""
+    snapshot_version: int = 1
+    header_hash:      str = ""
+    functions:        Dict[str, CFunction] = field(default_factory=dict)
+    enums:            Dict[str, CEnum] = field(default_factory=dict)
+    structs:          Dict[str, CStruct] = field(default_factory=dict)
+    defines:          Dict[str, CDefine] = field(default_factory=dict)
+    hand_enums:       Dict[str, CEnum] = field(default_factory=dict)
+
+    def to_dict(self) -> Dict[str, Any]:
+        return {
+            "_meta": {
+                "snapshot_version": self.snapshot_version,
+                "header_hash":      self.header_hash,
+            },
+            "functions":  {n: f.to_dict() for n, f in self.functions.items()},
+            "enums":      {n: e.to_dict() for n, e in self.enums.items()},
+            "structs":    {n: s.to_dict() for n, s in self.structs.items()},
+            "defines":    {
+                n: {"value": d.value, "doc": d.doc, "file": d.file, "line": d.line}
+                for n, d in self.defines.items()
+            },
+            "hand_enums": {n: e.to_dict() for n, e in self.hand_enums.items()},
+        }
diff --git a/Scripts/codegen/_vendored/mjspecmacro.h b/Scripts/codegen/_vendored/mjspecmacro.h
new file mode 100644
index 0000000..81e861e
--- /dev/null
+++ b/Scripts/codegen/_vendored/mjspecmacro.h
@@ -0,0 +1,603 @@
+// Copyright 2026 DeepMind Technologies Limited
+//
+// 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 MUJOCO_MJSPECMACRO_H_
+#define MUJOCO_MJSPECMACRO_H_
+
+
+//-------------------------------- mjsElement ------------------------------------------------------
+
+#define MJSELEMENT_FIELDS       \
+    X( mjtObj,   elemtype,  1 ) \
+    X( uint64_t, signature, 1 )
+
+
+//-------------------------------- mjsCompiler -----------------------------------------------------
+
+#define MJSCOMPILER_FIELDS                  \
+    X   ( mjtByte,   autolimits,        1 ) \
+    X   ( double,    boundmass,         1 ) \
+    X   ( double,    boundinertia,      1 ) \
+    X   ( double,    settotalmass,      1 ) \
+    X   ( mjtByte,   balanceinertia,    1 ) \
+    X   ( mjtByte,   fitaabb,           1 ) \
+    X   ( mjtByte,   degree,            1 ) \
+    XVEC( char,      eulerseq,          3 ) \
+    X   ( mjtByte,   discardvisual,     1 ) \
+    X   ( mjtByte,   usethread,         1 ) \
+    X   ( mjtByte,   fusestatic,        1 ) \
+    X   ( int,       inertiafromgeom,   1 ) \
+    XVEC( int,       inertiagrouprange, 2 ) \
+    X   ( mjtByte,   saveinertial,      1 ) \
+    X   ( int,       alignfree,         1 ) \
+    X   ( mjLROpt,   LRopt,             1 ) \
+    X   ( mjString*, meshdir,           1 ) \
+    X   ( mjString*, texturedir,        1 )
+
+
+//-------------------------------- mjSpec ----------------------------------------------------------
+
+#define MJSPEC_FIELDS                          \
+    X( mjsElement*, element,               1 ) \
+    X( mjString*,   modelname,             1 ) \
+    X( mjsCompiler, compiler,              1 ) \
+    X( mjtByte,     strippath,             1 ) \
+    X( mjOption,    option,                1 ) \
+    X( mjVisual,    visual,                1 ) \
+    X( mjStatistic, stat,                  1 ) \
+    X( mjtSize,     memory,                1 ) \
+    X( int,         nemax,                 1 ) \
+    X( int,         nuserdata,             1 ) \
+    X( int,         nuser_body,            1 ) \
+    X( int,         nuser_jnt,             1 ) \
+    X( int,         nuser_geom,            1 ) \
+    X( int,         nuser_site,            1 ) \
+    X( int,         nuser_cam,             1 ) \
+    X( int,         nuser_tendon,          1 ) \
+    X( int,         nuser_actuator,        1 ) \
+    X( int,         nuser_sensor,          1 ) \
+    X( int,         nkey,                  1 ) \
+    X( int,         njmax,                 1 ) \
+    X( int,         nconmax,               1 ) \
+    X( mjtSize,     nstack,                1 ) \
+    X( mjString*,   comment,               1 ) \
+    X( mjString*,   modelfiledir,          1 ) \
+    X( mjtByte,     hasImplicitPluginElem, 1 )
+
+
+//-------------------------------- mjsOrientation --------------------------------------------------
+
+#define MJSORIENTATION_FIELDS            \
+    X   ( mjtOrientation, type,      1 ) \
+    XVEC( double,         axisangle, 4 ) \
+    XVEC( double,         xyaxes,    6 ) \
+    XVEC( double,         zaxis,     3 ) \
+    XVEC( double,         euler,     3 )
+
+
+//-------------------------------- mjsPlugin -------------------------------------------------------
+
+#define MJSPLUGIN_FIELDS             \
+    X( mjsElement*, element,     1 ) \
+    X( mjString*,   name,        1 ) \
+    X( mjString*,   plugin_name, 1 ) \
+    X( mjtByte,     active,      1 ) \
+    X( mjString*,   info,        1 )
+
+
+//-------------------------------- mjsBody ---------------------------------------------------------
+
+#define MJSBODY_FIELDS                           \
+    X   ( mjsElement*,     element,          1 ) \
+    X   ( mjString*,       childclass,       1 ) \
+    XVEC( double,          pos,              3 ) \
+    XVEC( double,          quat,             4 ) \
+    X   ( mjsOrientation,  alt,              1 ) \
+    X   ( double,          mass,             1 ) \
+    XVEC( double,          ipos,             3 ) \
+    XVEC( double,          iquat,            4 ) \
+    XVEC( double,          inertia,          3 ) \
+    X   ( mjsOrientation,  ialt,             1 ) \
+    XVEC( double,          fullinertia,      6 ) \
+    X   ( mjtByte,         mocap,            1 ) \
+    X   ( double,          gravcomp,         1 ) \
+    X   ( mjtSleepPolicy,  sleep,            1 ) \
+    X   ( mjDoubleVec*,    userdata,         1 ) \
+    X   ( mjtByte,         explicitinertial, 1 ) \
+    X   ( mjsPlugin,       plugin,           1 ) \
+    X   ( mjString*,       info,             1 )
+
+
+//-------------------------------- mjsFrame --------------------------------------------------------
+
+#define MJSFRAME_FIELDS                   \
+    X   ( mjsElement*,    element,    1 ) \
+    X   ( mjString*,      childclass, 1 ) \
+    XVEC( double,         pos,        3 ) \
+    XVEC( double,         quat,       4 ) \
+    X   ( mjsOrientation, alt,        1 ) \
+    X   ( mjString*,      info,       1 )
+
+
+//-------------------------------- mjsJoint --------------------------------------------------------
+
+#define MJSJOINT_FIELDS                               \
+    X   ( mjsElement*,   element,         1         ) \
+    X   ( mjtJoint,      type,            1         ) \
+    XVEC( double,        pos,             3         ) \
+    XVEC( double,        axis,            3         ) \
+    X   ( double,        ref,             1         ) \
+    X   ( int,           align,           1         ) \
+    XVEC( double,        stiffness,       mjNPOLY+1 ) \
+    X   ( double,        springref,       1         ) \
+    XVEC( double,        springdamper,    2         ) \
+    X   ( int,           limited,         1         ) \
+    XVEC( double,        range,           2         ) \
+    X   ( double,        margin,          1         ) \
+    XVEC( mjtNum,        solref_limit,    mjNREF    ) \
+    XVEC( mjtNum,        solimp_limit,    mjNIMP    ) \
+    X   ( int,           actfrclimited,   1         ) \
+    XVEC( double,        actfrcrange,     2         ) \
+    X   ( double,        armature,        1         ) \
+    XVEC( double,        damping,         mjNPOLY+1 ) \
+    X   ( double,        frictionloss,    1         ) \
+    XVEC( mjtNum,        solref_friction, mjNREF    ) \
+    XVEC( mjtNum,        solimp_friction, mjNIMP    ) \
+    X   ( int,           group,           1         ) \
+    X   ( mjtByte,       actgravcomp,     1         ) \
+    X   ( mjDoubleVec*,  userdata,        1         ) \
+    X   ( mjString*,     info,            1         )
+
+
+//-------------------------------- mjsGeom ---------------------------------------------------------
+
+#define MJSGEOM_FIELDS                              \
+    X   ( mjsElement*,    element,         1      ) \
+    X   ( mjtGeom,        type,            1      ) \
+    XVEC( double,         pos,             3      ) \
+    XVEC( double,         quat,            4      ) \
+    X   ( mjsOrientation, alt,             1      ) \
+    XVEC( double,         fromto,          6      ) \
+    XVEC( double,         size,            3      ) \
+    X   ( int,            contype,         1      ) \
+    X   ( int,            conaffinity,     1      ) \
+    X   ( int,            condim,          1      ) \
+    X   ( int,            priority,        1      ) \
+    XVEC( double,         friction,        3      ) \
+    X   ( double,         solmix,          1      ) \
+    XVEC( mjtNum,         solref,          mjNREF ) \
+    XVEC( mjtNum,         solimp,          mjNIMP ) \
+    X   ( double,         margin,          1      ) \
+    X   ( double,         gap,             1      ) \
+    X   ( double,         mass,            1      ) \
+    X   ( double,         density,         1      ) \
+    X   ( mjtGeomInertia, typeinertia,     1      ) \
+    X   ( mjtNum,         fluid_ellipsoid, 1      ) \
+    XVEC( mjtNum,         fluid_coefs,     5      ) \
+    X   ( mjString*,      material,        1      ) \
+    XVEC( float,          rgba,            4      ) \
+    X   ( int,            group,           1      ) \
+    X   ( mjString*,      hfieldname,      1      ) \
+    X   ( mjString*,      meshname,        1      ) \
+    X   ( double,         fitscale,        1      ) \
+    X   ( mjDoubleVec*,   userdata,        1      ) \
+    X   ( mjsPlugin,      plugin,          1      ) \
+    X   ( mjString*,      info,            1      )
+
+
+//-------------------------------- mjsSite ---------------------------------------------------------
+
+#define MJSSITE_FIELDS                  \
+    X   ( mjsElement*,    element,  1 ) \
+    XVEC( double,         pos,      3 ) \
+    XVEC( double,         quat,     4 ) \
+    X   ( mjsOrientation, alt,      1 ) \
+    XVEC( double,         fromto,   6 ) \
+    XVEC( double,         size,     3 ) \
+    X   ( mjtGeom,        type,     1 ) \
+    X   ( mjString*,      material, 1 ) \
+    X   ( int,            group,    1 ) \
+    XVEC( float,          rgba,     4 ) \
+    X   ( mjDoubleVec*,   userdata, 1 ) \
+    X   ( mjString*,      info,     1 )
+
+
+//-------------------------------- mjsCamera -------------------------------------------------------
+
+#define MJSCAMERA_FIELDS                        \
+    X   ( mjsElement*,    element,          1 ) \
+    XVEC( double,         pos,              3 ) \
+    XVEC( double,         quat,             4 ) \
+    X   ( mjsOrientation, alt,              1 ) \
+    X   ( mjtCamLight,    mode,             1 ) \
+    X   ( mjString*,      targetbody,       1 ) \
+    X   ( mjtProjection,  proj,             1 ) \
+    XVEC( int,            resolution,       2 ) \
+    X   ( int,            output,           1 ) \
+    X   ( double,         fovy,             1 ) \
+    X   ( double,         ipd,              1 ) \
+    XVEC( float,          intrinsic,        4 ) \
+    XVEC( float,          sensor_size,      2 ) \
+    XVEC( float,          focal_length,     2 ) \
+    XVEC( float,          focal_pixel,      2 ) \
+    XVEC( float,          principal_length, 2 ) \
+    XVEC( float,          principal_pixel,  2 ) \
+    X   ( mjDoubleVec*,   userdata,         1 ) \
+    X   ( mjString*,      info,             1 )
+
+
+//-------------------------------- mjsLight --------------------------------------------------------
+
+#define MJSLIGHT_FIELDS                  \
+    X   ( mjsElement*,  element,     1 ) \
+    XVEC( double,       pos,         3 ) \
+    XVEC( double,       dir,         3 ) \
+    X   ( mjtCamLight,  mode,        1 ) \
+    X   ( mjString*,    targetbody,  1 ) \
+    X   ( mjtByte,      active,      1 ) \
+    X   ( mjtLightType, type,        1 ) \
+    X   ( mjString*,    texture,     1 ) \
+    X   ( mjtByte,      castshadow,  1 ) \
+    X   ( float,        bulbradius,  1 ) \
+    X   ( float,        intensity,   1 ) \
+    X   ( float,        range,       1 ) \
+    XVEC( float,        attenuation, 3 ) \
+    X   ( float,        cutoff,      1 ) \
+    X   ( float,        exponent,    1 ) \
+    XVEC( float,        ambient,     3 ) \
+    XVEC( float,        diffuse,     3 ) \
+    XVEC( float,        specular,    3 ) \
+    X   ( mjString*,    info,        1 )
+
+
+//-------------------------------- mjsFlex ---------------------------------------------------------
+
+#define MJSFLEX_FIELDS                           \
+    X   ( mjsElement*,   element,       1      ) \
+    X   ( int,           contype,       1      ) \
+    X   ( int,           conaffinity,   1      ) \
+    X   ( int,           condim,        1      ) \
+    X   ( int,           priority,      1      ) \
+    XVEC( double,        friction,      3      ) \
+    X   ( double,        solmix,        1      ) \
+    XVEC( mjtNum,        solref,        mjNREF ) \
+    XVEC( mjtNum,        solimp,        mjNIMP ) \
+    X   ( double,        margin,        1      ) \
+    X   ( double,        gap,           1      ) \
+    X   ( int,           dim,           1      ) \
+    X   ( double,        radius,        1      ) \
+    XVEC( double,        size,          3      ) \
+    X   ( mjtByte,       internal,      1      ) \
+    X   ( mjtByte,       flatskin,      1      ) \
+    X   ( int,           selfcollide,   1      ) \
+    X   ( int,           passive,       1      ) \
+    X   ( int,           activelayers,  1      ) \
+    X   ( int,           group,         1      ) \
+    X   ( double,        edgestiffness, 1      ) \
+    X   ( double,        edgedamping,   1      ) \
+    XVEC( float,         rgba,          4      ) \
+    X   ( mjString*,     material,      1      ) \
+    X   ( double,        young,         1      ) \
+    X   ( double,        poisson,       1      ) \
+    X   ( double,        damping,       1      ) \
+    X   ( double,        thickness,     1      ) \
+    X   ( int,           elastic2d,     1      ) \
+    XVEC( int,           cellcount,     3      ) \
+    X   ( int,           order,         1      ) \
+    X   ( mjStringVec*,  nodebody,      1      ) \
+    X   ( mjStringVec*,  vertbody,      1      ) \
+    X   ( mjDoubleVec*,  node,          1      ) \
+    X   ( mjDoubleVec*,  vert,          1      ) \
+    X   ( mjIntVec*,     elem,          1      ) \
+    X   ( mjFloatVec*,   texcoord,      1      ) \
+    X   ( mjIntVec*,     elemtexcoord,  1      ) \
+    X   ( mjString*,     info,          1      )
+
+
+//-------------------------------- mjsMesh ---------------------------------------------------------
+
+#define MJSMESH_FIELDS                           \
+    X   ( mjsElement*,     element,          1 ) \
+    X   ( mjString*,       content_type,     1 ) \
+    X   ( mjString*,       file,             1 ) \
+    XVEC( double,          refpos,           3 ) \
+    XVEC( double,          refquat,          4 ) \
+    XVEC( double,          scale,            3 ) \
+    X   ( mjtMeshInertia,  inertia,          1 ) \
+    X   ( mjtByte,         smoothnormal,     1 ) \
+    X   ( mjtByte,         needsdf,          1 ) \
+    X   ( int,             maxhullvert,      1 ) \
+    X   ( mjFloatVec*,     uservert,         1 ) \
+    X   ( mjFloatVec*,     usernormal,       1 ) \
+    X   ( mjFloatVec*,     usertexcoord,     1 ) \
+    X   ( mjIntVec*,       userface,         1 ) \
+    X   ( mjIntVec*,       userfacenormal,   1 ) \
+    X   ( mjIntVec*,       userfacetexcoord, 1 ) \
+    X   ( mjsPlugin,       plugin,           1 ) \
+    X   ( mjString*,       material,         1 ) \
+    X   ( int,             octree_maxdepth,  1 ) \
+    X   ( mjString*,       info,             1 )
+
+
+//-------------------------------- mjsHField -------------------------------------------------------
+
+#define MJSHFIELD_FIELDS                  \
+    X   ( mjsElement*,  element,      1 ) \
+    X   ( mjString*,    content_type, 1 ) \
+    X   ( mjString*,    file,         1 ) \
+    XVEC( double,       size,         4 ) \
+    X   ( int,          nrow,         1 ) \
+    X   ( int,          ncol,         1 ) \
+    X   ( mjFloatVec*,  userdata,     1 ) \
+    X   ( mjString*,    info,         1 )
+
+
+//-------------------------------- mjsSkin ---------------------------------------------------------
+
+#define MJSSKIN_FIELDS                    \
+    X   ( mjsElement*,    element,    1 ) \
+    X   ( mjString*,      file,       1 ) \
+    X   ( mjString*,      material,   1 ) \
+    XVEC( float,          rgba,       4 ) \
+    X   ( float,          inflate,    1 ) \
+    X   ( int,            group,      1 ) \
+    X   ( mjFloatVec*,    vert,       1 ) \
+    X   ( mjFloatVec*,    texcoord,   1 ) \
+    X   ( mjIntVec*,      face,       1 ) \
+    X   ( mjStringVec*,   bodyname,   1 ) \
+    X   ( mjFloatVec*,    bindpos,    1 ) \
+    X   ( mjFloatVec*,    bindquat,   1 ) \
+    X   ( mjIntVecVec*,   vertid,     1 ) \
+    X   ( mjFloatVecVec*, vertweight, 1 ) \
+    X   ( mjString*,      info,       1 )
+
+
+//-------------------------------- mjsTexture ------------------------------------------------------
+
+#define MJSTEXTURE_FIELDS                    \
+    X   ( mjsElement*,    element,      1  ) \
+    X   ( mjtTexture,     type,         1  ) \
+    X   ( mjtColorSpace,  colorspace,   1  ) \
+    X   ( int,            builtin,      1  ) \
+    X   ( int,            mark,         1  ) \
+    XVEC( double,         rgb1,         3  ) \
+    XVEC( double,         rgb2,         3  ) \
+    XVEC( double,         markrgb,      3  ) \
+    X   ( double,         random,       1  ) \
+    X   ( int,            height,       1  ) \
+    X   ( int,            width,        1  ) \
+    X   ( int,            nchannel,     1  ) \
+    X   ( mjString*,      content_type, 1  ) \
+    X   ( mjString*,      file,         1  ) \
+    XVEC( int,            gridsize,     2  ) \
+    XVEC( char,           gridlayout,   12 ) \
+    X   ( mjStringVec*,   cubefiles,    1  ) \
+    X   ( mjByteVec*,     data,         1  ) \
+    X   ( mjtByte,        hflip,        1  ) \
+    X   ( mjtByte,        vflip,        1  ) \
+    X   ( mjString*,      info,         1  )
+
+
+//-------------------------------- mjsMaterial -----------------------------------------------------
+
+#define MJSMATERIAL_FIELDS               \
+    X   ( mjsElement*,  element,     1 ) \
+    X   ( mjStringVec*, textures,    1 ) \
+    X   ( mjtByte,      texuniform,  1 ) \
+    XVEC( float,        texrepeat,   2 ) \
+    X   ( float,        emission,    1 ) \
+    X   ( float,        specular,    1 ) \
+    X   ( float,        shininess,   1 ) \
+    X   ( float,        reflectance, 1 ) \
+    X   ( float,        metallic,    1 ) \
+    X   ( float,        roughness,   1 ) \
+    XVEC( float,        rgba,        4 ) \
+    X   ( mjString*,    info,        1 )
+
+
+//-------------------------------- mjsPair ---------------------------------------------------------
+
+#define MJSPAIR_FIELDS                          \
+    X   ( mjsElement*, element,        1      ) \
+    X   ( mjString*,   geomname1,      1      ) \
+    X   ( mjString*,   geomname2,      1      ) \
+    X   ( int,         condim,         1      ) \
+    XVEC( mjtNum,      solref,         mjNREF ) \
+    XVEC( mjtNum,      solreffriction, mjNREF ) \
+    XVEC( mjtNum,      solimp,         mjNIMP ) \
+    X   ( double,      margin,         1      ) \
+    X   ( double,      gap,            1      ) \
+    XVEC( double,      friction,       5      ) \
+    X   ( mjString*,   info,           1      )
+
+
+//-------------------------------- mjsExclude ------------------------------------------------------
+
+#define MJSEXCLUDE_FIELDS          \
+    X( mjsElement*, element,   1 ) \
+    X( mjString*,   bodyname1, 1 ) \
+    X( mjString*,   bodyname2, 1 ) \
+    X( mjString*,   info,      1 )
+
+
+//-------------------------------- mjsEquality -----------------------------------------------------
+
+#define MJSEQUALITY_FIELDS                  \
+    X   ( mjsElement*, element, 1         ) \
+    X   ( mjtEq,       type,    1         ) \
+    XVEC( double,      data,    mjNEQDATA ) \
+    X   ( mjtByte,     active,  1         ) \
+    X   ( mjString*,   name1,   1         ) \
+    X   ( mjString*,   name2,   1         ) \
+    X   ( mjtObj,      objtype, 1         ) \
+    XVEC( mjtNum,      solref,  mjNREF    ) \
+    XVEC( mjtNum,      solimp,  mjNIMP    ) \
+    X   ( mjString*,   info,    1         )
+
+
+//-------------------------------- mjsTendon -------------------------------------------------------
+
+#define MJSTENDON_FIELDS                              \
+    X   ( mjsElement*,   element,         1         ) \
+    XVEC( double,        stiffness,       mjNPOLY+1 ) \
+    XVEC( double,        springlength,    2         ) \
+    XVEC( double,        damping,         mjNPOLY+1 ) \
+    X   ( double,        frictionloss,    1         ) \
+    XVEC( mjtNum,        solref_friction, mjNREF    ) \
+    XVEC( mjtNum,        solimp_friction, mjNIMP    ) \
+    X   ( double,        armature,        1         ) \
+    X   ( int,           limited,         1         ) \
+    X   ( int,           actfrclimited,   1         ) \
+    XVEC( double,        range,           2         ) \
+    XVEC( double,        actfrcrange,     2         ) \
+    X   ( double,        margin,          1         ) \
+    XVEC( mjtNum,        solref_limit,    mjNREF    ) \
+    XVEC( mjtNum,        solimp_limit,    mjNIMP    ) \
+    X   ( mjString*,     material,        1         ) \
+    X   ( double,        width,           1         ) \
+    XVEC( float,         rgba,            4         ) \
+    X   ( int,           group,           1         ) \
+    X   ( mjDoubleVec*,  userdata,        1         ) \
+    X   ( mjString*,     info,            1         )
+
+
+//-------------------------------- mjsWrap ---------------------------------------------------------
+
+#define MJSWRAP_FIELDS           \
+    X( mjsElement*, element, 1 ) \
+    X( mjtWrap,     type,    1 ) \
+    X( mjString*,   info,    1 )
+
+
+//-------------------------------- mjsActuator -----------------------------------------------------
+
+#define MJSACTUATOR_FIELDS                          \
+    X   ( mjsElement*,   element,       1         ) \
+    X   ( mjtGain,       gaintype,      1         ) \
+    XVEC( double,        gainprm,       mjNGAIN   ) \
+    X   ( mjtBias,       biastype,      1         ) \
+    XVEC( double,        biasprm,       mjNGAIN   ) \
+    X   ( mjtDyn,        dyntype,       1         ) \
+    XVEC( double,        dynprm,        mjNDYN    ) \
+    X   ( int,           actdim,        1         ) \
+    X   ( mjtByte,       actearly,      1         ) \
+    X   ( mjtTrn,        trntype,       1         ) \
+    XVEC( double,        gear,          6         ) \
+    X   ( mjString*,     target,        1         ) \
+    X   ( mjString*,     refsite,       1         ) \
+    X   ( mjString*,     slidersite,    1         ) \
+    X   ( double,        cranklength,   1         ) \
+    XVEC( double,        lengthrange,   2         ) \
+    X   ( double,        inheritrange,  1         ) \
+    XVEC( double,        damping,       mjNPOLY+1 ) \
+    X   ( double,        armature,      1         ) \
+    X   ( int,           ctrllimited,   1         ) \
+    XVEC( double,        ctrlrange,     2         ) \
+    X   ( int,           forcelimited,  1         ) \
+    XVEC( double,        forcerange,    2         ) \
+    X   ( int,           actlimited,    1         ) \
+    XVEC( double,        actrange,      2         ) \
+    X   ( int,           group,         1         ) \
+    X   ( int,           nsample,       1         ) \
+    X   ( int,           interp,        1         ) \
+    X   ( double,        delay,         1         ) \
+    X   ( mjDoubleVec*,  userdata,      1         ) \
+    X   ( mjsPlugin,     plugin,        1         ) \
+    X   ( mjString*,     info,          1         )
+
+
+//-------------------------------- mjsSensor -------------------------------------------------------
+
+#define MJSSENSOR_FIELDS                      \
+    X   ( mjsElement*,   element,   1       ) \
+    X   ( mjtSensor,     type,      1       ) \
+    X   ( mjtObj,        objtype,   1       ) \
+    X   ( mjString*,     objname,   1       ) \
+    X   ( mjtObj,        reftype,   1       ) \
+    X   ( mjString*,     refname,   1       ) \
+    XVEC( int,           intprm,    mjNSENS ) \
+    X   ( mjtDataType,   datatype,  1       ) \
+    X   ( mjtStage,      needstage, 1       ) \
+    X   ( int,           dim,       1       ) \
+    X   ( double,        cutoff,    1       ) \
+    X   ( double,        noise,     1       ) \
+    X   ( int,           nsample,   1       ) \
+    X   ( int,           interp,    1       ) \
+    X   ( double,        delay,     1       ) \
+    XVEC( double,        interval,  2       ) \
+    X   ( mjDoubleVec*,  userdata,  1       ) \
+    X   ( mjsPlugin,     plugin,    1       ) \
+    X   ( mjString*,     info,      1       )
+
+
+//-------------------------------- mjsNumeric ------------------------------------------------------
+
+#define MJSNUMERIC_FIELDS         \
+    X( mjsElement*,  element, 1 ) \
+    X( mjDoubleVec*, data,    1 ) \
+    X( int,          size,    1 ) \
+    X( mjString*,    info,    1 )
+
+
+//-------------------------------- mjsText ---------------------------------------------------------
+
+#define MJSTEXT_FIELDS           \
+    X( mjsElement*, element, 1 ) \
+    X( mjString*,   data,    1 ) \
+    X( mjString*,   info,    1 )
+
+
+//-------------------------------- mjsTuple --------------------------------------------------------
+
+#define MJSTUPLE_FIELDS           \
+    X( mjsElement*,  element, 1 ) \
+    X( mjIntVec*,    objtype, 1 ) \
+    X( mjStringVec*, objname, 1 ) \
+    X( mjDoubleVec*, objprm,  1 ) \
+    X( mjString*,    info,    1 )
+
+
+//-------------------------------- mjsKey ----------------------------------------------------------
+
+#define MJSKEY_FIELDS             \
+    X( mjsElement*,  element, 1 ) \
+    X( double,       time,    1 ) \
+    X( mjDoubleVec*, qpos,    1 ) \
+    X( mjDoubleVec*, qvel,    1 ) \
+    X( mjDoubleVec*, act,     1 ) \
+    X( mjDoubleVec*, mpos,    1 ) \
+    X( mjDoubleVec*, mquat,   1 ) \
+    X( mjDoubleVec*, ctrl,    1 ) \
+    X( mjString*,    info,    1 )
+
+
+//-------------------------------- mjsDefault ------------------------------------------------------
+
+#define MJSDEFAULT_FIELDS          \
+    X( mjsElement*,  element,  1 ) \
+    X( mjsJoint*,    joint,    1 ) \
+    X( mjsGeom*,     geom,     1 ) \
+    X( mjsSite*,     site,     1 ) \
+    X( mjsCamera*,   camera,   1 ) \
+    X( mjsLight*,    light,    1 ) \
+    X( mjsFlex*,     flex,     1 ) \
+    X( mjsMesh*,     mesh,     1 ) \
+    X( mjsMaterial*, material, 1 ) \
+    X( mjsPair*,     pair,     1 ) \
+    X( mjsEquality*, equality, 1 ) \
+    X( mjsTendon*,   tendon,   1 ) \
+    X( mjsActuator*, actuator, 1 )
+
+
+#endif  // MUJOCO_MJSPECMACRO_H_
diff --git a/Scripts/codegen/build_introspect_snapshot.py b/Scripts/codegen/build_introspect_snapshot.py
new file mode 100644
index 0000000..1098b81
--- /dev/null
+++ b/Scripts/codegen/build_introspect_snapshot.py
@@ -0,0 +1,487 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""
+clang.cindex-based scrape of MuJoCo headers (mjspec.h + mjmodel.h +
+optionally the vendored mjspecmacro.h) into
+``Scripts/codegen/snapshots/introspect_snapshot.json``. The codegen
+consumes this snapshot as its single source of truth for the MuJoCo C
+API surface.
+
+Captures:
+- function signatures (every MJAPI-tagged declaration in the headers)
+- enum decls with values + per-member doc comments
+- struct decls with fields + per-field doc comments + array dims
+- ``#define`` constants (after preprocessor evaluation when literal)
+
+Usage:
+    python Scripts/codegen/build_introspect_snapshot.py
+        [--mujoco-include /path/to/mujoco/include]
+        [--output Scripts/codegen/snapshots/introspect_snapshot.json]
+        [--libclang /path/to/libclang.dll]      (only if not auto-found)
+
+libclang resolution: respects ``$LIBCLANG_LIBRARY_FILE`` env var; falls
+back to clang.cindex's default search. Set ``--libclang`` to override.
+"""
+
+from __future__ import annotations
+
+import argparse
+import hashlib
+import json
+import os
+import re
+import sys
+from typing import Dict, List
+
+_HERE = os.path.dirname(os.path.abspath(__file__))
+_PLUGIN_ROOT = os.path.normpath(os.path.join(_HERE, "..", ".."))
+
+# Use the local vendored ast_nodes copy.
+sys.path.insert(0, os.path.join(_HERE, "_vendored"))
+from ast_nodes import (  # noqa: E402
+    CFunction,
+    CFunctionParam,
+    CEnum,
+    CEnumMember,
+    CStruct,
+    CStructField,
+    CDefine,
+    IntrospectSnapshot,
+)
+
+
+def _header_hash(paths: List[str]) -> str:
+    """SHA-256 of the concatenated header content. Lets callers skip
+    re-running the snapshot when nothing upstream changed."""
+    h = hashlib.sha256()
+    for p in sorted(paths):
+        if not os.path.exists(p):
+            continue
+        with open(p, "rb") as f:
+            h.update(f.read())
+    return h.hexdigest()
+
+
+def _strip_doc(raw: str) -> str:
+    """Collapse a libclang raw doc comment into one line, stripping
+    leading ``//`` / ``/*`` decoration."""
+    if not raw:
+        return ""
+    out_lines: List[str] = []
+    for line in raw.splitlines():
+        s = line.strip()
+        for prefix in ("///", "//!", "//", "/**", "*/", "*", "/*"):
+            if s.startswith(prefix):
+                s = s[len(prefix):].strip()
+                break
+        if s.endswith("*/"):
+            s = s[:-2].strip()
+        if s:
+            out_lines.append(s)
+    return " ".join(out_lines)
+
+
+def _walk(tu, mujoco_include: str, snapshot: IntrospectSnapshot) -> None:
+    """Visit every cursor in the translation unit. ``mujoco_include`` is
+    the include root; we only capture decls whose file lives under it
+    (so libc / Windows SDK / TinyXML noise is filtered out)."""
+    import clang.cindex as cx  # noqa: E402, local-only
+
+    plugin_root_norm = os.path.normcase(os.path.abspath(_PLUGIN_ROOT))
+
+    def _normalised_path(loc) -> str:
+        """Return the location's file as a forward-slash path relative to
+        the plugin root, so the snapshot is byte-identical across
+        developers (no absolute ``c:\\users\\...`` strings)."""
+        if not loc or not loc.file:
+            return ""
+        abs_path = os.path.normcase(os.path.abspath(loc.file.name))
+        if abs_path.startswith(plugin_root_norm):
+            rel = os.path.relpath(abs_path, plugin_root_norm)
+            return rel.replace("\\", "/")
+        return abs_path.replace("\\", "/")
+
+    # Filter root mirrors _normalised_path: relative-to-plugin-root with
+    # forward slashes.
+    mujoco_abs = os.path.normcase(os.path.abspath(mujoco_include))
+    if mujoco_abs.startswith(plugin_root_norm):
+        norm_root = os.path.relpath(mujoco_abs, plugin_root_norm).replace("\\", "/")
+    else:
+        norm_root = mujoco_abs.replace("\\", "/")
+
+    for cursor in tu.cursor.get_children():
+        fp = _normalised_path(cursor.location)
+        if not fp.startswith(norm_root):
+            continue
+
+        kind = cursor.kind
+
+        if kind == cx.CursorKind.FUNCTION_DECL:
+            params: List[CFunctionParam] = []
+            for arg in cursor.get_arguments():
+                t = arg.type.spelling
+                dim = None
+                if "[" in t and "]" in t:
+                    # e.g. "double [4]" -> dim=4
+                    try:
+                        dim_str = t[t.index("[") + 1:t.index("]")]
+                        if dim_str.strip().isdigit():
+                            dim = int(dim_str)
+                    except ValueError:
+                        pass
+                params.append(CFunctionParam(
+                    name=arg.spelling, c_type=t, array_dim=dim,
+                ))
+            fn = CFunction(
+                name=cursor.spelling,
+                return_type=cursor.result_type.spelling,
+                params=params,
+                doc=_strip_doc(cursor.raw_comment or ""),
+                file=fp,
+                line=cursor.location.line if cursor.location else 0,
+            )
+            snapshot.functions[fn.name] = fn
+
+        elif kind == cx.CursorKind.ENUM_DECL:
+            # Anonymous enums get spelling = "" — skip; we only want
+            # named typedef enums. The C tag name is ``mjtJoint_`` but
+            # callers reference the typedef ``mjtJoint``; normalise here.
+            name = cursor.spelling or cursor.type.spelling
+            if not name or name.startswith("(anonymous"):
+                continue
+            if name.endswith("_"):
+                name = name[:-1]
+            members: List[CEnumMember] = []
+            for child in cursor.get_children():
+                if child.kind == cx.CursorKind.ENUM_CONSTANT_DECL:
+                    members.append(CEnumMember(
+                        name=child.spelling,
+                        value=child.enum_value,
+                        doc=_strip_doc(child.raw_comment or ""),
+                    ))
+            underlying = cursor.enum_type.spelling if cursor.enum_type else None
+            snapshot.enums[name] = CEnum(
+                name=name, members=members,
+                underlying_type=underlying,
+                doc=_strip_doc(cursor.raw_comment or ""),
+                file=fp,
+                line=cursor.location.line if cursor.location else 0,
+            )
+
+        elif kind == cx.CursorKind.STRUCT_DECL:
+            name = cursor.spelling or cursor.type.spelling
+            if not name:
+                continue
+            # Strip trailing underscore from C tag names ("mjsBody_" -> "mjsBody").
+            if name.endswith("_"):
+                name = name[:-1]
+            fields: List[CStructField] = []
+            for child in cursor.get_children():
+                if child.kind != cx.CursorKind.FIELD_DECL:
+                    continue
+                t = child.type.spelling
+                dim = None
+                is_ptr = "*" in t
+                if "[" in t and "]" in t:
+                    try:
+                        dim_str = t[t.index("[") + 1:t.index("]")]
+                        if dim_str.strip().isdigit():
+                            dim = int(dim_str)
+                    except ValueError:
+                        pass
+                fields.append(CStructField(
+                    name=child.spelling,
+                    c_type=t,
+                    array_dim=dim,
+                    is_pointer=is_ptr,
+                    doc=_strip_doc(child.raw_comment or ""),
+                ))
+            snapshot.structs[name] = CStruct(
+                name=name, fields=fields,
+                doc=_strip_doc(cursor.raw_comment or ""),
+                file=fp,
+                line=cursor.location.line if cursor.location else 0,
+            )
+
+        elif kind == cx.CursorKind.MACRO_DEFINITION:
+            name = cursor.spelling
+            # Skip function-like macros; libclang doesn't expand them.
+            # Skip header guards.
+            if name.endswith("_H_") or name.startswith("__"):
+                continue
+            # Extract tokens between the cursor's extent to get the value.
+            tokens = list(tu.get_tokens(extent=cursor.extent))
+            if len(tokens) < 2:
+                continue
+            value = " ".join(t.spelling for t in tokens[1:])
+            snapshot.defines[name] = CDefine(
+                name=name, value=value,
+                doc=_strip_doc(cursor.raw_comment or ""),
+                file=fp,
+                line=cursor.location.line if cursor.location else 0,
+            )
+
+
+# ---------------------------------------------------------------------------
+# URLab hand-enum scrape
+#
+# UE hand-rolled ``enum class EMj* : uint8 { ... }`` declarations live in
+# C++ headers that #include CoreMinimal.h + lots of UE machinery libclang
+# can't parse standalone. Extract each enum block via regex (the
+# delimiters are stable), strip the UE-macro surface (UMETA/UENUM/UMETA),
+# and hand the cleaned single-enum source to libclang for AST-level
+# member extraction. That gives us libclang's accuracy on member values
+# without the UE-include hell, and is byte-comparable to the regex
+# scraper that used to live in generate_ue_components.py.
+# ---------------------------------------------------------------------------
+
+_HAND_ENUM_BLOCK_RE = re.compile(
+    r"(enum\s+class\s+EMj\w+\s*:\s*uint8\s*\{[^}]*\}\s*;)",
+    re.DOTALL,
+)
+_HAND_ENUM_NAME_RE = re.compile(r"enum\s+class\s+(EMj\w+)")
+
+
+_UE_FN_MACRO_RE = re.compile(r"\b(UMETA|UENUM)\s*\(")
+
+
+def _strip_balanced_call(text: str, start: int) -> int:
+    """Return the index just past the matching ``)`` for a function-call
+    opener at ``start``. Handles nested parens — naive ``[^)]*`` would
+    bail on ``UMETA(DisplayName = "Track (Centre of Mass)")``."""
+    depth = 1
+    i = start + 1
+    in_str = None
+    while i < len(text) and depth > 0:
+        ch = text[i]
+        if in_str:
+            if ch == "\\" and i + 1 < len(text):
+                i += 2
+                continue
+            if ch == in_str:
+                in_str = None
+        elif ch in ('"', "'"):
+            in_str = ch
+        elif ch == "(":
+            depth += 1
+        elif ch == ")":
+            depth -= 1
+        i += 1
+    return i
+
+
+def _strip_ue_macros_from_enum_block(text: str) -> str:
+    """Erase ``UMETA(...)`` and ``UENUM(...)`` function-like macros so a
+    plain libclang pass can compile the enum block. Each macro turns
+    into the empty string; the surrounding member identifiers + comma
+    separators stay intact. Handles nested parens inside the macro
+    argument — ``UMETA(DisplayName = "Track (Centre of Mass)")`` etc."""
+    out: List[str] = []
+    i = 0
+    while i < len(text):
+        m = _UE_FN_MACRO_RE.search(text, i)
+        if not m:
+            out.append(text[i:])
+            break
+        out.append(text[i:m.start()])
+        i = _strip_balanced_call(text, m.end() - 1)
+    stripped = "".join(out)
+    stripped = re.sub(r"//[^\n]*", "", stripped)
+    stripped = re.sub(r"/\*.*?\*/", "", stripped, flags=re.DOTALL)
+    return stripped
+
+
+def _scrape_hand_enums_via_libclang(
+    urlab_source_root: str,
+) -> Dict[str, CEnum]:
+    """Walk every .h under ``urlab_source_root`` looking for ``enum class
+    EMj* : uint8`` blocks; hand each cleaned block to libclang for
+    member + value extraction.
+
+    Returns ``{enum_name: CEnum}``. The plugin_root-relative path is
+    stamped on each entry so the snapshot stays byte-identical across
+    developer machines (no absolute ``c:\\users\\...`` strings)."""
+    import clang.cindex as cx  # noqa: E402, local-only
+
+    plugin_root_norm = os.path.normcase(os.path.abspath(_PLUGIN_ROOT))
+    out: Dict[str, CEnum] = {}
+    idx = cx.Index.create()
+
+    for root, _dirs, files in os.walk(urlab_source_root):
+        for name in files:
+            if not name.endswith(".h"):
+                continue
+            path = os.path.join(root, name)
+            try:
+                with open(path, "r", encoding="utf-8") as f:
+                    text = f.read()
+            except OSError:
+                continue
+            rel = os.path.relpath(
+                os.path.normcase(os.path.abspath(path)),
+                plugin_root_norm,
+            ).replace("\\", "/")
+            for block_match in _HAND_ENUM_BLOCK_RE.finditer(text):
+                block = block_match.group(1)
+                name_m = _HAND_ENUM_NAME_RE.search(block)
+                if not name_m:
+                    continue
+                enum_name = name_m.group(1)
+                cleaned = _strip_ue_macros_from_enum_block(block)
+                # Compute 1-based source line of the enum's opening
+                # ``enum`` keyword (the block_match is anchored on the
+                # whole declaration, so its start offset is also the
+                # enum's start).
+                start_line = text[:block_match.start()].count("\n") + 1
+                # UE typedefs uint8 to unsigned char; provide our own
+                # typedef so libclang doesn't need any system headers
+                # (which on Windows pull in MSVC STL that triggers a
+                # "Unexpected compiler version" static_assert).
+                tu = idx.parse(
+                    path="<urlab_enum>.cpp",
+                    args=["-x", "c++", "-std=c++17", "-nostdinc",
+                          "-nostdinc++", "-ferror-limit=0"],
+                    unsaved_files=[(
+                        "<urlab_enum>.cpp",
+                        "typedef unsigned char uint8;\n" + cleaned + "\n",
+                    )],
+                    options=cx.TranslationUnit.PARSE_SKIP_FUNCTION_BODIES,
+                )
+                # Don't bail on diagnostics — even with errors libclang
+                # produces usable ENUM_DECL cursors. The downstream
+                # match-by-name + member iteration covers correctness.
+                for cursor in tu.cursor.walk_preorder():
+                    if cursor.kind != cx.CursorKind.ENUM_DECL:
+                        continue
+                    if cursor.spelling != enum_name:
+                        continue
+                    members: List[CEnumMember] = []
+                    for child in cursor.get_children():
+                        if child.kind == cx.CursorKind.ENUM_CONSTANT_DECL:
+                            members.append(CEnumMember(
+                                name=child.spelling,
+                                value=child.enum_value,
+                                doc="",
+                            ))
+                    if not members:
+                        continue
+                    out[enum_name] = CEnum(
+                        name=enum_name,
+                        members=members,
+                        underlying_type="uint8",
+                        doc="",
+                        file=rel,
+                        line=start_line,
+                    )
+                    break
+    return out
+
+
+def main(argv: List[str] | None = None) -> int:
+    ap = argparse.ArgumentParser(description=__doc__)
+    ap.add_argument("--mujoco-include", default=os.path.join(
+        _PLUGIN_ROOT, "third_party", "install", "MuJoCo", "include",
+    ))
+    ap.add_argument("--output", default=os.path.join(
+        _PLUGIN_ROOT, "Scripts", "codegen", "snapshots", "introspect_snapshot.json",
+    ))
+    ap.add_argument("--libclang", default=None,
+                    help="path to libclang.dll/.so/.dylib; overrides auto-detect")
+    ap.add_argument("--urlab-source-root", default=os.path.join(
+        _PLUGIN_ROOT, "Source", "URLab", "Public",
+    ), help="root for the EMj* hand-enum libclang scrape; skipped if empty")
+    args = ap.parse_args(argv)
+
+    try:
+        import clang.cindex as cx
+    except ImportError:
+        print("ERROR: clang.cindex not installed. Run "
+              "`uv run --with libclang python build_introspect_snapshot.py` "
+              "or `pip install libclang`.", file=sys.stderr)
+        return 1
+
+    if args.libclang:
+        cx.Config.set_library_file(args.libclang)
+    elif os.environ.get("LIBCLANG_LIBRARY_FILE"):
+        cx.Config.set_library_file(os.environ["LIBCLANG_LIBRARY_FILE"])
+
+    mujoco_include = args.mujoco_include
+    if not os.path.exists(mujoco_include):
+        print(f"ERROR: --mujoco-include not found: {mujoco_include}", file=sys.stderr)
+        return 2
+
+    mjspec_h = os.path.join(mujoco_include, "mujoco", "mjspec.h")
+    mjmodel_h = os.path.join(mujoco_include, "mujoco", "mjmodel.h")
+    mjxmacro_h = os.path.join(mujoco_include, "mujoco", "mjxmacro.h")
+
+    # Vendored mjspecmacro.h (only used if non-stub; sync_vendored.py
+    # fills it from upstream).
+    vendored_mjspecmacro = os.path.join(_HERE, "_vendored", "mjspecmacro.h")
+
+    # Synthesize a small dispatch source that #includes the headers we
+    # care about. Letting clang chew on a real .c lets it resolve types
+    # without us threading -include flags.
+    dispatch_c = "\n".join([
+        "#include <mujoco/mjmodel.h>",
+        "#include <mujoco/mjspec.h>",
+        "#include <mujoco/mjxmacro.h>",
+        "#include <mujoco/mujoco.h>",
+    ])
+    args_clang = [
+        "-x", "c",
+        "-std=c11",
+        f"-I{mujoco_include}",
+        "-DMJ_STATIC",     # avoid MJAPI dllimport/export linkage attrs
+    ]
+
+    try:
+        idx = cx.Index.create()
+    except cx.LibclangError as exc:
+        print(f"ERROR: libclang failed to load: {exc}", file=sys.stderr)
+        return 3
+
+    tu = idx.parse(
+        path="<urlab_dispatch>.c",
+        args=args_clang,
+        unsaved_files=[("<urlab_dispatch>.c", dispatch_c)],
+        options=(cx.TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD
+                 | cx.TranslationUnit.PARSE_SKIP_FUNCTION_BODIES),
+    )
+    fatal_diags = [d for d in tu.diagnostics if d.severity >= cx.Diagnostic.Error]
+    if fatal_diags:
+        print(f"libclang errors:", file=sys.stderr)
+        for d in fatal_diags[:5]:
+            print(f"  {d.spelling} ({d.location})", file=sys.stderr)
+        return 4
+
+    snapshot = IntrospectSnapshot(
+        snapshot_version=1,
+        header_hash=_header_hash([mjspec_h, mjmodel_h, mjxmacro_h,
+                                  vendored_mjspecmacro]),
+    )
+    _walk(tu, mujoco_include, snapshot)
+
+    if args.urlab_source_root and os.path.isdir(args.urlab_source_root):
+        try:
+            snapshot.hand_enums = _scrape_hand_enums_via_libclang(
+                args.urlab_source_root,
+            )
+        except cx.LibclangError as exc:
+            print(f"warning: hand-enum libclang scrape skipped: {exc}",
+                  file=sys.stderr)
+
+    out_dict = snapshot.to_dict()
+    os.makedirs(os.path.dirname(args.output), exist_ok=True)
+    with open(args.output, "w", encoding="utf-8") as f:
+        json.dump(out_dict, f, indent=2)
+    print(f"wrote {args.output}: "
+          f"{len(snapshot.functions)} functions, "
+          f"{len(snapshot.enums)} enums, "
+          f"{len(snapshot.structs)} structs, "
+          f"{len(snapshot.defines)} defines, "
+          f"{len(snapshot.hand_enums)} hand_enums, "
+          f"header_hash={snapshot.header_hash[:12]}")
+    return 0
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())
diff --git a/Scripts/codegen/build_mjcf_schema_snapshot.py b/Scripts/codegen/build_mjcf_schema_snapshot.py
index 53f4327..634d448 100644
--- a/Scripts/codegen/build_mjcf_schema_snapshot.py
+++ b/Scripts/codegen/build_mjcf_schema_snapshot.py
@@ -175,7 +175,209 @@ def _find_child(parent: SchemaNode, name: str) -> Optional[SchemaNode]:
 ]
 
 
-def _build_snapshot(root: SchemaNode, mujoco_version: str) -> Dict:
+# ----------------------------------------------------------------------------
+# Per-sensor objtype/reftype extraction
+# ----------------------------------------------------------------------------
+#
+# The static MJCF[nMJCF] schema only carries attr lists — not the per-sensor
+# mapping between the XML element name and the mjsSensor objtype/reftype the
+# compiler expects. That logic lives in xml_native_reader.cc's `Sensor()`
+# method (a giant if/else cascade, one branch per sensor type). The codegen
+# scrapes those branches here so rules don't have to hand-list them.
+
+# Inside Sensor(), each "regular" branch follows:
+#   if (type == "<NAME>") {
+#     sensor->type    = mjSENS_X;
+#     sensor->objtype = mjOBJ_Y;            // OPTIONAL: literal default
+#     ReadAttrTxt(elem, "<attr>", objname, true);   // OPTIONAL: name source
+#     sensor->reftype = mjOBJ_Z;            // OPTIONAL
+#     ReadAttrTxt(elem, "<attr>", refname, true);   // OPTIONAL: ref name source
+#   }
+#
+# "Computed" branches (rangefinder, distance/normal/fromto, contact, user,
+# plugin) derive objtype/reftype from attribute presence at parse time;
+# those get marked computed=True so codegen rules keep their explicit
+# overrides for them.
+# Matches the open of a sensor branch, capturing the parenthesised condition
+# string. We post-process the condition to pull out every ``type == "NAME"``
+# clause so ``else if (type == "distance" || type == "normal" || type ==
+# "fromto") { ... }`` produces three entries that share the branch body.
+_SENSOR_BRANCH_RE = re.compile(
+    r'(?:if|else if)\s*\((?P<cond>[^{]*?type\s*==\s*"[^"]+"[^{]*)\)\s*\{',
+    re.DOTALL,
+)
+_SENSOR_NAMES_IN_COND_RE = re.compile(r'type\s*==\s*"([^"]+)"')
+_TYPE_LITERAL_RE     = re.compile(r'sensor->type\s*=\s*(mjSENS_\w+)\s*;')
+_OBJTYPE_LITERAL_RE  = re.compile(r'sensor->objtype\s*=\s*(mjOBJ_\w+)\s*;')
+_REFTYPE_LITERAL_RE  = re.compile(r'sensor->reftype\s*=\s*(mjOBJ_\w+)\s*;')
+_OBJTYPE_FROM_XML_RE = re.compile(
+    r'ReadAttrTxt\s*\(\s*elem\s*,\s*"objtype"\s*,\s*text'
+)
+_NAME_READ_RE = re.compile(
+    r'ReadAttrTxt\s*\(\s*elem\s*,\s*"([^"]+)"\s*,\s*(objname|refname)\b'
+)
+
+
+def _slice_balanced_braces(src: str, open_idx: int) -> str:
+    """Given an index pointing AT '{', return the substring up to the
+    matching close (exclusive of both braces). Tracks string literals and
+    line/block comments so braces inside ``"..."``, ``//`` or ``/* */`` are
+    ignored. Returns "" if no matching close found."""
+    assert src[open_idx] == "{"
+    n = len(src)
+    depth = 0
+    i = open_idx
+    in_str = False
+    str_q = ""
+    in_line_comment = False
+    in_block_comment = False
+    while i < n:
+        c = src[i]
+        c2 = src[i:i+2]
+        if in_line_comment:
+            if c == "\n":
+                in_line_comment = False
+            i += 1
+            continue
+        if in_block_comment:
+            if c2 == "*/":
+                in_block_comment = False
+                i += 2
+                continue
+            i += 1
+            continue
+        if in_str:
+            if c == "\\":
+                i += 2
+                continue
+            if c == str_q:
+                in_str = False
+            i += 1
+            continue
+        if c2 == "//":
+            in_line_comment = True
+            i += 2
+            continue
+        if c2 == "/*":
+            in_block_comment = True
+            i += 2
+            continue
+        if c in ('"', "'"):
+            in_str = True
+            str_q = c
+            i += 1
+            continue
+        if c == "{":
+            depth += 1
+        elif c == "}":
+            depth -= 1
+            if depth == 0:
+                return src[open_idx+1:i]
+        i += 1
+    return ""
+
+
+# Branches where objtype/reftype are derived from attribute presence at
+# parse time (multiple ReadAttrTxt + sensor->objtype = X ? mjOBJ_A : mjOBJ_B
+# pattern). Codegen rules keep explicit overrides for these; the extractor
+# marks them computed=True so the generated codegen doesn't pretend to know.
+_COMPUTED_OBJTYPE_SENSORS = {
+    "rangefinder", "distance", "normal", "fromto", "contact",
+    "user", "plugin",
+}
+
+
+def _extract_sensor_per_type(src_text: str) -> Dict[str, Dict[str, object]]:
+    """Scrape ``Sensor()``'s per-type if/else cascade. Returns a dict
+    keyed by XML element name; each entry has:
+
+        objtype:    "mjOBJ_X" | "from_xml" | "computed" | None
+        reftype:    "mjOBJ_Y" | "computed" | None
+        obj_attr:   "site"/"joint"/.../None    (XML attr name carrying objname)
+        ref_attr:   "camera"/"site"/.../None   (XML attr name carrying refname)
+        computed:   True iff objtype/reftype are derived from attr presence
+    """
+    # Slice out the Sensor() method body so we don't pick up branches from
+    # other methods that happen to share regex shapes.
+    sig_re = re.compile(r"void\s+mjXReader::Sensor\s*\(")
+    m = sig_re.search(src_text)
+    if not m:
+        return {}
+    open_idx = src_text.find("{", m.end())
+    if open_idx < 0:
+        return {}
+    body = _slice_balanced_braces(src_text, open_idx)
+    if not body:
+        return {}
+
+    result: Dict[str, Dict[str, object]] = {}
+    # Some outer branches (distance/normal/fromto, contact, plugin) contain
+    # an inner ``if (type == "X")`` that toggles ->type. Those inner matches
+    # would otherwise overwrite the outer-branch entry with a stripped-down
+    # body. Track the [start, end) of each outer-branch body and skip
+    # matches whose '{' falls inside.
+    consumed_ranges: List[tuple] = []
+    for bm in _SENSOR_BRANCH_RE.finditer(body):
+        names = _SENSOR_NAMES_IN_COND_RE.findall(bm.group("cond"))
+        if not names:
+            continue
+        brace_idx = bm.end() - 1
+        if brace_idx < 0 or body[brace_idx] != "{":
+            continue
+        if any(s < brace_idx < e for s, e in consumed_ranges):
+            continue
+        branch = _slice_balanced_braces(body, brace_idx)
+        consumed_ranges.append((brace_idx, brace_idx + 1 + len(branch)))
+
+        obj_literal = _OBJTYPE_LITERAL_RE.search(branch)
+        ref_literal = _REFTYPE_LITERAL_RE.search(branch)
+        obj_from_xml = bool(_OBJTYPE_FROM_XML_RE.search(branch)) and not obj_literal
+
+        obj_attr: Optional[str] = None
+        ref_attr: Optional[str] = None
+        for nm in _NAME_READ_RE.finditer(branch):
+            attr, target = nm.group(1), nm.group(2)
+            if attr == "objtype":
+                continue   # the literal "objtype" attr selector, not a name source
+            if target == "objname" and obj_attr is None:
+                obj_attr = attr
+            elif target == "refname" and ref_attr is None:
+                ref_attr = attr
+
+        # Capture the mjSENS_X assigned in this branch. Multi-name branches
+        # (distance/normal/fromto) carry an inner positional switch with
+        # one ->type per name. Single-name branches (touch, framepos, ...)
+        # have exactly one mjSENS_X.
+        mj_types = [m.group(1) for m in _TYPE_LITERAL_RE.finditer(branch)]
+        if len(mj_types) == len(names):
+            name_to_mj_type = dict(zip(names, mj_types))
+        elif mj_types:
+            # Single-type branch — every name shares it.
+            name_to_mj_type = {n: mj_types[0] for n in names}
+        else:
+            name_to_mj_type = {n: None for n in names}
+
+        # Multi-name branches (distance/normal/fromto share one block,
+        # routed inside by `if (type == ...)` to set ->type). All share
+        # the same objtype/reftype derivation, so emit one entry per name.
+        for name in names:
+            computed = name in _COMPUTED_OBJTYPE_SENSORS
+            result[name] = {
+                "mj_type":  name_to_mj_type.get(name),
+                "objtype":  ("computed" if computed
+                             else ("from_xml" if obj_from_xml
+                                   else (obj_literal.group(1) if obj_literal else None))),
+                "reftype":  ("computed" if computed
+                             else (ref_literal.group(1) if ref_literal else None)),
+                "obj_attr": obj_attr,
+                "ref_attr": ref_attr,
+                "computed": computed,
+            }
+    return result
+
+
+def _build_snapshot(root: SchemaNode, mujoco_version: str, *,
+                    src_text: str = "") -> Dict:
     """Walk the parsed MJCF tree and project it onto URLab's curated shape."""
     out: Dict = {
         "_meta": {
@@ -237,11 +439,22 @@ def _build_snapshot(root: SchemaNode, mujoco_version: str) -> Dict:
     # Sensor section: per-type attr declarations. URLab tracks:
     #   sensor_types = list of names
     #   sensor_common = shared attrs (_SENSOR_COMMON_ATTRS, URLab convention)
+    #   sensor_per_type = per-sensor objtype/reftype/attr defaults scraped
+    #     from xml_native_reader.cc's Sensor() method body
     sensor = _find_child(mujoco, "sensor")
     if sensor is not None:
         sensor_attrs = {c.name: list(c.attrs) for c in sensor.children}
         out["sensor_types"] = list(sensor_attrs.keys())
         out["sensor_common"] = {"attrs": list(_SENSOR_COMMON_ATTRS)}
+        if src_text:
+            per_type = _extract_sensor_per_type(src_text)
+            # Only emit per-type rows for sensors that exist in the static
+            # MJCF schema — keeps the snapshot in sync if MuJoCo adds a
+            # parser branch but not a schema entry (or vice versa).
+            out["sensor_per_type"] = {
+                name: per_type[name] for name in sensor_attrs
+                if name in per_type
+            }
 
     # Tendon section: spatial vs fixed, plus wrap types.
     tendon = _find_child(mujoco, "tendon")
@@ -348,7 +561,7 @@ def main(argv: Optional[List[str]] = None) -> int:
         plugin_root, "third_party", "MuJoCo", "src", "src", "xml",
         "xml_native_reader.cc",
     )
-    default_output = os.path.join(plugin_root, "Scripts", "mjcf_schema_snapshot.json")
+    default_output = os.path.join(plugin_root, "Scripts", "codegen", "snapshots", "mjcf_schema_snapshot.json")
 
     ap = argparse.ArgumentParser()
     ap.add_argument("--src", default=default_src,
@@ -367,7 +580,7 @@ def main(argv: Optional[List[str]] = None) -> int:
     blocks = _tokenise_blocks(body)
     root = _parse_tree(blocks)
     version = _detect_mujoco_version(args.src)
-    snapshot = _build_snapshot(root, mujoco_version=version)
+    snapshot = _build_snapshot(root, mujoco_version=version, src_text=src_text)
 
     os.makedirs(os.path.dirname(args.output), exist_ok=True)
     with open(args.output, "w", encoding="utf-8") as f:
diff --git a/Scripts/codegen/build_mjspec_snapshot.py b/Scripts/codegen/build_mjspec_snapshot.py
deleted file mode 100644
index 706d7ac..0000000
--- a/Scripts/codegen/build_mjspec_snapshot.py
+++ /dev/null
@@ -1,168 +0,0 @@
-# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
-"""Parse mjspec.h and emit a JSON snapshot of each `mjs<Name>` struct's
-fields so codegen can look up the real C field name (e.g. mjsExclude
-holds `bodyname1`, not the schema attr `body1`).
-
-Output: ``Scripts/mjspec_snapshot.json`` with shape
-    {
-      "structs": {
-        "mjsBody":  ["element", "name", "childclass", ...],
-        "mjsPair":  ["element", "geomname1", "geomname2", "condim", ...],
-        ...
-      }
-    }
-
-Usage:
-    python build_mjspec_snapshot.py
-        [--mjspec PATH]   (default: third_party/install/MuJoCo/include/mujoco/mjspec.h)
-        [--output PATH]   (default: Scripts/mjspec_snapshot.json)
-"""
-
-from __future__ import annotations
-
-import argparse
-import json
-import os
-import re
-import sys
-
-SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
-PLUGIN_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, "..", ".."))
-
-DEFAULT_MJSPEC = os.path.join(
-    PLUGIN_ROOT, "third_party", "install", "MuJoCo", "include", "mujoco", "mjspec.h"
-)
-DEFAULT_MUJOCO_H = os.path.join(
-    PLUGIN_ROOT, "third_party", "install", "MuJoCo", "include", "mujoco", "mujoco.h"
-)
-DEFAULT_OUTPUT = os.path.join(PLUGIN_ROOT, "Scripts", "mjspec_snapshot.json")
-
-# Matches `typedef struct mjsName_ { ... } mjsName;` blocks.
-_STRUCT_RE = re.compile(
-    r"typedef\s+struct\s+(mjs\w+)_\s*\{(.*?)\}\s*\1\s*;",
-    re.DOTALL,
-)
-
-# Inside a struct body, a field line looks like:
-#   `<type> <name>;`              -> e.g.  `int condim;`
-#   `<type> <name>[<dim>];`       -> e.g.  `mjtNum solref[mjNREF];`
-#   `<type>* <name>;`             -> e.g.  `mjString* name;`
-#   `<type> <name>[d1][d2];`      -> e.g.  `double data[mjNEQDATA];`
-# Comments after the `;` are stripped before matching.
-_FIELD_RE = re.compile(
-    r"^\s*"
-    r"(?:struct\s+)?"                  # optional `struct` keyword
-    r"(?P<type>[\w:]+\s*\**)\s+"        # type, possibly with `*`
-    r"(?P<name>\w+)"                    # field name
-    r"(?:\s*\[[^\]]*\])*"               # optional [dim] suffix(es)
-    r"\s*;",
-    re.MULTILINE,
-)
-
-
-def parse_structs(text: str) -> dict[str, list[str]]:
-    structs: dict[str, list[str]] = {}
-    for m in _STRUCT_RE.finditer(text):
-        name = m.group(1)
-        body = m.group(2)
-        # Strip line-end comments.
-        body_clean = re.sub(r"//[^\n]*", "", body)
-        body_clean = re.sub(r"/\*.*?\*/", "", body_clean, flags=re.DOTALL)
-        fields: list[str] = []
-        for fm in _FIELD_RE.finditer(body_clean):
-            fname = fm.group("name")
-            # Skip C-keyword-only matches; the regex shouldn't, but be safe.
-            if fname in ("struct", "const", "typedef", "if", "else"):
-                continue
-            fields.append(fname)
-        structs[name] = fields
-    return structs
-
-
-# Matches `MJAPI const char* mjs_setToX(mjsActuator* actuator, ... params ...);`.
-# DOTALL so the param list can span lines. `?:` non-capturing to keep groups
-# minimal; we re-parse the param string with `_PARAM_RE` below.
-_SETTO_RE = re.compile(
-    r"MJAPI\s+const\s+char\s*\*\s*"
-    r"(?P<name>mjs_setTo\w+)\s*"
-    r"\(\s*mjsActuator\s*\*\s*\w+\s*(?:,\s*(?P<params>[^)]*))?\s*\)",
-    re.DOTALL,
-)
-
-# Inside the param list, each param looks like `<type> <name>[<dim>]?`,
-# separated by commas. Handles `double kv[1]`, `double timeconst`, `int input_mode`.
-_PARAM_RE = re.compile(
-    r"^\s*(?P<c_type>[\w]+)\s+"
-    r"(?P<name>\w+)"
-    r"(?:\s*\[\s*(?P<dim>\d+)\s*\])?\s*$"
-)
-
-
-def parse_setto_functions(text: str) -> dict[str, dict]:
-    """Scrape ``mjs_setTo*`` function declarations from mujoco.h. Skips the
-    first ``mjsActuator* actuator`` parameter; returns the remaining
-    parameter list with C type and (if present) fixed array dimension."""
-    out: dict[str, dict] = {}
-    for m in _SETTO_RE.finditer(text):
-        name = m.group("name")
-        params_text = (m.group("params") or "").strip()
-        params: list[dict] = []
-        if params_text:
-            for raw in params_text.split(","):
-                raw_clean = re.sub(r"//[^\n]*", "", raw).strip()
-                if not raw_clean:
-                    continue
-                pm = _PARAM_RE.match(raw_clean)
-                if not pm:
-                    raise RuntimeError(
-                        f"unparsed mjs_setTo* param: {raw_clean!r} in {name}"
-                    )
-                params.append({
-                    "name": pm.group("name"),
-                    "c_type": pm.group("c_type"),
-                    "array_dim": int(pm.group("dim")) if pm.group("dim") else None,
-                })
-        out[name] = {"params": params}
-    return out
-
-
-def main() -> int:
-    ap = argparse.ArgumentParser(description=__doc__)
-    ap.add_argument("--mjspec", default=DEFAULT_MJSPEC)
-    ap.add_argument("--mujoco-h", default=DEFAULT_MUJOCO_H)
-    ap.add_argument("--output", default=DEFAULT_OUTPUT)
-    args = ap.parse_args()
-
-    if not os.path.exists(args.mjspec):
-        print(f"ERROR: mjspec.h not found at {args.mjspec}", file=sys.stderr)
-        return 1
-
-    with open(args.mjspec, "r", encoding="utf-8") as f:
-        text = f.read()
-
-    structs = parse_structs(text)
-    out = {"structs": structs}
-
-    # Also scrape mjs_setTo* signatures from mujoco.h so codegen can emit
-    # the actuator preset calls from introspection instead of hand-written
-    # C++ literals in codegen_rules.json.
-    if os.path.exists(args.mujoco_h):
-        with open(args.mujoco_h, "r", encoding="utf-8") as f:
-            mj_text = f.read()
-        out["setto_functions"] = parse_setto_functions(mj_text)
-    else:
-        print(f"WARN: mujoco.h not found at {args.mujoco_h} — skipping setto functions", file=sys.stderr)
-        out["setto_functions"] = {}
-
-    os.makedirs(os.path.dirname(args.output), exist_ok=True)
-    with open(args.output, "w", encoding="utf-8") as f:
-        json.dump(out, f, indent=2)
-
-    total_fields = sum(len(v) for v in structs.values())
-    print(f"wrote {args.output}: {len(structs)} structs, {total_fields} fields, "
-          f"{len(out['setto_functions'])} setto functions")
-    return 0
-
-
-if __name__ == "__main__":
-    raise SystemExit(main())
diff --git a/Scripts/codegen/build_mjxmacro_snapshot.py b/Scripts/codegen/build_mjxmacro_snapshot.py
index f60706f..d2053cc 100644
--- a/Scripts/codegen/build_mjxmacro_snapshot.py
+++ b/Scripts/codegen/build_mjxmacro_snapshot.py
@@ -30,7 +30,7 @@
 DEFAULT_MJXMACRO = os.path.join(
     PLUGIN_ROOT, "third_party", "install", "MuJoCo", "include", "mujoco", "mjxmacro.h"
 )
-DEFAULT_OUTPUT = os.path.join(PLUGIN_ROOT, "Scripts", "mjxmacro_snapshot.json")
+DEFAULT_OUTPUT = os.path.join(PLUGIN_ROOT, "Scripts", "codegen", "snapshots", "mjxmacro_snapshot.json")
 
 # Block names we care about. Categorized so the consumer knows the
 # kind of data (per-element pointer table vs flat struct fields).
@@ -76,9 +76,12 @@
     "MJDATA_ARENA_POINTERS_DUAL",
     "MJDATA_ARENA_POINTERS_ISLAND",
     "MJDATA_ARENA_POINTERS_SOLVER",
+    "MJDATA_ARENA_POINTERS_CONTACT",
 ]
 
 # Flat struct field blocks (no outer dimension — just type + name + size).
+# Lives across both mjxmacro.h (the mjmodel-side struct fields) and
+# mjspecmacro.h (the mjspec-side struct fields).
 STRUCT_FIELD_BLOCKS = [
     "MJOPTION_FIELDS",
     "MJSTATISTIC_FIELDS",
@@ -88,13 +91,26 @@
     "MJVISUAL_MAP_FIELDS",
     "MJVISUAL_SCALE_FIELDS",
     "MJVISUAL_RGBA_FIELDS",
+    "MJSCOMPILER_FIELDS",
+    "MJSPEC_FIELDS",
 ]
 
+DEFAULT_MJSPECMACRO = os.path.join(
+    SCRIPT_DIR, "_vendored", "mjspecmacro.h",
+)
+
 # Regexes
 _BLOCK_START_RE = re.compile(r"^\s*#define\s+(\w+)\s*(?:\(\s*\w+\s*\))?\s*\\?\s*$")
+# Single source of truth for the X-macro tag set. Accepts X / XVEC / XNV.
+# XNV ("X No Vec") is structurally identical to X (same 4-tuple shape);
+# MuJoCo uses the tag so its own codegen knows to skip the vectorised
+# emission path. URLab doesn't care about that distinction — we just want
+# the entry shape. Add new tags here whenever MuJoCo introduces one;
+# duplicating the list in a sibling regex is the bug class that bit XNV.
+_X_TAG_RE = r"X(?:VEC|NV)?"
 # X-macro entry. Tolerates 4 or 3-tuple forms (pointer vs flat field).
 # Captures parenthesised contents; field split below trims/normalizes.
-_X_ENTRY_RE = re.compile(r"^\s*X(?:VEC)?\s*\(\s*(.*?)\s*\)\s*\\?\s*$")
+_X_ENTRY_RE = re.compile(rf"^\s*{_X_TAG_RE}\s*\(\s*(.*?)\s*\)\s*\\?\s*$")
 
 
 def _strip_comments(text: str) -> str:
@@ -104,13 +120,7 @@ def _strip_comments(text: str) -> str:
     return text
 
 
-def _join_continued_lines(text: str) -> str:
-    # mjxmacro spreads multi-line macros via backslash continuation; preserve
-    # individual entries on their own lines for downstream parsing.
-    return text
-
-
-_BODY_ENTRY_RE = re.compile(r"^\s*(X|XVEC)\s*\(")
+_BODY_ENTRY_RE = re.compile(rf"^\s*{_X_TAG_RE}\s*\(")
 
 
 def _parse_block_body(lines: List[str], start_idx: int) -> List[str]:
@@ -170,12 +180,16 @@ def _parse_pointer_entry(raw: str) -> Optional[Dict[str, Any]]:
 
 
 def _parse_struct_field_entry(raw: str) -> Optional[Dict[str, Any]]:
-    """Parse `X(type, name, count)` or `X(name, count)` or `XVEC(...)`.
+    """Parse `X(type, name, count)` or `X(name, count)` or `X(name)` or `XVEC(...)`.
 
     mjxmacro flat-struct blocks have a few shapes:
     - `X(type, name, count)` (mjOption)
     - `X(name, count)` (mjStatistic — no type, defaults to mjtNum)
     - `X(type, name)` (mjVisual fields — no explicit count, treated as scalar)
+    - `X(name)` (mjVisualQuality/Map/Scale/Rgba — bare field name; type is
+      whatever the corresponding ``struct mjVisualX`` member is. URLab
+      defaults to mjtNum since these structs are float/int in practice and
+      consumers can override via ``field_types``.)
     - `XVEC(type, name, count)` (vec3 / vec5 / etc.)
     """
     m = _X_ENTRY_RE.match(raw)
@@ -193,6 +207,9 @@ def _parse_struct_field_entry(raw: str) -> Optional[Dict[str, Any]]:
             ctype, name, count = fields[0], fields[1], "1"
         else:
             ctype, name, count = "mjtNum", fields[0], fields[1]
+    elif len(fields) == 1:
+        # Bare field name — type defaults to mjtNum, scalar.
+        ctype, name, count = "mjtNum", fields[0], "1"
     else:
         return None
     return {
@@ -203,6 +220,24 @@ def _parse_struct_field_entry(raw: str) -> Optional[Dict[str, Any]]:
     }
 
 
+# Block names from MuJoCo's X-macro headers that we KNOWINGLY do not
+# consume — additions are still flagged at parse time so a new block
+# can't disappear silently, but listed names produce no warning.
+KNOWN_UNCONSUMED_BLOCKS = {
+    # Sentinels / size constants — defined as #define X foo without a body.
+    "MJMODEL_POINTERS_PREAMBLE",
+    "MJMODEL_POINTERS",  # meta-aggregator (re-includes all POINTERS_* blocks)
+    "MJMODEL_SIZES",     # int field counts
+    "MJMODEL_INTS",
+    "MJMODEL_INTS_PREAMBLE",
+    "MJMODEL_INTS_NUMERIC",
+    "MJMODEL_FLOATS",
+    "MJDATA_VECTOR",
+    "MJDATA_SCALAR",
+    "MJDATA_SCALAR_PREAMBLE",
+}
+
+
 def parse_mjxmacro(path: str) -> Dict[str, Any]:
     with open(path, "r", encoding="utf-8") as f:
         raw_text = f.read()
@@ -219,6 +254,12 @@ def parse_mjxmacro(path: str) -> Dict[str, Any]:
         "struct_fields": {},
     }
 
+    expected = (
+        set(POINTER_BLOCKS_MJMODEL) | set(POINTER_BLOCKS_MJDATA)
+        | set(STRUCT_FIELD_BLOCKS) | KNOWN_UNCONSUMED_BLOCKS
+    )
+    seen_blocks: set = set()
+
     i = 0
     while i < len(lines):
         line = lines[i]
@@ -227,8 +268,27 @@ def parse_mjxmacro(path: str) -> Dict[str, Any]:
             i += 1
             continue
         block_name = m.group(1)
+        seen_blocks.add(block_name)
         # Collect block body — entries on subsequent lines.
         entries = _parse_block_body(lines, i + 1)
+        # Defensive sentinel: a non-entry, non-empty line ended body
+        # collection inside an EXPECTED block. The XNV regression was
+        # exactly this — surface it so the next new X-tag bumps cause a
+        # loud warning rather than silently truncating the snapshot.
+        end_idx = i + 1 + len(entries)
+        if (block_name in expected and end_idx < len(lines)
+                and lines[end_idx].strip()
+                and not _BODY_ENTRY_RE.match(lines[end_idx].strip())):
+            # If the very next line was also an entry-shaped token we
+            # missed (i.e. it has parens), warn loudly.
+            tail = lines[end_idx].strip()
+            if "(" in tail and tail.endswith(")") or tail.endswith(") \\"):
+                print(
+                    f"warning: body collector stopped INSIDE block "
+                    f"{block_name} at line {end_idx + 1}: '{tail[:80]}' — "
+                    f"new X-macro tag? Update _X_TAG_RE in this script.",
+                    file=sys.stderr,
+                )
         if block_name in POINTER_BLOCKS_MJMODEL:
             parsed = [e for e in (_parse_pointer_entry(r) for r in entries) if e]
             out["mjmodel_pointers"][block_name] = parsed
@@ -239,6 +299,29 @@ def parse_mjxmacro(path: str) -> Dict[str, Any]:
             parsed = [e for e in (_parse_struct_field_entry(r) for r in entries) if e]
             out["struct_fields"][block_name] = parsed
         i += 1
+
+    # New-block tripwire: surface any #define block whose name we have
+    # never seen before. Catches scenarios like a new
+    # MJMODEL_POINTERS_PLUGIN_SUB landing upstream — without this, the
+    # snapshot silently drops the block.
+    unexpected = {
+        b for b in seen_blocks
+        if b not in expected and (b.startswith("MJMODEL_") or
+                                  b.startswith("MJDATA_") or
+                                  b.startswith("MJOPTION_") or
+                                  b.startswith("MJSTATISTIC_") or
+                                  b.startswith("MJVISUAL_") or
+                                  b.startswith("MJSCOMPILER_") or
+                                  b.startswith("MJSPEC_"))
+    }
+    if unexpected:
+        print(
+            f"warning: parsed {len(unexpected)} unrecognised X-macro "
+            f"block(s) — add to POINTER_BLOCKS_MJMODEL/MJDATA, "
+            f"STRUCT_FIELD_BLOCKS, or KNOWN_UNCONSUMED_BLOCKS in this "
+            f"script: {sorted(unexpected)}",
+            file=sys.stderr,
+        )
     return out
 
 
@@ -249,6 +332,32 @@ def main() -> int:
         print(f"error: mjxmacro.h not found at {src}", file=sys.stderr)
         return 1
     snapshot = parse_mjxmacro(src)
+    # Track which source each struct-field block came from so a snapshot
+    # reader can tell mjmodel-side (mjxmacro.h) blocks apart from mjspec-side
+    # (mjspecmacro.h) blocks without re-parsing the headers.
+    block_sources: Dict[str, str] = {}
+    for block_name in snapshot["struct_fields"]:
+        block_sources[block_name] = snapshot["_meta"]["source"]
+    # Also fold in the vendored mjspecmacro.h (MJSCOMPILER_FIELDS +
+    # MJSPEC_FIELDS) — both live in the same ``struct_fields`` dict so
+    # synthetic_categories rules can reference either by block name.
+    if os.path.isfile(DEFAULT_MJSPECMACRO):
+        spec_snapshot = parse_mjxmacro(DEFAULT_MJSPECMACRO)
+        spec_rel = os.path.relpath(DEFAULT_MJSPECMACRO, PLUGIN_ROOT)
+        for block_name, entries in spec_snapshot["struct_fields"].items():
+            snapshot["struct_fields"][block_name] = entries
+            block_sources[block_name] = spec_rel
+    else:
+        # Loud rather than silent — MJSCOMPILER_FIELDS / MJSPEC_FIELDS
+        # blocks WILL be missing if the vendored header was removed, and
+        # that surfaces only as a downstream KeyError much later in
+        # generate_ue_components.py.
+        print(
+            f"warning: vendored mjspecmacro.h not found at {DEFAULT_MJSPECMACRO}; "
+            f"MJSCOMPILER_FIELDS + MJSPEC_FIELDS will be absent from the snapshot.",
+            file=sys.stderr,
+        )
+    snapshot["_meta"]["struct_field_sources"] = block_sources
     # Stats for sanity.
     total_model = sum(len(v) for v in snapshot["mjmodel_pointers"].values())
     total_data = sum(len(v) for v in snapshot["mjdata_pointers"].values())
diff --git a/Scripts/codegen/codegen_rules.json b/Scripts/codegen/codegen_rules.json
index c29fa76..12e9909 100644
--- a/Scripts/codegen/codegen_rules.json
+++ b/Scripts/codegen/codegen_rules.json
@@ -4,22 +4,27 @@
     "version": "5.0"
   },
 
+  "_doc_transform_widget_policy": "Categories whose Pos/Quat are authored via the UE Transform widget (geom/site/camera/frame/inertial) hide the canon Pos/Quat UPROPERTYs from the Details panel. The UPROPERTYs still exist for the import seed + BP read access, but EditConditionHides keeps them out of the user-facing surface so authoring stays in one place. Per-element _note_hidden entries point at this policy instead of restating it.",
+
   "global_exclusions": [
-    "user", "name", "class", "type"
+    "user", "name", "class", "type", "objtype", "reftype"
   ],
   "default_type": "float",
   "_note_property_renames": "Schema attr -> UPROPERTY identifier. Used when the UE-side identifier must differ from the verbatim schema attr (typically to avoid C++ keyword collisions or to keep PascalCase for legacy consumers). qpos/qvel/etc are PascalCase on UMjKeyframe because BP graphs / runtime accessors reference them that way.",
   "property_renames": {
-    "qpos":  "Qpos",
-    "qvel":  "Qvel",
-    "act":   "Act",
-    "ctrl":  "Ctrl",
-    "mpos":  "Mpos",
-    "mquat": "Mquat",
-    "time":  "Time"
+    "qpos":               "Qpos",
+    "qvel":               "Qvel",
+    "act":                "Act",
+    "ctrl":               "Ctrl",
+    "mpos":               "Mpos",
+    "mquat":              "Mquat",
+    "time":               "Time",
+    "actuatorfrclimited": "ActFrcLimited",
+    "actuatorfrcrange":   "ActFrcRange",
+    "axis":               "Axis"
   },
 
-  "_note_setto_param_defaults": "MuJoCo's mjs_setTo* API treats specific scalar defaults as sentinels. These values are how URLab signals 'not overridden, use MuJoCo's default'. Most use -1.0; a few use 0.0 or 0.01 — encoded here as data, NOT as C++ string literals.",
+  "_note_setto_param_defaults": "MuJoCo's mjs_setTo* API treats specific scalar defaults as sentinels. These values are how URLab signals 'not overridden, use MuJoCo's default'. Most use -1.0; a few use 0.0 or 0.01. Values are stored as strings because the codegen drops them verbatim into the generated C++ call — typing them as JSON numbers would force a format choice (1 vs 1.0 vs 1.0f) and lose round-trip fidelity.",
   "setto_param_defaults": {
     "mjs_setToPosition":     {"inheritrange": "0.0"},
     "_note_mjs_setToIntVelocity": "intvelocity's mjs_setTo signature accepts a timeconst array param but MuJoCo's XML schema doesn't expose `timeconst` as an MJCF attr on <intvelocity> (unlike <position>/<cylinder>/<muscle>). Pin it to the -1.0 sentinel so the setto leaves the type-derived default alone.",
@@ -38,6 +43,91 @@
     "skin":  "Children of <asset>. URLab handles skinning via UE skeletal meshes; <skin> in MJCF would need a dedicated asset pipeline."
   },
 
+  "_note_hand_vs_generated_enums": "URLab keeps every EMj*Type enum hand-rolled. Subtype-driving enums (EMjJointType, EMjGeomType, etc.) must stay hand-rolled because each member drives a UClass subclass — adding one is a product decision, not mechanical. Pure-mapping enums (EMjGainType, EMjBiasType, ...) are mechanically codegenable but live next to their parent UClass; moving them to Generated/ would require include reordering for no functional gain. The hand_enum_drift phase in generate_ue_components.py provides the equivalent forward-maintainability guarantee: any new mjt* value upstream surfaces as a diagnostic instead of a silent runtime fallthrough.",
+
+  "_note_compiler_attr_field_map": "MJCF <compiler attr=...> XML attrs URLab honours, mapped to their FMjCompilerSettings field name. The field is hand-rolled in MuJoCo/Utils/MjOrientationUtils.h; the drift check uses this map purely as an allowlist. New MuJoCo compiler flags surface as a diagnostic until added here or to intentionally_unmodeled_compiler_attrs.",
+  "compiler_attr_field_map": {
+    "angle":      "bAngleInDegrees",
+    "eulerseq":   "EulerSeq",
+    "meshdir":    "MeshDir",
+    "assetdir":   "AssetDir",
+    "autolimits": "bAutoLimits"
+  },
+
+  "_note_intentionally_unmodeled_compiler_attrs": "<compiler> XML attrs that don't affect URLab's UE-side parsing — physics tuning knobs, compiler-only optimisations, paths URLab supersedes with UE asset routing. Listed explicitly so the compiler-coverage diagnostic stays clean while remaining loud on additions.",
+  "intentionally_unmodeled_compiler_attrs": [
+    "boundmass", "boundinertia", "settotalmass", "balanceinertia",
+    "strippath", "coordinate", "fitaabb", "discardvisual",
+    "usethread", "fusestatic", "inertiafromgeom", "inertiagrouprange",
+    "saveinertial", "texturedir", "alignfree"
+  ],
+
+  "_note_intentionally_default_typed_attrs": "Attrs that legitimately want default_type ('float') — single scalar floats whose UE shape happens to match the rule fallback. Listed explicitly so the new_attr_typing diagnostic stays clean. Any new MuJoCo XML attr added in a future version that ISN'T a scalar float will surface as a diagnostic.",
+  "intentionally_default_typed_attrs": [
+    "active", "actuatorgravcomp", "area", "armature", "bias",
+    "cranklength", "cutoff", "dampratio", "density", "diameter",
+    "force", "fovy", "fpmax", "frictionloss", "fvmax",
+    "gain", "gap", "gravcomp", "inheritrange", "ipd",
+    "kp", "kv", "lmax", "lmin", "margin",
+    "mass", "noise", "output", "radius", "ref",
+    "resistance", "solmix", "springref", "tausmooth", "torquescale",
+    "vmax", "width"
+  ],
+
+  "_note_intentionally_unmapped_mj_enum_values": "MuJoCo enum constants the codegen knowingly does NOT expose as URLab enum members. Silences the hand_enum_drift diagnostic. Two scopes: top-level keys are mjt* enum names whose entries apply to EVERY element using that enum (visualizer-only types, sentinel counts, alternative-class subsumption); '__per_element__' carries per-(element, attr) overrides for cases where the same mjt* enum admits a different subset depending on the host (e.g. <site> rejects mjGEOM_PLANE/HFIELD/MESH but <geom> accepts them).",
+  "intentionally_unmapped_mj_enum_values": {
+    "mjtGain":  ["mjGAIN_DCMOTOR"],
+    "mjtBias":  ["mjBIAS_DCMOTOR"],
+    "mjtDyn":   ["mjDYN_DCMOTOR"],
+    "mjtGeom":  [
+      "mjNGEOMTYPES",
+      "mjGEOM_ARROW", "mjGEOM_ARROW1", "mjGEOM_ARROW2",
+      "mjGEOM_LINE", "mjGEOM_LINEBOX", "mjGEOM_LABEL", "mjGEOM_TRIANGLE",
+      "mjGEOM_FLEX", "mjGEOM_SKIN", "mjGEOM_SDF", "mjGEOM_NONE"
+    ],
+    "__per_element__": {
+      "_note_site_type": "Site exposes Mesh + Hfield. Plane stays omitted because a site is a point/extent surface, not an infinite plane; SDF stays omitted because it carries heavy resource data the spawn-surface UX doesn't need.",
+      "site.type": ["mjGEOM_PLANE"]
+    }
+  },
+
+  "_note_intentionally_unmodeled_mjs_fields": "Per-mjsX-struct list of fields URLab knows about but doesn't model via codegen. Silences the mjsX-field-drift diagnostic. Add entries for fields handled by a different UE component, by canon, or that we deliberately skip.",
+  "intentionally_unmodeled_mjs_fields": {
+    "mjsBody": [
+      "inertia", "ipos", "ialt", "mass", "iquat", "fullinertia", "alt",
+      "plugin"
+    ],
+    "mjsFrame": [
+      "pos", "quat", "childclass", "alt"
+    ],
+    "mjsFlex": [
+      "solimp", "solmix", "contype", "activelayers", "edgedamping", "elem",
+      "size", "passive", "condim", "order", "nodebody",
+      "vertbody", "elemtexcoord", "selfcollide", "poisson", "vert", "margin",
+      "conaffinity", "damping", "priority", "edgestiffness", "node",
+      "solref", "gap", "elastic2d", "young", "friction", "thickness", "internal"
+    ],
+    "mjsCamera": [
+      "intrinsic", "alt"
+    ],
+    "mjsGeom": [
+      "alt", "plugin"
+    ],
+    "mjsSite": [
+      "alt"
+    ],
+    "mjsActuator": [
+      "plugin"
+    ],
+    "mjsJoint": [
+      "align"
+    ],
+    "mjsSensor": [
+      "datatype", "dim", "intprm", "needstage", "objname", "plugin",
+      "refname", "reftype"
+    ]
+  },
+
   "type_mappings": {
     "axis":              "FVector",
     "dir":               "FVector",
@@ -59,9 +149,7 @@
     "origin":            "TArray<float>",
     "dim":               "int32",
     "rgba":              "FLinearColor",
-    "rgba2":             "FLinearColor",
     "range":             "TArray<float>",
-    "actfrcrange":       "TArray<float>",
     "actuatorfrcrange":  "TArray<float>",
     "ctrlrange":         "TArray<float>",
     "forcerange":        "TArray<float>",
@@ -80,9 +168,7 @@
     "thermal":           "TArray<float>",
     "lugre":             "TArray<float>",
     "stiffness":         "TArray<float>",
-    "stiffnesspoly":     "TArray<float>",
     "damping":           "TArray<float>",
-    "dampingpoly":       "TArray<float>",
     "friction":          "TArray<float>",
     "fullinertia":       "TArray<float>",
     "diaginertia":       "TArray<float>",
@@ -99,24 +185,18 @@
     "biasprm":           "TArray<float>",
     "dynprm":            "TArray<float>",
     "polycoef":          "TArray<float>",
-    "intprm":            "TArray<int32>",
     "limited":           "bool",
     "mocap":             "bool",
     "actlimited":        "bool",
     "ctrllimited":       "bool",
     "forcelimited":      "bool",
     "actuatorfrclimited":"bool",
-    "actgravcomp":       "bool",
     "actearly":          "bool",
     "shellinertia":      "bool",
-    "fitscale":          "bool",
+    "fitscale":          "double",
     "fitaabb":           "bool",
     "discardvisual":     "bool",
-    "needstage":         "FString",
-    "datatype":          "FString",
-    "objtype":           "FString",
     "objname":           "FString",
-    "reftype":           "FString",
     "refname":           "FString",
     "gaintype":          "FString",
     "biastype":          "FString",
@@ -146,7 +226,6 @@
     "file":              "FString",
     "target":            "FString",
     "mode":              "FString",
-    "side":              "FString",
     "anchor":            "TArray<float>",
     "relpose":           "TArray<float>",
     "ctrl":              "TArray<float>",
@@ -183,18 +262,14 @@
       "export_helper":  "MjUtils::UEToMjRotation"
     },
     "actuator_transmission": {
-      "_note": "MJCF actuators encode their transmission via one of {joint, tendon, site, body, jointinparent} (the target name) plus optional {slidersite, refsite}. URLab collapses this to a single EMjActuatorTrnType enum + TargetName/SliderSite/RefSite strings. Codegen emits the parse+write blocks; the UPROPERTY declarations stay hand-side on UMjActuator.",
+      "_note": "MJCF actuators encode their transmission via one of {joint, tendon, site, body, jointinparent} (the target name) plus optional {slidersite, refsite}. URLab collapses this to a single EMjActuatorTrnType enum + TargetName/SliderSite/RefSite strings. Codegen emits the parse+write blocks inline (no rule-side helper); the UPROPERTY declarations stay hand-side on UMjActuator.",
       "absorbs_attrs": ["joint", "tendon", "site", "body", "jointinparent", "slidersite", "refsite"],
-      "emits_property": null,
-      "import_helper":  "(inline)",
-      "export_helper":  "(inline)"
+      "emits_property": null
     },
     "body_sleep_policy": {
-      "_note": "MJCF body.sleep is one of {'never','allowed','init'} or absent; mjsBody.sleep is the mjtSleepPolicy enum. URLab exposes EMjBodySleepPolicy SleepPolicy (with a Default entry that means 'let the global option decide'). Codegen emits the string<->enum mapping on both sides; the SleepPolicy UPROPERTY itself stays declared hand-side on UMjBody.",
+      "_note": "MJCF body.sleep is one of {'never','allowed','init'} or absent; mjsBody.sleep is the mjtSleepPolicy enum. URLab exposes EMjBodySleepPolicy SleepPolicy (with a Default entry that means 'let the global option decide'). Codegen emits the string<->enum mapping on both sides inline (no rule-side helper); the SleepPolicy UPROPERTY itself stays declared hand-side on UMjBody.",
       "absorbs_attrs": ["sleep"],
-      "emits_property": null,
-      "import_helper":  "(inline)",
-      "export_helper":  "(inline)"
+      "emits_property": null
     },
     "fromto_decompose": {
       "_note": "MJCF's `fromto` is an alternative form for capsule/cylinder/box/ellipsoid (an XY-XY segment). Decompose at import into Pos/Quat plus a half-length scalar written into size[1] (Y slot, e.g. capsule half-length) or size[2] (Z slot, e.g. box half-height) depending on shape Type. The codegen-owned size TArray<float> is grown as needed.",
@@ -215,7 +290,7 @@
   "element_rules": {
     "actuator": {
       "_note_enums": "gaintype/biastype/dyntype are mjtGain/mjtBias/mjtDyn enums on mjsActuator. URLab declares EMjGainType/EMjBiasType/EMjDynType hand-side on UMjActuator; codegen owns the XML<->enum map via xml_enum_attrs. dcmotor 'input' is per-subtype (only on UMjDcMotorActuator) — emitter gates on attr-in-subtype-schema for that case.",
-      "exclude_attrs": [],
+      "common_imports": ["class"],
       "applies_canonicalizations": ["actuator_transmission"],
       "xml_enum_attrs": {
         "gaintype": {
@@ -223,6 +298,8 @@
           "ue_enum_type": "EMjGainType",
           "mjs_field":    "gaintype",
           "mjs_cast":     "mjtGain",
+          "emit_property_decl": true,
+          "property_default":   "Fixed",
           "value_map": {
             "fixed":  ["Fixed",  "mjGAIN_FIXED"],
             "affine": ["Affine", "mjGAIN_AFFINE"],
@@ -235,6 +312,8 @@
           "ue_enum_type": "EMjBiasType",
           "mjs_field":    "biastype",
           "mjs_cast":     "mjtBias",
+          "emit_property_decl": true,
+          "property_default":   "None",
           "value_map": {
             "none":   ["None",   "mjBIAS_NONE"],
             "affine": ["Affine", "mjBIAS_AFFINE"],
@@ -247,6 +326,8 @@
           "ue_enum_type": "EMjDynType",
           "mjs_field":    "dyntype",
           "mjs_cast":     "mjtDyn",
+          "emit_property_decl": true,
+          "property_default":   "None",
           "value_map": {
             "none":        ["None",        "mjDYN_NONE"],
             "integrator":  ["Integrator",  "mjDYN_INTEGRATOR"],
@@ -269,13 +350,81 @@
       }
     },
     "sensor": {
-      "exclude_attrs": []
+      "_note": "Sensor reads its target name from one of several xor attrs depending on subtype (site for touch/accel/gyro, joint for jointpos, etc.). target_collations collapses them into UE FString TargetName. ReferenceName is direct from 'refname'. ObjType / RefType are EMjObjType enums with their own string<->enum mapping (xml_enum_attrs).",
+      "common_imports": ["class"],
+      "target_collations": {
+        "TargetName": {
+          "mjs_field": "objname",
+          "absorbs_attrs": ["site", "joint", "tendon", "actuator", "body", "geom", "objname", "camera", "mesh"]
+        },
+        "ReferenceName": {
+          "mjs_field": "refname",
+          "absorbs_attrs": ["refname"]
+        }
+      },
+      "xml_enum_attrs": {
+        "objtype": {
+          "ue_property":  "ObjType",
+          "ue_enum_type": "EMjObjType",
+          "has_override_toggle": false,
+          "value_map": {
+            "body":     ["Body",     "mjOBJ_BODY"],
+            "xbody":    ["XBody",    "mjOBJ_XBODY"],
+            "joint":    ["Joint",    "mjOBJ_JOINT"],
+            "dof":      ["DoF",      "mjOBJ_DOF"],
+            "geom":     ["Geom",     "mjOBJ_GEOM"],
+            "site":     ["Site",     "mjOBJ_SITE"],
+            "camera":   ["Camera",   "mjOBJ_CAMERA"],
+            "light":    ["Light",    "mjOBJ_LIGHT"],
+            "mesh":     ["Mesh",     "mjOBJ_MESH"],
+            "hfield":   ["HField",   "mjOBJ_HFIELD"],
+            "texture":  ["Texture",  "mjOBJ_TEXTURE"],
+            "material": ["Material", "mjOBJ_MATERIAL"],
+            "pair":     ["Pair",     "mjOBJ_PAIR"],
+            "exclude":  ["Exclude",  "mjOBJ_EXCLUDE"],
+            "equality": ["Equality", "mjOBJ_EQUALITY"],
+            "tendon":   ["Tendon",   "mjOBJ_TENDON"],
+            "actuator": ["Actuator", "mjOBJ_ACTUATOR"]
+          }
+        },
+        "reftype": {
+          "ue_property":  "RefType",
+          "ue_enum_type": "EMjObjType",
+          "has_override_toggle": false,
+          "value_map": {
+            "body":     ["Body",     "mjOBJ_BODY"],
+            "xbody":    ["XBody",    "mjOBJ_XBODY"],
+            "joint":    ["Joint",    "mjOBJ_JOINT"],
+            "geom":     ["Geom",     "mjOBJ_GEOM"],
+            "site":     ["Site",     "mjOBJ_SITE"],
+            "camera":   ["Camera",   "mjOBJ_CAMERA"]
+          }
+        }
+      }
     },
     "joint": {
-      "exclude_attrs": ["pos", "axis"],
-      "extra_property_inits": {
-        "Pos":  "FVector::ZeroVector",
-        "Axis": "FVector(0.0f, 0.0f, 1.0f)"
+      "_note_axis_pos": "axis: MuJoCo direction vector with opposite Y handedness vs UE; vec3_convert.axis = y_negate flips on import + export. pos: spatial_pose canon emits the Pos UPROPERTY pair + drives SetRelativeLocation on import.",
+      "common_imports": ["class"],
+      "applies_canonicalizations": ["spatial_pose"],
+      "vec3_convert": {"axis": "y_negate"},
+      "property_defaults": {"axis": "FVector(0.0f, 0.0f, 1.0f)"},
+      "_note_hidden": "Pos is the joint anchor offset — derived from the UE component's RelativeLocation. Axis stays visible as a direction (not a position) and is the user-authored joint hinge direction.",
+      "hidden_canon_properties": ["spatial_pose"],
+      "_note_property_meta": "Joint fields whose unit semantics shift with the joint Type get a tooltip that spells out the per-type interpretation, so a user reading the Details panel knows whether the number is metres or degrees. UE's Units meta is intentionally NOT used here — a static label would be wrong for the other Type.",
+      "property_meta": {
+        "axis":             "ToolTip=\"Joint axis in the parent body's local frame. Hinge: rotation axis. Slide: translation direction. Ignored for Ball and Free.\"",
+        "range":            "ToolTip=\"Joint position bounds [min, max]. Hinge/Ball: degrees. Slide: centimetres. Ignored when 'limited' is false.\"",
+        "ref":              "ToolTip=\"Reference (zero) position. Hinge/Ball: degrees. Slide: centimetres.\"",
+        "springref":        "ToolTip=\"Equilibrium position for the joint spring. Hinge/Ball: degrees. Slide: centimetres.\"",
+        "stiffness":        "ToolTip=\"Per-axis spring stiffness. Hinge/Ball: N·m/rad. Slide: N/m.\"",
+        "damping":          "ToolTip=\"Per-axis damping. Hinge/Ball: N·m·s/rad. Slide: N·s/m.\"",
+        "springdamper":     "ToolTip=\"Compact spring spec [stiffness, damping]; populated when MJCF used <joint springdamper='...'>. Overrides stiffness + damping when set.\"",
+        "armature":         "ToolTip=\"Added rotor inertia for hinges (kg·m²) or added mass for slides (kg). Models reflected inertia of the actuator.\"",
+        "frictionloss":     "ToolTip=\"Dry-friction loss. Hinge/Ball: N·m. Slide: N.\"",
+        "actuatorgravcomp": "ToolTip=\"Gravity compensation factor passed to actuators on this joint. 0 = no compensation, 1 = full gravity cancelled.\"",
+        "actuatorfrcrange": "ToolTip=\"Actuator force bounds [min, max] when 'ActFrcLimited' is true. Hinge/Ball: N·m. Slide: N.\"",
+        "margin":           "ToolTip=\"Contact-detection margin (m). Joints inherit this when computing limit constraints.\"",
+        "group":            "ToolTip=\"Visualisation group 0–5. MuJoCo viewer uses this to toggle visibility.\""
       },
       "xml_enum_attrs": {
         "type": {
@@ -315,12 +464,20 @@
     },
     "geom": {
       "_note": "size is codegen-owned TArray<float>; fromto-decomposed half-length is written into size[1]/[2] by the canon. material is codegen-owned for IMPORT (FString -> UE material lookup) but SKIPPED on export because URLab handles materials as UE assets and does not register them in the mjsSpec (mjs_setString on Element->material would reference a non-existent spec entry and fail mj_compile). Orientation cluster absorbed by `orientation`; fromto absorbed by `fromto_decompose`. type is an URLab enum (EMjGeomType); codegen owns the XML<->enum string map via xml_enum_attrs.",
-      "exclude_attrs": [],
+      "common_imports": ["class"],
       "_note_hfield": "hfield references URLab's own hfield actor system, not a mjsHField spec entry. Codegen still imports the string for editor visibility but skips the spec write to avoid 'hfield not found' on compile.",
       "export_skip_attrs": ["material", "hfield"],
       "applies_canonicalizations": ["spatial_pose", "orientation", "fromto_decompose"],
       "_note_sentinel": "fromto-only XML inputs leave the radius slot unset; the canon writes -1.0f as the sentinel and the per-attr size export skips those slots so the parent default's radius is preserved.",
       "sentinel_skip_export": {"size": "-1.0f"},
+      "_note_property_units": "Scalar lengths exposed in mjsGeom carry meta=(Units=\"m\") so the editor shows a 'm' suffix or auto-converts to cm/mm when 'Display Units' is enabled. Storage stays raw MJ metres; export path unaffected. size is hidden (Scale3D is canonical) — see hidden_attrs.",
+      "property_units": {
+        "margin": "m",
+        "gap":    "m"
+      },
+      "_note_hidden": "See _doc_transform_widget_policy. size is hidden too — Scale3D is canonical.",
+      "hidden_canon_properties": ["spatial_pose", "orientation"],
+      "hidden_attrs": ["size"],
       "_note_attr_to_mjs_field": "MuJoCo renames fluidcoef -> fluid_coefs on the mjsGeom struct. shellinertia and fluidshape are handled by xml_enum_attrs below (they need string<->enum + field-rename together).",
       "attr_to_mjs_field": {
         "fluidcoef": "fluid_coefs"
@@ -369,10 +526,12 @@
     },
     "site": {
       "_note": "Same as geom: size codegen-owned TArray; material codegen-owned for IMPORT but export-skipped (UE-side materials); orientation + fromto canonicalized. mjsSite.type uses mjtGeom (sites are visual 'geoms'). Site has no bOverride_Type — Type always carries a value (default Sphere).",
-      "exclude_attrs": [],
       "export_skip_attrs": ["material"],
       "applies_canonicalizations": ["spatial_pose", "orientation", "fromto_decompose"],
       "sentinel_skip_export": {"size": "-1.0f"},
+      "_note_hidden": "See _doc_transform_widget_policy. size hidden too — Scale3D is canonical.",
+      "hidden_canon_properties": ["spatial_pose", "orientation"],
+      "hidden_attrs": ["size"],
       "xml_enum_attrs": {
         "type": {
           "ue_property":  "Type",
@@ -385,15 +544,21 @@
             "capsule":   ["Capsule",   "mjGEOM_CAPSULE"],
             "ellipsoid": ["Ellipsoid", "mjGEOM_ELLIPSOID"],
             "cylinder":  ["Cylinder",  "mjGEOM_CYLINDER"],
-            "box":       ["Box",       "mjGEOM_BOX"]
+            "box":       ["Box",       "mjGEOM_BOX"],
+            "mesh":      ["Mesh",      "mjGEOM_MESH"],
+            "hfield":    ["Hfield",    "mjGEOM_HFIELD"]
           }
         }
       }
     },
     "tendon": {
+      "common_imports": ["class"],
       "_note_material": "material codegen-owned for IMPORT but export-skipped — URLab handles materials as UE assets, not mjsMaterial spec entries.",
-      "exclude_attrs": [],
-      "export_skip_attrs": ["material"]
+      "export_skip_attrs": ["material"],
+      "_note_property_units": "springlength is unambiguously a length in metres. range is intentionally NOT tagged — it can be a length (slide-mode tendon) or an angle (rare hinge-coupled tendon) and labelling it with one unit would be wrong as often as right.",
+      "property_units": {
+        "springlength": "m"
+      }
     },
     "equality": {
       "_note_target_collation": "MJCF equality elements address one or two of {body, site, joint, tendon, flex} per the equality kind. URLab collapses to FString Obj1/Obj2 (hand-declared UPROPERTYs with dynamic GetOptions based on EqualityType). Excluded from codegen so they don't appear as separate UPROPERTYs; target_collations rule emits the import collation + mjs_setString export. site1/site2 stay as their own UPROPERTYs (codegen-owned) so the objtype switch in ExportTo can detect site-mode connects/welds and pick mjOBJ_SITE.",
@@ -416,6 +581,10 @@
            "import_op": "m_to_cm"}
         ]
       },
+      "_note_property_units": "anchor is stored in UE centimetres (post unit_conversion). Tagging the field with Units=\"cm\" so the editor shows the suffix that matches the actual stored value — no auto-conversion needed.",
+      "property_units": {
+        "anchor": "cm"
+      },
       "mjs_data_packed_attrs": {
         "anchor": {
           "slot_range": [0, 3],
@@ -439,14 +608,25 @@
     },
     "camera": {
       "_note": "MJCF camera.mode is a tracking mode (mjtCamLight subset). URLab declares EMjCameraTrackingMode hand-side; codegen owns the XML<->enum map. NOT to be confused with URLab's EMjCameraMode which is a UE-side render-mode selector (RGB/Depth/Segmentation).",
-      "exclude_attrs": [],
+      "_note_common_imports": "Camera also reads name; the hand-written ImportFromXml has a fallback `if MjName empty -> 'Camera'` that runs AFTER this read.",
+      "common_imports": ["name"],
       "applies_canonicalizations": ["spatial_pose", "orientation"],
+      "_note_hidden": "See _doc_transform_widget_policy. Intrinsics (fovy, ipd, focal, etc.) stay visible.",
+      "hidden_canon_properties": ["spatial_pose", "orientation"],
       "_note_attr_to_mjs_field": "MuJoCo renames a handful of camera attrs between the XML schema and the mjsCamera struct: target -> targetbody (mjString*), projection -> proj (mjtProjection enum), focal -> focal_length, principal -> principal_length. sensorsize auto-resolves to sensor_size via the underscore-normalise heuristic. Without this map the auto-resolver in _resolve_mjs_field can't bridge target/focal/principal and they were silently dropped on export.",
       "attr_to_mjs_field": {
         "target": "targetbody",
         "focal": "focal_length",
         "principal": "principal_length"
       },
+      "_note_property_units": "Camera lengths (ipd, focal, principal, sensorsize) stay in raw MJ metres on the UE side; tagging them with Units=\"m\" gives the editor a suffix and lets Display Units convert to mm/cm. fovy is degrees per the MJCF spec. focalpixel / principalpixel are pixel counts (not lengths) and are intentionally untagged.",
+      "property_units": {
+        "ipd":        "m",
+        "focal":      "m",
+        "principal":  "m",
+        "sensorsize": "m",
+        "fovy":       "deg"
+      },
       "xml_enum_attrs": {
         "mode": {
           "ue_property":  "TrackingMode",
@@ -474,32 +654,39 @@
       }
     },
     "body": {
-      "exclude_attrs": [],
+      "common_imports": ["name"],
       "applies_canonicalizations": ["spatial_pose", "orientation", "body_sleep_policy"],
       "_note_canon_export_skip": "Body's pos/quat are written by MjSpecWrapper::CreateBody from the UE component transform (the source of truth for body placement). The codegen-emitted spatial_pose/orientation export would overwrite those with the Pos/Quat UPROPERTYs, which are stale for any body not freshly imported from XML. Skip the export side for these two canons.",
       "canon_export_skip": ["spatial_pose", "orientation"],
+      "_note_hidden": "See _doc_transform_widget_policy. canon_export_skip routes the write through the UE transform.",
+      "hidden_canon_properties": ["spatial_pose", "orientation"],
       "property_meta": {
         "mocap":      "DisplayName=\"Driven By Unreal\"",
         "childclass": "GetOptions=\"GetChildClassOptions\""
       }
     },
     "inertial": {
-      "exclude_attrs": [],
-      "applies_canonicalizations": ["spatial_pose", "orientation"]
+      "applies_canonicalizations": ["spatial_pose", "orientation"],
+      "_note_hidden": "See _doc_transform_widget_policy. UMjInertial::RegisterToSpec (hand-rolled) writes ParentBody->ipos/iquat from the UE transform.",
+      "hidden_canon_properties": ["spatial_pose", "orientation"]
     },
     "frame": {
       "_note": "Like body: the UE component transform is the source of truth for placement. Codegen imports XML pos/quat into Pos/Quat UPROPERTYs (UE units / UE frame) and the cpp drives SetRelativeLocation/SetRelativeRotation from them. Spec export is skipped for the canon Pos/Quat because MjSpecWrapper::CreateFrame writes pos/quat from the UE transform.",
-      "exclude_attrs": [],
       "applies_canonicalizations": ["spatial_pose", "orientation"],
-      "canon_export_skip": ["spatial_pose", "orientation"]
+      "canon_export_skip": ["spatial_pose", "orientation"],
+      "hidden_canon_properties": ["spatial_pose", "orientation"]
     },
     "flexcomp": {
       "_note": "Flexcomp doesn't register via mjs_addX — at spec time UMjFlexcomp builds a tiny standalone MJCF fragment and re-parses it via mj_parseXMLString + mjs_attach (the flexcomp macro is expanded by MuJoCo's XML parser, not the spec API). Codegen still owns the UPROPERTY surface so new MJCF attrs auto-appear; BuildFlexcompXml in MjFlexcomp.cpp reads from those codegen-owned fields. Sub-elements (<contact>/<edge>/<elasticity>/<pin>) remain hand-rolled because the codegen doesn't have per-sub-element UPROPERTY support yet.",
       "_note_material": "material codegen-owned for IMPORT but export-skipped — URLab handles materials as UE assets, not mjsMaterial spec entries.",
-      "exclude_attrs": [],
       "export_skip_attrs": ["material"],
       "applies_canonicalizations": ["spatial_pose", "orientation"],
       "canon_export_skip": ["spatial_pose", "orientation"],
+      "hidden_canon_properties": ["spatial_pose", "orientation"],
+      "property_units": {
+        "radius":  "m",
+        "spacing": "m"
+      },
       "xml_enum_attrs": {
         "type": {
           "ue_property":  "FlexcompType",
@@ -535,17 +722,19 @@
       }
     },
     "freejoint": {
-      "exclude_attrs": []
     },
     "contact_pair": {
-      "exclude_attrs": []
+      "common_imports": [{"attr": "name", "ue_field": "Name"}],
+      "property_units": {
+        "margin": "m",
+        "gap":    "m"
+      }
     },
     "contact_exclude": {
-      "exclude_attrs": []
+      "common_imports": [{"attr": "name", "ue_field": "Name"}]
     },
     "keyframe": {
       "_note": "qpos/qvel/act/ctrl/mpos/mquat target mjsKey's mjDoubleVec* fields (std::vector<double>*) — require ->clear()+->push_back() rather than direct assignment. Codegen emits the import (TArray<float> read) + the mjs_double_vec-style export. RegisterToSpec hand-rolls freejoint-padding for qpos/qvel.",
-      "exclude_attrs": [],
       "mjs_double_vec_attrs": ["qpos", "qvel", "act", "ctrl", "mpos", "mquat"]
     }
   },
@@ -574,7 +763,7 @@
         {"key": "adhesion",    "enum_value": "Adhesion",   "class_name": "UMjAdhesionActuator",    "header": "MjAdhesionActuator.h"},
         {"key": "dcmotor",     "enum_value": "DcMotor",    "class_name": "UMjDcMotorActuator",     "header": "MjDcMotorActuator.h"}
       ],
-      "_note_subtype_setto": "Codegen-driven mjs_setTo* call config. Function signatures are scraped into Scripts/mjspec_snapshot.json by build_mjspec_snapshot.py; the generator emits the call from the signature + the per-subtype config below. NO hand-written C++ literals in this file.",
+      "_note_subtype_setto": "Codegen-driven mjs_setTo* call config. Function signatures come from the clang-AST introspect snapshot, projected by _mjspec_from_introspect; the generator emits the call from the signature + the per-subtype config below. NO hand-written C++ literals in this file.",
       "subtype_setto": {
         "general":     null,
         "motor":       {"call": "mjs_setToMotor"},
@@ -604,55 +793,57 @@
       "schema_subtypes_block": "sensor_types",
       "type_enum_name": "EMjSensorType",
       "subtypes": [
-        {"key": "touch",            "enum_value": "Touch",            "class_name": "UMjTouchSensor",          "header": "MjTouchSensor.h"},
-        {"key": "accelerometer",    "enum_value": "Accelerometer",    "class_name": "UMjAccelerometer",        "header": "MjAccelerometer.h"},
-        {"key": "velocimeter",      "enum_value": "Velocimeter",      "class_name": "UMjVelocimeter",          "header": "MjVelocimeter.h"},
-        {"key": "gyro",             "enum_value": "Gyro",             "class_name": "UMjGyro",                 "header": "MjGyro.h"},
-        {"key": "force",            "enum_value": "Force",            "class_name": "UMjForceSensor",          "header": "MjForceSensor.h"},
-        {"key": "torque",           "enum_value": "Torque",           "class_name": "UMjTorqueSensor",         "header": "MjTorqueSensor.h"},
-        {"key": "magnetometer",     "enum_value": "Magnetometer",     "class_name": "UMjMagnetometer",         "header": "MjMagnetometer.h"},
-        {"key": "camprojection",    "enum_value": "CamProjection",    "class_name": "UMjCamProjectionSensor",  "header": "MjCamProjectionSensor.h"},
-        {"key": "rangefinder",      "enum_value": "RangeFinder",      "class_name": "UMjRangeFinderSensor",    "header": "MjRangeFinderSensor.h"},
+        {"key": "touch",            "enum_value": "Touch",            "class_name": "UMjTouchSensor",          "header": "MjTouchSensor.h", "fully_emitted": true},
+        {"key": "accelerometer",    "enum_value": "Accelerometer",    "class_name": "UMjAccelerometer",        "header": "MjAccelerometer.h", "fully_emitted": true},
+        {"key": "velocimeter",      "enum_value": "Velocimeter",      "class_name": "UMjVelocimeter",          "header": "MjVelocimeter.h", "fully_emitted": true},
+        {"key": "gyro",             "enum_value": "Gyro",             "class_name": "UMjGyro",                 "header": "MjGyro.h", "fully_emitted": true},
+        {"key": "force",            "enum_value": "Force",            "class_name": "UMjForceSensor",          "header": "MjForceSensor.h", "fully_emitted": true},
+        {"key": "torque",           "enum_value": "Torque",           "class_name": "UMjTorqueSensor",         "header": "MjTorqueSensor.h", "fully_emitted": true},
+        {"key": "magnetometer",     "enum_value": "Magnetometer",     "class_name": "UMjMagnetometer",         "header": "MjMagnetometer.h", "fully_emitted": true},
+        {"key": "camprojection",    "enum_value": "CamProjection",    "class_name": "UMjCamProjectionSensor",  "header": "MjCamProjectionSensor.h", "fully_emitted": true},
+        {"key": "rangefinder",      "enum_value": "RangeFinder",      "class_name": "UMjRangeFinderSensor",    "header": "MjRangeFinderSensor.h",
+         "case_body_override": "Element->type = mjSENS_RANGEFINDER; Element->objtype = (ObjType == EMjObjType::Camera) ? mjOBJ_CAMERA : mjOBJ_SITE;",
+         "fully_emitted": true},
         {"key": "jointpos",         "enum_value": "JointPos",         "class_name": "UMjJointPosSensor",       "header": "MjJointPosSensor.h"},
-        {"key": "jointvel",         "enum_value": "JointVel",         "class_name": "UMjJointVelSensor",       "header": "MjJointVelSensor.h"},
-        {"key": "ballquat",         "enum_value": "BallQuat",         "class_name": "UMjBallQuatSensor",       "header": "MjBallQuatSensor.h"},
-        {"key": "ballangvel",       "enum_value": "BallAngVel",       "class_name": "UMjBallAngVelSensor",     "header": "MjBallAngVelSensor.h"},
-        {"key": "jointlimitpos",    "enum_value": "JointLimitPos",    "class_name": "UMjJointLimitPosSensor",  "header": "MjJointLimitPosSensor.h"},
-        {"key": "jointlimitvel",    "enum_value": "JointLimitVel",    "class_name": "UMjJointLimitVelSensor",  "header": "MjJointLimitVelSensor.h"},
-        {"key": "jointlimitfrc",    "enum_value": "JointLimitFrc",    "class_name": "UMjJointLimitFrcSensor",  "header": "MjJointLimitFrcSensor.h"},
-        {"key": "tendonpos",        "enum_value": "TendonPos",        "class_name": "UMjTendonPosSensor",      "header": "MjTendonPosSensor.h"},
-        {"key": "tendonvel",        "enum_value": "TendonVel",        "class_name": "UMjTendonVelSensor",      "header": "MjTendonVelSensor.h"},
-        {"key": "tendonlimitpos",   "enum_value": "TendonLimitPos",   "class_name": "UMjTendonLimitPosSensor", "header": "MjTendonLimitPosSensor.h"},
-        {"key": "tendonlimitvel",   "enum_value": "TendonLimitVel",   "class_name": "UMjTendonLimitVelSensor", "header": "MjTendonLimitVelSensor.h"},
-        {"key": "tendonlimitfrc",   "enum_value": "TendonLimitFrc",   "class_name": "UMjTendonLimitFrcSensor", "header": "MjTendonLimitFrcSensor.h"},
-        {"key": "actuatorpos",      "enum_value": "ActuatorPos",      "class_name": "UMjActuatorPosSensor",    "header": "MjActuatorPosSensor.h"},
-        {"key": "actuatorvel",      "enum_value": "ActuatorVel",      "class_name": "UMjActuatorVelSensor",    "header": "MjActuatorVelSensor.h"},
-        {"key": "actuatorfrc",      "enum_value": "ActuatorFrc",      "class_name": "UMjActuatorFrcSensor",    "header": "MjActuatorFrcSensor.h"},
-        {"key": "jointactuatorfrc", "enum_value": "JointActFrc",      "class_name": "UMjJointActFrcSensor",    "header": "MjJointActFrcSensor.h"},
-        {"key": "tendonactuatorfrc","enum_value": "TendonActFrc",     "class_name": "UMjTendonActFrcSensor",   "header": "MjTendonActFrcSensor.h"},
-        {"key": "framepos",         "enum_value": "FramePos",         "class_name": "UMjFramePosSensor",       "header": "MjFramePosSensor.h"},
-        {"key": "framequat",        "enum_value": "FrameQuat",        "class_name": "UMjFrameQuatSensor",      "header": "MjFrameQuatSensor.h"},
-        {"key": "framexaxis",       "enum_value": "FrameXAxis",       "class_name": "UMjFrameXAxisSensor",     "header": "MjFrameXAxisSensor.h"},
-        {"key": "frameyaxis",       "enum_value": "FrameYAxis",       "class_name": "UMjFrameYAxisSensor",     "header": "MjFrameYAxisSensor.h"},
-        {"key": "framezaxis",       "enum_value": "FrameZAxis",       "class_name": "UMjFrameZAxisSensor",     "header": "MjFrameZAxisSensor.h"},
-        {"key": "framelinvel",      "enum_value": "FrameLinVel",      "class_name": "UMjFrameLinVelSensor",    "header": "MjFrameLinVelSensor.h"},
-        {"key": "frameangvel",      "enum_value": "FrameAngVel",      "class_name": "UMjFrameAngVelSensor",    "header": "MjFrameAngVelSensor.h"},
-        {"key": "framelinacc",      "enum_value": "FrameLinAcc",      "class_name": "UMjFrameLinAccSensor",    "header": "MjFrameLinAccSensor.h"},
-        {"key": "frameangacc",      "enum_value": "FrameAngAcc",      "class_name": "UMjFrameAngAccSensor",    "header": "MjFrameAngAccSensor.h"},
-        {"key": "subtreecom",       "enum_value": "SubtreeCom",       "class_name": "UMjSubtreeComSensor",     "header": "MjSubtreeComSensor.h"},
-        {"key": "subtreelinvel",    "enum_value": "SubtreeLinVel",    "class_name": "UMjSubtreeLinVelSensor",  "header": "MjSubtreeLinVelSensor.h"},
-        {"key": "subtreeangmom",    "enum_value": "SubtreeAngMom",    "class_name": "UMjSubtreeAngMomSensor",  "header": "MjSubtreeAngMomSensor.h"},
-        {"key": "insidesite",       "enum_value": "InsideSite",       "class_name": "UMjInsideSiteSensor",     "header": "MjInsideSiteSensor.h"},
-        {"key": "distance",         "enum_value": "GeomDist",         "class_name": "UMjGeomDistSensor",       "header": "MjGeomDistSensor.h"},
-        {"key": "normal",           "enum_value": "GeomNormal",       "class_name": "UMjGeomNormalSensor",     "header": "MjGeomNormalSensor.h"},
-        {"key": "fromto",           "enum_value": "GeomFromTo",       "class_name": "UMjGeomFromToSensor",     "header": "MjGeomFromToSensor.h"},
-        {"key": "contact",          "enum_value": "Contact",          "class_name": "UMjContactSensor",        "header": "MjContactSensor.h"},
-        {"key": "e_potential",      "enum_value": "EPotential",       "class_name": "UMjEPotentialSensor",     "header": "MjEPotentialSensor.h"},
-        {"key": "e_kinetic",        "enum_value": "EKinetic",         "class_name": "UMjEKineticSensor",       "header": "MjEKineticSensor.h"},
-        {"key": "clock",            "enum_value": "Clock",            "class_name": "UMjClockSensor",          "header": "MjClockSensor.h"},
+        {"key": "jointvel",         "enum_value": "JointVel",         "class_name": "UMjJointVelSensor",       "header": "MjJointVelSensor.h", "fully_emitted": true},
+        {"key": "ballquat",         "enum_value": "BallQuat",         "class_name": "UMjBallQuatSensor",       "header": "MjBallQuatSensor.h", "fully_emitted": true},
+        {"key": "ballangvel",       "enum_value": "BallAngVel",       "class_name": "UMjBallAngVelSensor",     "header": "MjBallAngVelSensor.h", "fully_emitted": true},
+        {"key": "jointlimitpos",    "enum_value": "JointLimitPos",    "class_name": "UMjJointLimitPosSensor",  "header": "MjJointLimitPosSensor.h", "fully_emitted": true},
+        {"key": "jointlimitvel",    "enum_value": "JointLimitVel",    "class_name": "UMjJointLimitVelSensor",  "header": "MjJointLimitVelSensor.h", "fully_emitted": true},
+        {"key": "jointlimitfrc",    "enum_value": "JointLimitFrc",    "class_name": "UMjJointLimitFrcSensor",  "header": "MjJointLimitFrcSensor.h", "fully_emitted": true},
+        {"key": "tendonpos",        "enum_value": "TendonPos",        "class_name": "UMjTendonPosSensor",      "header": "MjTendonPosSensor.h", "fully_emitted": true},
+        {"key": "tendonvel",        "enum_value": "TendonVel",        "class_name": "UMjTendonVelSensor",      "header": "MjTendonVelSensor.h", "fully_emitted": true},
+        {"key": "tendonlimitpos",   "enum_value": "TendonLimitPos",   "class_name": "UMjTendonLimitPosSensor", "header": "MjTendonLimitPosSensor.h", "fully_emitted": true},
+        {"key": "tendonlimitvel",   "enum_value": "TendonLimitVel",   "class_name": "UMjTendonLimitVelSensor", "header": "MjTendonLimitVelSensor.h", "fully_emitted": true},
+        {"key": "tendonlimitfrc",   "enum_value": "TendonLimitFrc",   "class_name": "UMjTendonLimitFrcSensor", "header": "MjTendonLimitFrcSensor.h", "fully_emitted": true},
+        {"key": "actuatorpos",      "enum_value": "ActuatorPos",      "class_name": "UMjActuatorPosSensor",    "header": "MjActuatorPosSensor.h", "fully_emitted": true},
+        {"key": "actuatorvel",      "enum_value": "ActuatorVel",      "class_name": "UMjActuatorVelSensor",    "header": "MjActuatorVelSensor.h", "fully_emitted": true},
+        {"key": "actuatorfrc",      "enum_value": "ActuatorFrc",      "class_name": "UMjActuatorFrcSensor",    "header": "MjActuatorFrcSensor.h", "fully_emitted": true},
+        {"key": "jointactuatorfrc", "enum_value": "JointActFrc",      "class_name": "UMjJointActFrcSensor",    "header": "MjJointActFrcSensor.h", "fully_emitted": true},
+        {"key": "tendonactuatorfrc","enum_value": "TendonActFrc",     "class_name": "UMjTendonActFrcSensor",   "header": "MjTendonActFrcSensor.h", "fully_emitted": true},
+        {"key": "framepos",         "enum_value": "FramePos",         "class_name": "UMjFramePosSensor",       "header": "MjFramePosSensor.h", "fully_emitted": true},
+        {"key": "framequat",        "enum_value": "FrameQuat",        "class_name": "UMjFrameQuatSensor",      "header": "MjFrameQuatSensor.h", "fully_emitted": true},
+        {"key": "framexaxis",       "enum_value": "FrameXAxis",       "class_name": "UMjFrameXAxisSensor",     "header": "MjFrameXAxisSensor.h", "fully_emitted": true},
+        {"key": "frameyaxis",       "enum_value": "FrameYAxis",       "class_name": "UMjFrameYAxisSensor",     "header": "MjFrameYAxisSensor.h", "fully_emitted": true},
+        {"key": "framezaxis",       "enum_value": "FrameZAxis",       "class_name": "UMjFrameZAxisSensor",     "header": "MjFrameZAxisSensor.h", "fully_emitted": true},
+        {"key": "framelinvel",      "enum_value": "FrameLinVel",      "class_name": "UMjFrameLinVelSensor",    "header": "MjFrameLinVelSensor.h", "fully_emitted": true},
+        {"key": "frameangvel",      "enum_value": "FrameAngVel",      "class_name": "UMjFrameAngVelSensor",    "header": "MjFrameAngVelSensor.h", "fully_emitted": true},
+        {"key": "framelinacc",      "enum_value": "FrameLinAcc",      "class_name": "UMjFrameLinAccSensor",    "header": "MjFrameLinAccSensor.h", "fully_emitted": true},
+        {"key": "frameangacc",      "enum_value": "FrameAngAcc",      "class_name": "UMjFrameAngAccSensor",    "header": "MjFrameAngAccSensor.h", "fully_emitted": true},
+        {"key": "subtreecom",       "enum_value": "SubtreeCom",       "class_name": "UMjSubtreeComSensor",     "header": "MjSubtreeComSensor.h", "fully_emitted": true},
+        {"key": "subtreelinvel",    "enum_value": "SubtreeLinVel",    "class_name": "UMjSubtreeLinVelSensor",  "header": "MjSubtreeLinVelSensor.h", "fully_emitted": true},
+        {"key": "subtreeangmom",    "enum_value": "SubtreeAngMom",    "class_name": "UMjSubtreeAngMomSensor",  "header": "MjSubtreeAngMomSensor.h", "fully_emitted": true},
+        {"key": "insidesite",       "enum_value": "InsideSite",       "class_name": "UMjInsideSiteSensor",     "header": "MjInsideSiteSensor.h", "fully_emitted": true},
+        {"key": "distance",         "enum_value": "GeomDist",         "class_name": "UMjGeomDistSensor",       "header": "MjGeomDistSensor.h", "fully_emitted": true},
+        {"key": "normal",           "enum_value": "GeomNormal",       "class_name": "UMjGeomNormalSensor",     "header": "MjGeomNormalSensor.h", "fully_emitted": true},
+        {"key": "fromto",           "enum_value": "GeomFromTo",       "class_name": "UMjGeomFromToSensor",     "header": "MjGeomFromToSensor.h", "fully_emitted": true},
+        {"key": "contact",          "enum_value": "Contact",          "class_name": "UMjContactSensor",        "header": "MjContactSensor.h", "fully_emitted": true},
+        {"key": "e_potential",      "enum_value": "EPotential",       "class_name": "UMjEPotentialSensor",     "header": "MjEPotentialSensor.h", "fully_emitted": true},
+        {"key": "e_kinetic",        "enum_value": "EKinetic",         "class_name": "UMjEKineticSensor",       "header": "MjEKineticSensor.h", "fully_emitted": true},
+        {"key": "clock",            "enum_value": "Clock",            "class_name": "UMjClockSensor",          "header": "MjClockSensor.h", "fully_emitted": true},
         {"key": "tactile",          "enum_value": "Tactile",          "class_name": "UMjTactileSensor",        "header": "MjTactileSensor.h"},
-        {"key": "user",             "enum_value": "User",             "class_name": "UMjUserSensor",           "header": "MjUserSensor.h"},
-        {"key": "plugin",           "enum_value": "Plugin",           "class_name": "UMjPluginSensor",         "header": "MjPluginSensor.h"}
+        {"key": "user",             "enum_value": "User",             "class_name": "UMjUserSensor",           "header": "MjUserSensor.h", "fully_emitted": true},
+        {"key": "plugin",           "enum_value": "Plugin",           "class_name": "UMjPluginSensor",         "header": "MjPluginSensor.h", "fully_emitted": true}
       ]
     },
 
@@ -666,9 +857,13 @@
       "schema_common_block": "joint.attrs",
       "type_enum_name": "EMjJointType",
       "subtypes": [
-        {"key": "hinge", "enum_value": "Hinge", "class_name": "UMjHingeJoint", "header": "MjHingeJoint.h"},
-        {"key": "slide", "enum_value": "Slide", "class_name": "UMjSlideJoint", "header": "MjSlideJoint.h"},
-        {"key": "ball",  "enum_value": "Ball",  "class_name": "UMjBallJoint",  "header": "MjBallJoint.h"},
+        {"key": "hinge", "enum_value": "Hinge", "class_name": "UMjHingeJoint", "header": "MjHingeJoint.h", "fully_emitted": true,
+         "_note_extra_ctor": "UMjJoint::ExportTo only writes Element->type when bOverride_Type is true. Without this flag set in the subclass ctor, BP-spawned joints fall back to the mjsJoint default (mjJNT_HINGE), silently substituting Ball/Slide joints for Hinge.",
+         "extra_constructor": "bOverride_Type = true;"},
+        {"key": "slide", "enum_value": "Slide", "class_name": "UMjSlideJoint", "header": "MjSlideJoint.h", "fully_emitted": true,
+         "extra_constructor": "bOverride_Type = true;"},
+        {"key": "ball",  "enum_value": "Ball",  "class_name": "UMjBallJoint",  "header": "MjBallJoint.h", "fully_emitted": true,
+         "extra_constructor": "bOverride_Type = true;"},
         {"key": "free",  "enum_value": "Free",  "class_name": "UMjFreeJoint",  "header": "MjFreeJoint.h"}
       ]
     },
@@ -681,11 +876,11 @@
       "schema_common_block": "site.attrs",
       "type_enum_name": "EMjSiteType",
       "subtypes": [
-        {"key": "box",       "enum_value": "Box",       "class_name": "UMjBoxSite",       "display_name": "MuJoCo Box Site"},
-        {"key": "sphere",    "enum_value": "Sphere",    "class_name": "UMjSphereSite",    "display_name": "MuJoCo Sphere Site"},
-        {"key": "capsule",   "enum_value": "Capsule",   "class_name": "UMjCapsuleSite",   "display_name": "MuJoCo Capsule Site"},
-        {"key": "cylinder",  "enum_value": "Cylinder",  "class_name": "UMjCylinderSite",  "display_name": "MuJoCo Cylinder Site"},
-        {"key": "ellipsoid", "enum_value": "Ellipsoid", "class_name": "UMjEllipsoidSite", "display_name": "MuJoCo Ellipsoid Site"}
+        {"key": "box",       "enum_value": "Box",       "class_name": "UMjBoxSite"},
+        {"key": "sphere",    "enum_value": "Sphere",    "class_name": "UMjSphereSite"},
+        {"key": "capsule",   "enum_value": "Capsule",   "class_name": "UMjCapsuleSite"},
+        {"key": "cylinder",  "enum_value": "Cylinder",  "class_name": "UMjCylinderSite"},
+        {"key": "ellipsoid", "enum_value": "Ellipsoid", "class_name": "UMjEllipsoidSite"}
       ]
     },
 
@@ -697,8 +892,8 @@
       "schema_common_block": "tendon.spatial.attrs",
       "type_enum_name": "EMjTendonType",
       "subtypes": [
-        {"key": "spatial", "enum_value": "Spatial", "class_name": "UMjSpatialTendon", "display_name": "MuJoCo Spatial Tendon"},
-        {"key": "fixed",   "enum_value": "Fixed",   "class_name": "UMjFixedTendon",   "display_name": "MuJoCo Fixed Tendon"}
+        {"key": "spatial", "enum_value": "Spatial", "class_name": "UMjSpatialTendon"},
+        {"key": "fixed",   "enum_value": "Fixed",   "class_name": "UMjFixedTendon"}
       ]
     },
 
@@ -710,14 +905,26 @@
       "schema_common_block": "equality.weld",
       "schema_common_extra_attrs": ["polycoef"],
       "type_enum_name": "EMjEqualityType",
+      "_note_objtype_dispatch": "mjsEquality.objtype tells MuJoCo what kind of object name1/name2 refer to. Connect/Weld are dual-mode: site-to-site if site1 was specified, else body-to-body. Codegen emits a switch on EqualityType into the CODEGEN_OBJTYPE_DISPATCH markers in MjEquality.cpp.",
+      "objtype_dispatch": {
+        "discriminator": "EqualityType",
+        "target": "Element->objtype",
+        "default": "mjOBJ_UNKNOWN",
+        "cases": [
+          {"keys": ["Connect", "Weld"], "expr": "!site1.IsEmpty() ? mjOBJ_SITE : mjOBJ_BODY"},
+          {"keys": ["Joint"],   "expr": "mjOBJ_JOINT"},
+          {"keys": ["Tendon"],  "expr": "mjOBJ_TENDON"},
+          {"keys": ["Flex", "FlexVert", "FlexStrain"], "expr": "mjOBJ_FLEX"}
+        ]
+      },
       "subtypes": [
-        {"key": "connect",    "enum_value": "Connect",    "class_name": "UMjConnectEquality",    "display_name": "MuJoCo Connect Equality"},
-        {"key": "weld",       "enum_value": "Weld",       "class_name": "UMjWeldEquality",       "display_name": "MuJoCo Weld Equality"},
-        {"key": "joint",      "enum_value": "Joint",      "class_name": "UMjJointEquality",      "display_name": "MuJoCo Joint Equality"},
-        {"key": "tendon",     "enum_value": "Tendon",     "class_name": "UMjTendonEquality",     "display_name": "MuJoCo Tendon Equality"},
-        {"key": "flex",       "enum_value": "Flex",       "class_name": "UMjFlexEquality",       "display_name": "MuJoCo Flex Equality"},
-        {"key": "flexvert",   "enum_value": "FlexVert",   "class_name": "UMjFlexVertEquality",   "display_name": "MuJoCo FlexVert Equality"},
-        {"key": "flexstrain", "enum_value": "FlexStrain", "class_name": "UMjFlexStrainEquality", "display_name": "MuJoCo FlexStrain Equality"}
+        {"key": "connect",    "enum_value": "Connect",    "class_name": "UMjConnectEquality"},
+        {"key": "weld",       "enum_value": "Weld",       "class_name": "UMjWeldEquality"},
+        {"key": "joint",      "enum_value": "Joint",      "class_name": "UMjJointEquality"},
+        {"key": "tendon",     "enum_value": "Tendon",     "class_name": "UMjTendonEquality"},
+        {"key": "flex",       "enum_value": "Flex",       "class_name": "UMjFlexEquality"},
+        {"key": "flexvert",   "enum_value": "FlexVert",   "class_name": "UMjFlexVertEquality"},
+        {"key": "flexstrain", "enum_value": "FlexStrain", "class_name": "UMjFlexStrainEquality"}
       ]
     },
 
@@ -730,14 +937,25 @@
       "mjs_struct": "mjsGeom",
       "schema_common_block": "geom.attrs",
       "type_enum_name": "EMjGeomType",
+      "_note_geom_final_type": "Mesh-name export depends on the COMPILE-TIME geom type, which is either the UPROPERTY Type (if overridden) or the Default geom's type (otherwise). Codegen emits the resolution + the conditional mesh-name write into the CODEGEN_GEOM_FINAL_TYPE markers in MjGeom.cpp. The value_map is pulled from element_rules.geom.xml_enum_attrs.type so there's only one source of truth for the XML/UE/mj triplet.",
+      "geom_final_type": {
+        "xml_enum_ref":       "type",
+        "override_field":     "bOverride_Type",
+        "default_lookup":     "Default->geom->type",
+        "default_fallback":   "mjGEOM_MESH",
+        "name_field":         "MeshName",
+        "name_export_for":    "mjGEOM_MESH",
+        "name_target":        "Element->meshname",
+        "name_setter":        "MjSetStringRaw"
+      },
       "subtypes": [
-        {"key": "box",       "enum_value": "Box",       "class_name": "UMjBox",       "header": "MjBox.h",       "is_hybrid": true},
-        {"key": "sphere",    "enum_value": "Sphere",    "class_name": "UMjSphere",    "header": "MjSphere.h",    "is_hybrid": true},
-        {"key": "capsule",   "enum_value": "Capsule",   "class_name": "UMjCapsule",   "header": "MjCapsule.h",   "is_hybrid": true},
-        {"key": "cylinder",  "enum_value": "Cylinder",  "class_name": "UMjCylinder",  "header": "MjCylinder.h",  "is_hybrid": true},
-        {"key": "ellipsoid", "enum_value": "Ellipsoid", "class_name": "UMjEllipsoid", "header": "MjEllipsoid.h", "is_hybrid": false},
-        {"key": "plane",     "enum_value": "Plane",     "class_name": "UMjPlane",     "header": "MjPlane.h",     "is_hybrid": false},
-        {"key": "sdf",       "enum_value": "SDF",       "class_name": "UMjSdf",       "header": "MjSdf.h",       "is_hybrid": false}
+        {"key": "box",       "enum_value": "Box",       "class_name": "UMjBox",       "header": "MjBox.h"},
+        {"key": "sphere",    "enum_value": "Sphere",    "class_name": "UMjSphere",    "header": "MjSphere.h"},
+        {"key": "capsule",   "enum_value": "Capsule",   "class_name": "UMjCapsule",   "header": "MjCapsule.h"},
+        {"key": "cylinder",  "enum_value": "Cylinder",  "class_name": "UMjCylinder",  "header": "MjCylinder.h"},
+        {"key": "ellipsoid", "enum_value": "Ellipsoid", "class_name": "UMjEllipsoid", "header": "MjEllipsoid.h", "fully_emitted": true},
+        {"key": "plane",     "enum_value": "Plane",     "class_name": "UMjPlane",     "header": "MjPlane.h",     "fully_emitted": true},
+        {"key": "sdf",       "enum_value": "SDF",       "class_name": "UMjSdf",       "header": "MjSdf.h",       "fully_emitted": true}
       ],
       "_note_skipped_subtypes": ["hfield (URLab has its own hfield actor system)", "mesh (existing UMjMeshGeom)"]
     },
@@ -779,11 +997,13 @@
     },
 
     "inertial": {
+      "_note_export": "MJCF <inertial> has no corresponding mjsInertial struct; the export path writes to ParentBody->{mass,inertia,fullinertia,ipos,iquat} in a hand-rolled UMjInertial::RegisterToSpec. disable_mjs_export_emission suppresses the codegen-emitted Element->* export block so the hand-rolled path stays canonical.",
       "layout": "no_subclasses",
       "base_class_name": "UMjInertial",
       "base_class_header": "MuJoCo/Components/Physics/MjInertial.h",
       "mjs_struct": "mjsInertial",
-      "schema_common_block": "inertial.attrs"
+      "schema_common_block": "inertial.attrs",
+      "disable_mjs_export_emission": true
     },
 
     "contact_pair": {
@@ -814,10 +1034,7 @@
 
   "views": {
     "BodyView": {
-      "obj_type": "mjOBJ_BODY",
-      "owner_count_var": "nbody",
       "include_blocks": ["MJMODEL_POINTERS_BODY", "MJMODEL_POINTERS_TREE"],
-      "field_renames": {},
       "data_fields": {
         "xpos":   {"index_var": "id",          "stride": "3"},
         "xquat":  {"index_var": "id",          "stride": "4"},
@@ -828,8 +1045,6 @@
       }
     },
     "JointView": {
-      "obj_type": "mjOBJ_JOINT",
-      "owner_count_var": "njnt",
       "include_blocks": ["MJMODEL_POINTERS_JOINT", "MJMODEL_POINTERS_DOF"],
       "derived_index_vars": {
         "qpos_adr": "m->jnt_qposadr[id]",
@@ -839,7 +1054,6 @@
         "MJMODEL_POINTERS_JOINT": "id",
         "MJMODEL_POINTERS_DOF":   "dof_adr"
       },
-      "field_renames": {},
       "data_fields": {
         "qpos":    {"index_var": "qpos_adr", "stride": "1"},
         "qvel":    {"index_var": "dof_adr",  "stride": "1"},
@@ -849,55 +1063,464 @@
       }
     },
     "GeomView": {
-      "obj_type": "mjOBJ_GEOM",
-      "owner_count_var": "ngeom",
       "include_blocks": ["MJMODEL_POINTERS_GEOM"],
-      "field_renames": {},
       "data_fields": {
         "geom_xpos": {"index_var": "id", "stride": "3"},
         "geom_xmat": {"index_var": "id", "stride": "9"}
       }
     },
     "SiteView": {
-      "obj_type": "mjOBJ_SITE",
-      "owner_count_var": "nsite",
       "include_blocks": ["MJMODEL_POINTERS_SITE"],
-      "field_renames": {},
       "data_fields": {
         "site_xpos": {"index_var": "id", "stride": "3"},
         "site_xmat": {"index_var": "id", "stride": "9"}
       }
     },
     "ActuatorView": {
-      "obj_type": "mjOBJ_ACTUATOR",
-      "owner_count_var": "nu",
       "include_blocks": ["MJMODEL_POINTERS_ACTUATOR"],
-      "field_renames": {},
       "data_fields": {
-        "ctrl":          {"index_var": "id", "stride": "1"},
-        "actuator_force":{"index_var": "id", "stride": "1"},
+        "ctrl":           {"index_var": "id", "stride": "1"},
+        "actuator_force": {"index_var": "id", "stride": "1"},
         "actuator_length":{"index_var": "id", "stride": "1"},
-        "actuator_velocity":{"index_var": "id", "stride": "1"}
+        "actuator_velocity":{"index_var": "id", "stride": "1"},
+        "_note_moment": "Per-actuator row of the actuator_moment (nu x nv) matrix. Stride is m->nv, which is a valid stride expression — no bind_override needed.",
+        "actuator_moment": {"index_var": "id", "stride": "m->nv"},
+        "_note_act": "act lives in d->act indexed by m->actuator_actadr[id]. The adr can be -1 for actuators without an internal activation state; bind_override emits the nullable lookup so consumers can null-check ``act`` directly.",
+        "act": {"bind_override": "act = (m->actuator_actadr[id] >= 0) ? d->act + m->actuator_actadr[id] : nullptr;"}
       }
     },
     "TendonView": {
-      "obj_type": "mjOBJ_TENDON",
-      "owner_count_var": "ntendon",
       "include_blocks": ["MJMODEL_POINTERS_TENDON"],
-      "field_renames": {},
       "data_fields": {
         "ten_length":   {"index_var": "id", "stride": "1"},
         "ten_velocity": {"index_var": "id", "stride": "1"}
       }
     },
     "SensorView": {
-      "obj_type": "mjOBJ_SENSOR",
-      "owner_count_var": "nsensor",
       "include_blocks": ["MJMODEL_POINTERS_SENSOR"],
-      "field_renames": {},
       "data_fields": {
-        "sensordata": {"index_var": "m->sensor_adr[id]", "stride": "m->sensor_dim[id]"}
+        "_note_sensordata": "Bounds-checked slice — the default base+id*stride formula is wrong for sensordata (it would double-multiply adr*dim). bind_override is a verbatim C++ statement the codegen drops inside the BIND marker.",
+        "sensordata": {"bind_override": "sensordata = (sensor_adr >= 0 && sensor_adr < m->nsensordata) ? d->sensordata + sensor_adr : nullptr;"}
+      }
+    }
+  },
+
+  "_note_editor_option_helpers": "Codegens trivial UMjX::GetYOptions() editor wrappers, injecting them between CODEGEN_EDITOR_OPTIONS_START/END markers inside each host .cpp's #if WITH_EDITOR block. Wrappers with real logic (TransmissionType / EqualityType / GetClassForObjType switches) stay hand-written in the same #if block.",
+  "editor_option_helpers": {
+    "wrappers": [
+      {"class": "UMjActuator", "host_cpp": "MuJoCo/Components/Actuators/MjActuator.cpp",
+       "method": "GetSliderSiteOptions",     "filter_class": "UMjSite"},
+      {"class": "UMjActuator", "host_cpp": "MuJoCo/Components/Actuators/MjActuator.cpp",
+       "method": "GetRefSiteOptions",        "filter_class": "UMjSite"},
+      {"class": "UMjActuator", "host_cpp": "MuJoCo/Components/Actuators/MjActuator.cpp",
+       "method": "GetDefaultClassOptions",   "filter_class": "UMjDefault", "include_defaults": true},
+      {"class": "UMjBody",          "host_cpp": "MuJoCo/Components/Bodies/MjBody.cpp",
+       "method": "GetChildClassOptions",     "filter_class": "UMjDefault", "include_defaults": true},
+      {"class": "UMjGeom",          "host_cpp": "MuJoCo/Components/Geometry/MjGeom.cpp",
+       "method": "GetDefaultClassOptions",   "filter_class": "UMjDefault", "include_defaults": true},
+      {"class": "UMjSite",          "host_cpp": "MuJoCo/Components/Geometry/MjSite.cpp",
+       "method": "GetDefaultClassOptions",   "filter_class": "UMjDefault", "include_defaults": true},
+      {"class": "UMjJoint",         "host_cpp": "MuJoCo/Components/Joints/MjJoint.cpp",
+       "method": "GetDefaultClassOptions",   "filter_class": "UMjDefault", "include_defaults": true},
+      {"class": "UMjContactExclude","host_cpp": "MuJoCo/Components/Physics/MjContactExclude.cpp",
+       "method": "GetBodyOptions",           "filter_class": "UMjBody"},
+      {"class": "UMjContactPair",   "host_cpp": "MuJoCo/Components/Physics/MjContactPair.cpp",
+       "method": "GetGeomOptions",           "filter_class": "UMjGeom"},
+      {"class": "UMjSensor",        "host_cpp": "MuJoCo/Components/Sensors/MjSensor.cpp",
+       "method": "GetDefaultClassOptions",   "filter_class": "UMjDefault", "include_defaults": true},
+      {"class": "UMjTendon",        "host_cpp": "MuJoCo/Components/Tendons/MjTendon.cpp",
+       "method": "GetDefaultClassOptions",   "filter_class": "UMjDefault", "include_defaults": true}
+    ]
+  },
+
+  "_note_generated_enums": "Banner-mode .h with one or more UENUMs derived from the introspect snapshot's mjt* enums (projected into mjspec.enums by _mjspec_from_introspect). Hand-edits get clobbered on next regen. ``extra_members`` appends URLab-specific values that have no mjt counterpart (e.g. DcMotor).",
+  "generated_enums": {
+    "MjOptionEnums": {
+      "public_header": "MuJoCo/Generated/MjOptionEnums.h",
+      "enums": [
+        {
+          "ue_name": "EMjIntegrator",
+          "from_mj_enum": "mjtIntegrator",
+          "ue_member_from_mj": {
+            "mjINT_EULER":        "Euler",
+            "mjINT_RK4":          "RK4",
+            "mjINT_IMPLICIT":     "Implicit",
+            "mjINT_IMPLICITFAST": "ImplicitFast"
+          }
+        },
+        {
+          "ue_name": "EMjCone",
+          "from_mj_enum": "mjtCone",
+          "ue_member_from_mj": {
+            "mjCONE_PYRAMIDAL": "Pyramidal",
+            "mjCONE_ELLIPTIC":  "Elliptic"
+          }
+        },
+        {
+          "ue_name": "EMjSolver",
+          "from_mj_enum": "mjtSolver",
+          "ue_member_from_mj": {
+            "mjSOL_PGS":    "PGS",
+            "mjSOL_CG":     "CG",
+            "mjSOL_NEWTON": "Newton"
+          }
+        }
+      ]
+    },
+    "_note_MjArticulationEnums": "Pure value-mapping enums emitted as MuJoCo/Generated/MjArticulationEnums.h. Components that need them (MjActuator.h, MjGeom.h, MjCameraTypes.h, MjSensor.h, MjFlexcomp.h) include this header.",
+    "MjArticulationEnums": {
+      "public_header": "MuJoCo/Generated/MjArticulationEnums.h",
+      "enums": [
+        {
+          "ue_name": "EMjGainType",
+          "from_mj_enum": "mjtGain",
+          "ue_member_from_mj": {
+            "mjGAIN_FIXED":   "Fixed",
+            "mjGAIN_AFFINE":  "Affine",
+            "mjGAIN_MUSCLE":  "Muscle",
+            "mjGAIN_DCMOTOR": "DcMotor",
+            "mjGAIN_USER":    "User"
+          }
+        },
+        {
+          "ue_name": "EMjBiasType",
+          "from_mj_enum": "mjtBias",
+          "ue_member_from_mj": {
+            "mjBIAS_NONE":    "None",
+            "mjBIAS_AFFINE":  "Affine",
+            "mjBIAS_MUSCLE":  "Muscle",
+            "mjBIAS_DCMOTOR": "DcMotor",
+            "mjBIAS_USER":    "User"
+          }
+        },
+        {
+          "ue_name": "EMjDynType",
+          "from_mj_enum": "mjtDyn",
+          "ue_member_from_mj": {
+            "mjDYN_NONE":        "None",
+            "mjDYN_INTEGRATOR":  "Integrator",
+            "mjDYN_FILTER":      "Filter",
+            "mjDYN_FILTEREXACT": "FilterExact",
+            "mjDYN_MUSCLE":      "Muscle",
+            "mjDYN_DCMOTOR":     "DcMotor",
+            "mjDYN_USER":        "User"
+          }
+        },
+        {
+          "_note_EMjGeomInertia": "No mjt counterpart; MJCF geom inertia kind is parser-side enum string-mapped on import. Pure extra_members.",
+          "ue_name": "EMjGeomInertia",
+          "extra_members": [
+            {"name": "Volume", "value": 0},
+            {"name": "Shell",  "value": 1}
+          ]
+        },
+        {
+          "_note_EMjFluidShape": "URLab-only; MuJoCo fluid shape is a runtime computation, not a public enum. Pure extra_members.",
+          "ue_name": "EMjFluidShape",
+          "extra_members": [
+            {"name": "None",      "value": 0},
+            {"name": "Ellipsoid", "value": 1}
+          ]
+        },
+        {
+          "_note_EMjCameraTrackingMode": "Mirrors mjtCamLight but only the camera-side subset; values come from mjCAMLIGHT_* keyed by URLab member name.",
+          "ue_name": "EMjCameraTrackingMode",
+          "from_mj_enum": "mjtCamLight",
+          "ue_member_from_mj": {
+            "mjCAMLIGHT_FIXED":         "Fixed",
+            "mjCAMLIGHT_TRACK":         "Track",
+            "mjCAMLIGHT_TRACKCOM":      "TrackCom",
+            "mjCAMLIGHT_TARGETBODY":    "TargetBody",
+            "mjCAMLIGHT_TARGETBODYCOM": "TargetBodyCom"
+          }
+        },
+        {
+          "ue_name": "EMjCameraProjection",
+          "from_mj_enum": "mjtProjection",
+          "ue_member_from_mj": {
+            "mjPROJ_PERSPECTIVE":  "Perspective",
+            "mjPROJ_ORTHOGRAPHIC": "Orthographic"
+          }
+        },
+        {
+          "ue_name": "EMjObjType",
+          "from_mj_enum": "mjtObj",
+          "exclude_mj_members": ["mjNOBJECT", "mjOBJ_DEFAULT", "mjOBJ_MODEL"],
+          "ue_member_from_mj": {
+            "mjOBJ_UNKNOWN":  "Unknown",
+            "mjOBJ_BODY":     "Body",
+            "mjOBJ_XBODY":    "XBody",
+            "mjOBJ_JOINT":    "Joint",
+            "mjOBJ_DOF":      "DoF",
+            "mjOBJ_GEOM":     "Geom",
+            "mjOBJ_SITE":     "Site",
+            "mjOBJ_CAMERA":   "Camera",
+            "mjOBJ_LIGHT":    "Light",
+            "mjOBJ_FLEX":     "Flex",
+            "mjOBJ_MESH":     "Mesh",
+            "mjOBJ_SKIN":     "Skin",
+            "mjOBJ_HFIELD":   "HField",
+            "mjOBJ_TEXTURE":  "Texture",
+            "mjOBJ_MATERIAL": "Material",
+            "mjOBJ_PAIR":     "Pair",
+            "mjOBJ_EXCLUDE":  "Exclude",
+            "mjOBJ_EQUALITY": "Equality",
+            "mjOBJ_TENDON":   "Tendon",
+            "mjOBJ_ACTUATOR": "Actuator",
+            "mjOBJ_SENSOR":   "Sensor",
+            "mjOBJ_NUMERIC":  "Numeric",
+            "mjOBJ_TEXT":     "Text",
+            "mjOBJ_TUPLE":    "Tuple",
+            "mjOBJ_KEY":      "Key",
+            "mjOBJ_PLUGIN":   "Plugin",
+            "mjOBJ_FRAME":    "Frame"
+          }
+        },
+        {
+          "_note_EMjActuatorTrnType": "mjTRN_UNDEFINED has C value 1000 (way out of uint8 range), so it can't be a direct ue_member_from_mj entry. Excluded from the snapshot pass and reintroduced as an extra_member with the next contiguous slot (6).",
+          "ue_name": "EMjActuatorTrnType",
+          "from_mj_enum": "mjtTrn",
+          "exclude_mj_members": ["mjTRN_UNDEFINED"],
+          "ue_member_from_mj": {
+            "mjTRN_JOINT":         "Joint",
+            "mjTRN_JOINTINPARENT": "JointInParent",
+            "mjTRN_SLIDERCRANK":   "SliderCrank",
+            "mjTRN_TENDON":        "Tendon",
+            "mjTRN_SITE":          "Site",
+            "mjTRN_BODY":          "Body"
+          },
+          "extra_members": [
+            {"name": "Undefined", "value": 6}
+          ]
+        },
+        {
+          "_note_EMjFlexcompDof": "URLab-only flex-component DoF reduction kind; no mjt counterpart (the MJCF attr is `dof` and parsed string-side). Pure extra_members; matches the pre-migration hand-rolled set.",
+          "ue_name": "EMjFlexcompDof",
+          "extra_members": [
+            {"name": "Full",      "value": 0},
+            {"name": "Radial",    "value": 1},
+            {"name": "Trilinear", "value": 2},
+            {"name": "Quadratic", "value": 3},
+            {"name": "TwoD",      "value": 4}
+          ]
+        }
+      ]
+    }
+  },
+
+  "_note_synthetic_categories": "Whole-file banner-mode emission for USTRUCTs mirroring C structs from mjxmacro_block. Hand-edits to generated files get clobbered on next regen. See generate_ue_components.py::_phase_synthetic_categories.",
+  "synthetic_categories": {
+    "MjStatistic": {
+      "ue_struct_name":   "FMjStatistic",
+      "mjxmacro_block":   "MJSTATISTIC_FIELDS",
+      "c_struct_type":    "mjStatistic",
+      "public_header":    "MuJoCo/Generated/MjStatistic.h",
+      "private_source":   "MuJoCo/Generated/MjStatistic.cpp",
+      "category_label":   "Statistic"
+    },
+
+    "_note_MjOption": "Codegen-emitted FMjOptionGenerated mirrors mjOption (MJOPTION_FIELDS) with URLab extras (MemoryMB, bEnableMultiCCD, bEnableSleep, SleepTolerance). AMjArticulation::SimOptions and UMjPhysicsEngine::SimOptions point at this struct directly.",
+    "MjOption": {
+      "ue_struct_name":   "FMjOptionGenerated",
+      "mjxmacro_block":   "MJOPTION_FIELDS",
+      "c_struct_type":    "mjOption",
+      "public_header":    "MuJoCo/Generated/MjOptionGenerated.h",
+      "private_source":   "MuJoCo/Generated/MjOptionGenerated.cpp",
+      "category_label":   "MjOption",
+      "include_directives": [
+        "mujoco/mujoco.h",
+        "MuJoCo/Generated/MjOptionEnums.h"
+      ],
+      "field_renames": {
+        "timestep":         "Timestep",
+        "impratio":         "Impratio",
+        "tolerance":        "Tolerance",
+        "ls_tolerance":     "LsTolerance",
+        "noslip_tolerance": "NoslipTolerance",
+        "ccd_tolerance":    "CCD_Tolerance",
+        "sdf_initpoints":   "SdfInitPoints",
+        "sdf_iterations":   "SdfIterations",
+        "iterations":       "Iterations",
+        "ls_iterations":    "LsIterations",
+        "noslip_iterations": "NoslipIterations",
+        "ccd_iterations":   "CCD_Iterations",
+        "disableflags":     "DisableFlags",
+        "enableflags":      "EnableFlags",
+        "disableactuator":  "DisableActuator",
+        "apirate":          "ApiRate",
+        "sleep_tolerance":  "SleepToleranceMj",
+        "rebuild_interval": "RebuildInterval"
+      },
+      "field_types": {
+        "timestep": "float",
+        "apirate":  "float",
+        "integrator": "EMjIntegrator",
+        "cone":       "EMjCone",
+        "solver":     "EMjSolver"
+      },
+      "field_defaults": {
+        "Timestep":         "0.002",
+        "Impratio":         "1.0",
+        "Tolerance":        "1e-8",
+        "Iterations":       "100",
+        "LsIterations":     "50",
+        "NoslipTolerance":  "1e-6",
+        "CCD_Tolerance":    "1e-6",
+        "Gravity":          "FVector(0.0f, 0.0f, -981.0f)",
+        "Magnetic":         "FVector(0.0f, -0.5f, 0.0f)",
+        "Integrator":       "EMjIntegrator::Euler",
+        "Cone":             "EMjCone::Pyramidal",
+        "Solver":           "EMjSolver::Newton"
+      },
+      "field_apply_to_spec_mode": {
+        "gravity":  "vec3_cm",
+        "wind":     "vec3_cm",
+        "magnetic": "vec3_y_flip",
+        "integrator": "guarded",
+        "cone":       "guarded",
+        "solver":     "guarded",
+        "noslip_iterations": "guarded",
+        "noslip_tolerance":  "guarded",
+        "ccd_iterations":    "guarded",
+        "ccd_tolerance":     "guarded"
+      },
+      "_note_skip_vs_exclude": "field_skip_apply: emitted as UPROPERTY but not written to mjOption (apply body skips). exclude_fields: omitted from the struct entirely. exclude_fields takes precedence in iteration, so entries listed in both would be dead — keep them apart.",
+      "field_skip_apply": [
+        "disableflags", "enableflags", "disableactuator",
+        "sdf_initpoints", "sdf_iterations", "sleep_tolerance"
+      ],
+      "exclude_fields": ["apirate", "rebuild_interval", "o_solref", "o_solimp", "o_friction"],
+      "urlab_extra_fields": [
+        {
+          "name": "MemoryMB",
+          "type": "int32",
+          "default": "100",
+          "meta": "ClampMin=\"1\"",
+          "has_override": true
+        },
+        {
+          "name": "bEnableMultiCCD",
+          "type": "bool",
+          "default": "false",
+          "sub_category": "Collision",
+          "meta": "ToolTip=\"Enable multi-contact convex collision detection. Useful for flat surfaces (e.g. mesh-mesh) where single-point contact causes sliding or wobbling.\"",
+          "has_override": false
+        },
+        {
+          "name": "bEnableSleep",
+          "type": "bool",
+          "default": "false",
+          "sub_category": "Sleep",
+          "has_override": false
+        },
+        {
+          "name": "SleepTolerance",
+          "type": "float",
+          "default": "1e-4f",
+          "sub_category": "Sleep",
+          "edit_condition": "bEnableSleep",
+          "has_override": false
+        }
+      ],
+      "apply_extras_function": "ApplyMjOptionExtras",
+      "emit_apply_methods": true
+    },
+
+    "_note_MjVisualStructs": "Codegen-emitted FMjVisual{Global,Headlight,Quality,Map,Scale,Rgba} parallel to mjVisual sub-blocks.",
+    "MjVisualGlobal": {
+      "ue_struct_name":   "FMjVisualGlobal",
+      "mjxmacro_block":   "MJVISUAL_GLOBAL_FIELDS",
+      "c_struct_type":    "mjVisualGlobal",
+      "public_header":    "MuJoCo/Generated/MjVisualGlobal.h",
+      "private_source":   "MuJoCo/Generated/MjVisualGlobal.cpp",
+      "category_label":   "Visual|Global",
+      "field_defaults": {
+        "Fovy":       "45.0f",
+        "Ipd":        "0.068f",
+        "Realtime":   "1.0f"
+      }
+    },
+    "MjVisualHeadlight": {
+      "ue_struct_name":   "FMjVisualHeadlight",
+      "mjxmacro_block":   "MJVISUAL_HEADLIGHT_FIELDS",
+      "c_struct_type":    "mjVisualHeadlight",
+      "public_header":    "MuJoCo/Generated/MjVisualHeadlight.h",
+      "private_source":   "MuJoCo/Generated/MjVisualHeadlight.cpp",
+      "category_label":   "Visual|Headlight",
+      "field_defaults": {
+        "Ambient":  "FVector(0.1f, 0.1f, 0.1f)",
+        "Diffuse":  "FVector(0.4f, 0.4f, 0.4f)",
+        "Specular": "FVector(0.5f, 0.5f, 0.5f)",
+        "Active":   "1"
+      }
+    },
+    "MjVisualQuality": {
+      "ue_struct_name":   "FMjVisualQuality",
+      "mjxmacro_block":   "MJVISUAL_QUALITY_FIELDS",
+      "c_struct_type":    "mjVisualQuality",
+      "public_header":    "MuJoCo/Generated/MjVisualQuality.h",
+      "category_label":   "Visual|Quality",
+      "_note": "All quality fields are int in mjmodel.h (shadowsize=1024, offsamples=4, numslices=28, numstacks=16, numquads=4). mjxmacro carries no type info for these (bare X(name) form); explicit field_types override.",
+      "field_types": {
+        "shadowsize": "int32", "offsamples": "int32", "numslices": "int32",
+        "numstacks":  "int32", "numquads":   "int32"
+      },
+      "field_defaults": {
+        "Shadowsize": "1024", "Offsamples": "4", "Numslices": "28",
+        "Numstacks":  "16",   "Numquads":   "4"
       }
+    },
+    "MjVisualMap": {
+      "ue_struct_name":   "FMjVisualMap",
+      "mjxmacro_block":   "MJVISUAL_MAP_FIELDS",
+      "c_struct_type":    "mjVisualMap",
+      "public_header":    "MuJoCo/Generated/MjVisualMap.h",
+      "category_label":   "Visual|Map",
+      "_note": "All map fields are float scalars in mjVisualMap (force, torque, stiffness, etc.). mjxmacro defaults them to mjtNum (double) which compiles cleanly via implicit conversion."
+    },
+    "MjVisualScale": {
+      "ue_struct_name":   "FMjVisualScale",
+      "mjxmacro_block":   "MJVISUAL_SCALE_FIELDS",
+      "c_struct_type":    "mjVisualScale",
+      "public_header":    "MuJoCo/Generated/MjVisualScale.h",
+      "category_label":   "Visual|Scale",
+      "_note": "Same as MjVisualMap — float scalars in mjmodel.h, mjxmacro defaults to mjtNum which compiles fine."
+    },
+    "MjVisualRgba": {
+      "ue_struct_name":   "FMjVisualRgba",
+      "mjxmacro_block":   "MJVISUAL_RGBA_FIELDS",
+      "c_struct_type":    "mjVisualRgba",
+      "public_header":    "MuJoCo/Generated/MjVisualRgba.h",
+      "category_label":   "Visual|Rgba",
+      "_note": "Every field is float[4] color in mjVisualRgba. mjxmacro's bare X(name) form drops that info; all_fields_type forces FLinearColor for the UPROPERTY + flips the ApplyTo emit to .R/.G/.B/.A writes.",
+      "all_fields_type":  "FLinearColor"
+    },
+    "_note_MjsCompilerOptions": "Mirror of mjsCompiler (MJSCOMPILER_FIELDS in mjspecmacro.h, vendored via Scripts/codegen/sync_vendored.py). Header-only USTRUCT — no .cpp emission today since URLab parses compiler settings via the legacy FMjCompilerSettings struct in MjOrientationUtils.h. Future migration: route MjArticulation through this struct + an ApplyToSpec method.",
+    "MjsCompilerOptions": {
+      "ue_struct_name":   "FMjsCompilerOptions",
+      "mjxmacro_block":   "MJSCOMPILER_FIELDS",
+      "c_struct_type":    "mjsCompiler",
+      "public_header":    "MuJoCo/Generated/MjsCompilerOptions.h",
+      "category_label":   "Spec|Compiler",
+      "_note_exclude": "LRopt is mjLROpt (nested struct, not mirror-able as a UPROPERTY); meshdir/texturedir are mjString* (different shape — handled by the existing MeshDir/AssetDir on FMjCompilerSettings). eulerseq is a char[3] vec — mirror as FString instead, but skip in ApplyTo since FString -> char[3] needs a copy not a cast.",
+      "exclude_fields":   ["LRopt", "meshdir", "texturedir"],
+      "field_skip_apply": ["eulerseq"],
+      "field_types":      {"eulerseq": "FString"},
+      "field_defaults":   {"Eulerseq": "TEXT(\"xyz\")"}
+    },
+    "_note_MjsSpec": "Mirror of mjSpec top-level fields (MJSPEC_FIELDS in mjspecmacro.h). Many entries are NESTED structs (mjsCompiler, mjOption, mjVisual, mjStatistic) — those are exposed via the dedicated FMjsCompilerOptions / FMjOptionGenerated / FMjVisual* / FMjStatistic structs. We only mirror the scalar + string fields here.",
+    "MjsSpec": {
+      "ue_struct_name":   "FMjsSpec",
+      "mjxmacro_block":   "MJSPEC_FIELDS",
+      "c_struct_type":    "mjSpec",
+      "public_header":    "MuJoCo/Generated/MjsSpec.h",
+      "category_label":   "Spec",
+      "_note_exclude": "element/compiler/option/visual/stat are nested structs already exposed via their own synthetic_categories. modelname / comment / modelfiledir / meshdir / texturedir are mjString* — distinct shape. memory is mjtSize (handle separately).",
+      "exclude_fields":   [
+        "element", "compiler", "option", "visual", "stat",
+        "modelname", "comment", "modelfiledir", "meshdir", "texturedir",
+        "memory", "strippath"
+      ]
     }
   }
 }
diff --git a/Scripts/codegen/generate_ue_components.py b/Scripts/codegen/generate_ue_components.py
index 477d92c..8388cd5 100644
--- a/Scripts/codegen/generate_ue_components.py
+++ b/Scripts/codegen/generate_ue_components.py
@@ -9,9 +9,12 @@
 URLab UE component codegen, schema-driven.
 
 Inputs:
-  - Scripts/mjcf_schema_snapshot.json       (MJCF schema attrs)
-  - Scripts/mjxmacro_snapshot.json          (mjModel/mjData pointer table)
-  - Scripts/codegen/codegen_rules.json      (per-category layout, rules)
+  - Scripts/codegen/snapshots/mjcf_schema_snapshot.json   (MJCF schema attrs)
+  - Scripts/codegen/snapshots/mjxmacro_snapshot.json      (mjModel/mjData pointer table)
+  - Scripts/codegen/snapshots/introspect_snapshot.json    (libclang-scraped C API:
+                                                          mjsX struct fields, mjt*
+                                                          enums, mjs_setTo* signatures)
+  - Scripts/codegen/codegen_rules.json                    (per-category layout, rules)
 
 Outputs:
   - Component .h / .cpp files in Source/URLab/Public|Private/MuJoCo/Components/
@@ -38,15 +41,45 @@
 import os
 import re
 import sys
-from dataclasses import dataclass
-from typing import Any, Dict, List, Optional, Sequence, Tuple
+from dataclasses import dataclass, field
+from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Tuple
+
+# Sibling modules. Only names actually consumed by this orchestrator's
+# body are imported here. Test files import private helpers (e.g.
+# _resolve_direct, _classify_c_type, _check_brace_balance) directly from
+# their owning module — the orchestrator is not a re-export surface.
+from _codegen_core import (
+    _DIAGS_BUFFER, _diag_add, _diag_flush, _reset_diags,
+    _ue_type_accepts_units_meta, _attr_default_value,
+    override_toggle_name,
+    schema_attrs, schema_subtype_attrs,
+    compute_canon_absorbed, iter_category_attrs,
+    _VEC3_CONVERT_FMT,
+    _resolve_mjs_field, _resolve_value_map,
+    FileWrite,
+)
+from _codegen_inject import (
+    _find_matching_brace,
+    inject_between_tags, _inject_tags_into_cpp,
+)
+from _codegen_checks import (
+    _check_hand_enum_drift, _check_type_shape_drift,
+    _check_new_attr_typing, _check_orphan_rule_entries,
+    _check_mjxmacro_block_coverage, _check_apply_mode_validity,
+    _check_embedded_cpp_references,
+    _check_allowlist_staleness,
+    _check_property_units_validity, _check_compiler_attrs_coverage,
+    _check_value_map_stale_mj_consts,
+    _emit_drift_diagnostics,
+)
 
 SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
 PLUGIN_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, "..", ".."))
 
-DEFAULT_SCHEMA      = os.path.join(PLUGIN_ROOT, "Scripts", "mjcf_schema_snapshot.json")
-DEFAULT_MJXMACRO    = os.path.join(PLUGIN_ROOT, "Scripts", "mjxmacro_snapshot.json")
-DEFAULT_MJSPEC      = os.path.join(PLUGIN_ROOT, "Scripts", "mjspec_snapshot.json")
+_SNAPSHOT_DIR       = os.path.join(PLUGIN_ROOT, "Scripts", "codegen", "snapshots")
+DEFAULT_SCHEMA      = os.path.join(_SNAPSHOT_DIR, "mjcf_schema_snapshot.json")
+DEFAULT_MJXMACRO    = os.path.join(_SNAPSHOT_DIR, "mjxmacro_snapshot.json")
+DEFAULT_INTROSPECT  = os.path.join(_SNAPSHOT_DIR, "introspect_snapshot.json")
 DEFAULT_RULES       = os.path.join(SCRIPT_DIR, "codegen_rules.json")
 DEFAULT_PUBLIC_ROOT = os.path.join(PLUGIN_ROOT, "Source", "URLab", "Public")
 DEFAULT_PRIVATE_ROOT= os.path.join(PLUGIN_ROOT, "Source", "URLab", "Private")
@@ -80,95 +113,31 @@
 """
 
 
-# ---------------------------------------------------------------------------
-# Naming helpers
-# ---------------------------------------------------------------------------
-
-def pascal_case(snake_or_camel: str) -> str:
-    """**Schema-verbatim** naming. We use MuJoCo's attribute name AS-IS
-    as the C++ UPROPERTY identifier — no PascalCase, no renames. This
-    keeps the codegen pure: schema name == C++ name == wire field name.
-
-    Future-proof for MuJoCo bumps: a new attr appears in MJCF → it
-    flows through without any rename table to maintain.
+def _should_emit_xml_enum_at_this_layer(
+    attr: str,
+    attrs: Sequence[str],
+    global_excl: Set[str],
+    *,
+    apply_canonicalizations: bool,
+) -> bool:
+    """Whether this codegen layer owns the xml_enum import/export for
+    ``attr``. Two cases fire:
+
+      - The attr appears in this layer's attrs list — subtype-specific
+        case, e.g. ``dcmotor.input`` fires on the DCMotor subtype's
+        emission.
+      - The base-class layer (apply_canonicalizations=True) AND the
+        attr is in global_exclusions — always-emit-at-base case, e.g.
+        ``joint.type`` is globally excluded from per-attr UPROPERTY
+        emission but xml_enum_attrs still owns its import/export at
+        the base.
     """
-    return snake_or_camel
-
-
-def override_toggle_name(prop_name: str) -> str:
-    return f"bOverride_{prop_name}"
+    if attr in attrs:
+        return True
+    return apply_canonicalizations and attr in global_excl
 
 
-# ---------------------------------------------------------------------------
-# Schema access
-# ---------------------------------------------------------------------------
-
-def schema_attrs(schema: Dict[str, Any], path: str) -> List[str]:
-    """Read an attribute list at a dotted path, e.g. ``actuator_common.attrs``
-    or ``camera.attrs`` or ``tendon.spatial.attrs``. Returns [] if missing."""
-    cur: Any = schema
-    for piece in path.split("."):
-        if isinstance(cur, dict) and piece in cur:
-            cur = cur[piece]
-        else:
-            return []
-    if isinstance(cur, list):
-        # Could be list of strings OR list of dicts {name, attrs}; normalise.
-        if cur and isinstance(cur[0], str):
-            return list(cur)
-        attrs: List[str] = []
-        for entry in cur:
-            if isinstance(entry, dict):
-                # equality types are dicts with key/attrs nested under
-                # the type name (e.g. {"connect": [...]}, or {"name": ..., "attrs": [...]})
-                if "attrs" in entry and isinstance(entry["attrs"], list):
-                    attrs.extend([str(x) for x in entry["attrs"]])
-                else:
-                    for v in entry.values():
-                        if isinstance(v, list):
-                            attrs.extend([str(x) for x in v])
-        # de-dupe preserving order
-        seen = set()
-        out = []
-        for a in attrs:
-            if a not in seen:
-                seen.add(a)
-                out.append(a)
-        return out
-    if isinstance(cur, dict):
-        # E.g. equality.weld is a list inside a dict — handle by walking
-        # all string-list values one level deeper.
-        attrs2: List[str] = []
-        for v in cur.values():
-            if isinstance(v, list):
-                attrs2.extend([str(x) for x in v if isinstance(x, str)])
-        return attrs2
-    return []
-
-
-def schema_subtype_attrs(schema: Dict[str, Any], category: str, subtype_key: str) -> List[str]:
-    """Look up per-subtype attrs from schema['actuator_types'][key] /
-    schema['sensor_types'] (list-of-dict). Returns [] if not found."""
-    if category == "actuator":
-        block = schema.get("actuator_types", {})
-        if isinstance(block, dict):
-            entry = block.get(subtype_key)
-            if isinstance(entry, list):
-                return [str(x) for x in entry]
-            if isinstance(entry, dict) and "attrs" in entry:
-                return [str(x) for x in entry["attrs"]]
-        return []
-    if category == "sensor":
-        block = schema.get("sensor_types", [])
-        if isinstance(block, list):
-            for entry in block:
-                if isinstance(entry, dict) and entry.get("name") == subtype_key:
-                    attrs = entry.get("attrs", [])
-                    return [str(x) for x in attrs] if isinstance(attrs, list) else []
-                if isinstance(entry, str) and entry == subtype_key:
-                    return []  # name-only entry, no extra attrs
-        return []
-    return []
+# `schema_attrs` and `schema_subtype_attrs` live in `_codegen_core`.
 
 
 # ---------------------------------------------------------------------------
@@ -183,33 +152,171 @@ class EmittedSchema:
     exports_cpp:  str   # ExportTo body (between codegen tags)
 
 
-def _attr_default_value(ue_type: str) -> str:
-    if ue_type == "float":      return "0.0f"
-    if ue_type == "int32":      return "0"
-    if ue_type == "bool":       return "false"
-    if ue_type == "FString":    return "TEXT(\"\")"
-    if ue_type == "FVector":    return "FVector::ZeroVector"
-    if ue_type == "FQuat":      return "FQuat::Identity"
-    if ue_type == "FLinearColor": return "FLinearColor::White"
-    if ue_type.startswith("TArray"): return "{}"
-    return "{}"
+@dataclass
+class EmissionContext:
+    """Cross-cutting rules / element / call-site state threaded through
+    ``emit_schema_for_attrs`` and its helpers. Built once via ``from_call``.
+
+    Carries every shared computation the property/import/export helpers
+    need (canon_absorbed, xml_enum_attr_names, elem_canons, the various
+    rule dicts) so the helpers take one ``ctx`` arg instead of 15
+    positional parameters."""
+    # --- caller-provided ---
+    attrs:                Sequence[str]
+    rules:                Dict[str, Any]
+    element_name:         str
+    apply_canonicalizations: bool = True
+    mjs_fields:           Optional[set] = None
+    consumed_by_setto:    Optional[set] = None
+    mjs_array_caps:       Optional[Dict[str, int]] = None
+    # --- derived from rules (cached on construct) ---
+    global_excl:          set = field(default_factory=set)
+    type_map:             Dict[str, str] = field(default_factory=dict)
+    default_type:         str = "float"
+    renames:              Dict[str, str] = field(default_factory=dict)
+    elem_rules:           Dict[str, Any] = field(default_factory=dict)
+    elem_excl:            set = field(default_factory=set)
+    canonicalizations:    Dict[str, Any] = field(default_factory=dict)
+    attr_to_mjs_field:    Dict[str, str] = field(default_factory=dict)
+    # --- derived in from_call but parameterised by apply_canonicalizations ---
+    elem_canons:          List[str] = field(default_factory=list)
+    canon_absorbed:       set = field(default_factory=set)
+    xml_enum_attr_names:  set = field(default_factory=set)
+
+    @classmethod
+    def from_call(cls,
+                  attrs: Sequence[str],
+                  rules: Dict[str, Any],
+                  element_name: str,
+                  *,
+                  apply_canonicalizations: bool = True,
+                  mjs_fields: Optional[set] = None,
+                  consumed_by_setto: Optional[set] = None,
+                  mjs_array_caps: Optional[Dict[str, int]] = None) -> "EmissionContext":
+        elem_rules = rules.get("element_rules", {}).get(element_name, {})
+        canonicalizations = rules.get("canonicalizations", {})
+        # ``elem_canons`` is the raw applied-canon list; the helpers
+        # clear it when apply_canonicalizations is False, but
+        # ``canon_absorbed`` keeps the raw absorbed set so per-attr
+        # emission still excludes them at the subtype layer.
+        elem_canons: List[str] = list(
+            elem_rules.get("applies_canonicalizations", [])
+        )
+        canon_absorbed = compute_canon_absorbed(elem_canons, canonicalizations)
+        if not apply_canonicalizations:
+            elem_canons = []
+        return cls(
+            attrs=attrs,
+            rules=rules,
+            element_name=element_name,
+            apply_canonicalizations=apply_canonicalizations,
+            mjs_fields=mjs_fields,
+            consumed_by_setto=consumed_by_setto,
+            mjs_array_caps=mjs_array_caps,
+            global_excl=set(rules.get("global_exclusions", [])),
+            type_map=rules.get("type_mappings", {}),
+            default_type=rules.get("default_type", "float"),
+            renames=rules.get("property_renames", {}),
+            elem_rules=elem_rules,
+            elem_excl=set(elem_rules.get("exclude_attrs", [])),
+            canonicalizations=canonicalizations,
+            attr_to_mjs_field=elem_rules.get("attr_to_mjs_field", {}),
+            elem_canons=elem_canons,
+            canon_absorbed=canon_absorbed,
+            xml_enum_attr_names=set(elem_rules.get("xml_enum_attrs", {}).keys()),
+        )
+
+
+
+def _inject_or_diag(
+    text: str,
+    tag: str,
+    body: str,
+    *,
+    host_path: str,
+    host_kind: str,
+    block_label: str,
+    diag_source: str,
+) -> Tuple[str, bool]:
+    """Inject ``body`` between the ``CODEGEN_<tag>`` markers in ``text``.
+    When the markers are missing AND the body is non-empty, fire a
+    diagnostic that names the host kind (e.g. 'subclass header'), the
+    path, the tag, and what didn't land. Returns (new_text, ok).
+
+    This collapses ~12 near-identical try-inject-then-diag stanzas
+    that used to live in the emit_*_class_injection callers."""
+    new_text, ok = inject_between_tags(text, tag, body, balance_source=diag_source)
+    if not ok and body.strip():
+        _diag_add(
+            f"[diagnostic] {host_kind} '{host_path}' is missing the "
+            f"CODEGEN_{tag}_START/END marker pair; {block_label} was "
+            f"NOT injected.",
+            source=diag_source,
+        )
+    return new_text, ok
+
+
+def _guarded_export(toggle: str, body: str,
+                    extra_cond: Optional[str] = None,
+                    extra_cond_first: bool = False,
+                    multiline: bool = False) -> str:
+    """Wrap ``body`` in an ``if ({toggle})`` guard and return the indented
+    emission.
+
+    ``extra_cond=None``:                 ``if ({toggle}) {body}``
+    ``extra_cond`` + first=False:        ``if ({toggle} && {extra_cond}) {body}``
+    ``extra_cond`` + first=True:         ``if (({extra_cond}) && {toggle}) {body}``
+
+    ``multiline=True``: emit a braced block. ``body`` is dropped verbatim
+    inside the braces (caller controls inner indent — the helper does
+    NOT auto-indent each line so existing multi-line bodies migrate
+    byte-identically). Use for blocks that were previously hand-rolled
+    as ``if ({toggle})\\n{{\\n<body>\\n}}\\n``.
+    """
+    if extra_cond is None:
+        cond = toggle
+    elif extra_cond_first:
+        cond = f"({extra_cond}) && {toggle}"
+    else:
+        cond = f"{toggle} && {extra_cond}"
+    if multiline:
+        return f"    if ({cond})\n    {{\n{body}    }}\n"
+    return f"    if ({cond}) {body}\n"
+
+
+# `_UEType`, `_UE_TYPE_INFO`, and `_attr_default_value` live in
+# `_codegen_core` (re-exported at the top of this file).
 
 
 def _emit_uproperty(prop_name: str, ue_type: str, category_label: str,
                     sub_category: Optional[str] = None,
-                    extra_meta: Optional[str] = None) -> str:
+                    extra_meta: Optional[str] = None,
+                    default_override: Optional[str] = None,
+                    hide_from_details: bool = False) -> str:
     """Emit the toggle + value UPROPERTY pair for a schema-driven attr.
 
     ``extra_meta`` appends to the value UPROPERTY's meta=() block — used to
     inject ``DisplayName``, ``GetOptions``, etc. via per-attr rules in
     ``codegen_rules.json#element_rules.<cat>.property_meta.<attr>``.
+
+    ``hide_from_details=True`` keeps the field as a C++ member (so codegen
+    import paths can still seed it and BPs can still read/write it) but
+    suppresses the Details-panel widget via
+    ``EditCondition="false", EditConditionHides``. Used for properties whose
+    canonical authoring surface is the UE Transform widget (Pos/Quat ->
+    RelativeLocation/Rotation, size -> RelativeScale3D).
     """
     cat = f"MuJoCo|{category_label}"
     if sub_category:
         cat = f"{cat}|{sub_category}"
     toggle = override_toggle_name(prop_name)
-    default = _attr_default_value(ue_type)
-    meta_inner = f'EditCondition="{toggle}"'
+    default = default_override if default_override else _attr_default_value(ue_type)
+    if hide_from_details:
+        # EditConditionHides on the value also collapses the InlineEditConditionToggle
+        # toggle bool above it because UE renders them as a single merged row.
+        meta_inner = 'EditCondition="false", EditConditionHides'
+    else:
+        meta_inner = f'EditCondition="{toggle}"'
     if extra_meta:
         meta_inner = f'{meta_inner}, {extra_meta}'
     return (
@@ -221,11 +328,14 @@ def _emit_uproperty(prop_name: str, ue_type: str, category_label: str,
     )
 
 
-def _emit_import_line(attr: str, prop_name: str, ue_type: str) -> str:
+def _emit_import_line(attr: str, prop_name: str, ue_type: str,
+                      vec3_convert: Optional[str] = None) -> str:
     toggle = override_toggle_name(prop_name)
     key = f'TEXT("{attr}")'
     if ue_type == "float":
         return f'    MjXmlUtils::ReadAttrFloat(Node, {key}, {prop_name}, {toggle});\n'
+    if ue_type == "double":
+        return f'    MjXmlUtils::ReadAttrDouble(Node, {key}, {prop_name}, {toggle});\n'
     if ue_type == "int32":
         return f'    MjXmlUtils::ReadAttrInt(Node, {key}, {prop_name}, {toggle});\n'
     if ue_type == "bool":
@@ -234,7 +344,12 @@ def _emit_import_line(attr: str, prop_name: str, ue_type: str) -> str:
         return (f'    if (MjXmlUtils::ReadAttrString(Node, {key}, {prop_name}))'
                 f' {toggle} = true;\n')
     if ue_type == "FVector":
-        return f'    MjXmlUtils::ReadAttrVec3(Node, {key}, {prop_name}, {toggle});\n'
+        out = f'    MjXmlUtils::ReadAttrVec3(Node, {key}, {prop_name}, {toggle});\n'
+        # MuJoCo direction vectors don't carry units but DO use opposite Y
+        # handedness vs UE. Apply Y-negate post-read when configured.
+        if vec3_convert == "y_negate":
+            out += f'    if ({toggle}) {prop_name}.Y = -{prop_name}.Y;\n'
+        return out
     if ue_type == "FLinearColor":
         return f'    MjXmlUtils::ReadAttrColor(Node, {key}, {prop_name}, {toggle});\n'
     if ue_type.startswith("TArray<float"):
@@ -247,61 +362,23 @@ def _emit_import_line(attr: str, prop_name: str, ue_type: str) -> str:
     return f'    MjXmlUtils::ReadAttrFloat(Node, {key}, {prop_name}, {toggle});\n'
 
 
-def _resolve_mjs_field(attr: str, mjs_fields: set,
-                       attr_to_mjs_field: Dict[str, str] | None = None) -> str:
-    """Pick the correct C field name on a category's mjs struct for a
-    given schema attribute. Per-element ``attr_to_mjs_field`` overrides
-    apply first; otherwise we capture MuJoCo's own naming conventions:
-
-      0. Per-element ``attr_to_mjs_field`` override   (target -> targetbody,
-                                                       focal  -> focal_length)
-      1. ``attr`` itself                (condim -> mjsPair.condim)
-      2. ``attr + "name"``              (body1 -> bodyname1, mesh -> meshname)
-      3. ``<root>name<digits>``         (geom1 -> geomname1, body2 -> bodyname2)
-      4. Underscore-normalised match    (solreflimit -> solref_limit)
-      5. ``actuator`` prefix compressed (actuatorfrclimited -> actfrclimited,
-                                         actuatorgravcomp -> actgravcomp)
-
-    Falls back to ``attr`` if nothing matches — the build will then
-    surface the mismatch loudly."""
-    if attr_to_mjs_field and attr in attr_to_mjs_field:
-        return attr_to_mjs_field[attr]
-    if not mjs_fields:
-        return attr
-    if attr in mjs_fields:
-        return attr
-    if (attr + "name") in mjs_fields:
-        return attr + "name"
-    m = re.match(r"^(.*?)(\d+)$", attr)
-    if m:
-        root = m.group(1)
-        digits = m.group(2)
-        for candidate in (f"{root}name{digits}", f"{root}_name{digits}"):
-            if candidate in mjs_fields:
-                return candidate
-    # Underscore-normalised match — strip all underscores and look for the
-    # same letters. E.g. mjsJoint.solref_limit <- schema solreflimit.
-    norm = attr.replace("_", "")
-    for f in mjs_fields:
-        if f.replace("_", "") == norm:
-            return f
-    # `actuator`-prefix compression: schema spells it out, mjs trims to `act`.
-    if attr.startswith("actuator"):
-        cand = "act" + attr[len("actuator"):]
-        if cand in mjs_fields:
-            return cand
-        cand_norm = cand.replace("_", "")
-        for f in mjs_fields:
-            if f.replace("_", "") == cand_norm:
-                return f
-    return attr
+# Per-strategy resolvers for the schema-attr -> mjs-field name mapper.
+# Each takes (attr, mjs_fields) and returns ``Optional[str]`` — the
+# winning field name, or None to fall through to the next strategy.
+# Order in _MJS_FIELD_RESOLVERS matters: more specific rules first.
+
+
+# The 5-strategy mjs-field resolver chain (+ ``_resolve_mjs_field``)
+# lives in `_codegen_core`.
 
 
 def _emit_export_line(attr: str, prop_name: str, ue_type: str,
                       mjs_fields: set | None = None,
                       slot_sentinel: str | None = None,
                       attr_to_mjs_field: Dict[str, str] | None = None,
-                      consumed_by_setto: set | None = None) -> str:
+                      consumed_by_setto: set | None = None,
+                      vec3_convert: Optional[str] = None,
+                      array_cap: Optional[int] = None) -> str:
     toggle = override_toggle_name(prop_name)
     # If we have an mjs field set and the attr can't be resolved to one of
     # them, this attr isn't directly assignable on the mjs struct (e.g.
@@ -322,31 +399,51 @@ def _emit_export_line(attr: str, prop_name: str, ue_type: str,
     else:
         field = attr_to_mjs_field.get(attr, attr) if attr_to_mjs_field else attr
     if ue_type.startswith("TArray<float") or ue_type.startswith("TArray<int") or ue_type.startswith("TArray<double"):
+        # When the destination C field is a fixed-size array, cap the
+        # loop count against the C array dim so a future MuJoCo shrink
+        # (gainprm[10] -> gainprm[6]) doesn't memory-overrun. Dynamic
+        # vec destinations (mjFloatVec*, mjDoubleVec*) pass array_cap=None
+        # and use the raw prop.Num() — the dynamic vec resizes itself.
+        upper = f"FMath::Min({prop_name}.Num(), {array_cap})" if array_cap else f"{prop_name}.Num()"
         if slot_sentinel is not None:
             # Per-slot sentinel guard: skip slots whose value equals the
             # sentinel (used for `size` so fromto-only imports don't
             # clobber the inherited default radius / earlier slots).
-            return (
-                f'    if ({toggle}) {{ for (int32 i = 0; i < {prop_name}.Num(); ++i)'
-                f' {{ if ({prop_name}[i] != {slot_sentinel}) Element->{field}[i] = {prop_name}[i]; }} }}\n'
+            return _guarded_export(toggle,
+                f'{{ for (int32 i = 0; i < {upper}; ++i)'
+                f' {{ if ({prop_name}[i] != {slot_sentinel}) Element->{field}[i] = {prop_name}[i]; }} }}',
             )
-        return (f'    if ({toggle}) {{ for (int32 i = 0; i < {prop_name}.Num(); ++i)'
-                f' Element->{field}[i] = {prop_name}[i]; }}\n')
+        return _guarded_export(toggle,
+            f'{{ for (int32 i = 0; i < {upper}; ++i)'
+            f' Element->{field}[i] = {prop_name}[i]; }}',
+        )
     if ue_type == "FVector":
-        return (f'    if ({toggle}) {{ Element->{field}[0] = {prop_name}.X;'
-                f' Element->{field}[1] = {prop_name}.Y;'
-                f' Element->{field}[2] = {prop_name}.Z; }}\n')
+        if vec3_convert == "y_negate":
+            return _guarded_export(toggle,
+                f'{{ Element->{field}[0] = {prop_name}.X;'
+                f' Element->{field}[1] = -{prop_name}.Y;'
+                f' Element->{field}[2] = {prop_name}.Z; }}',
+            )
+        return _guarded_export(toggle,
+            f'{{ Element->{field}[0] = {prop_name}.X;'
+            f' Element->{field}[1] = {prop_name}.Y;'
+            f' Element->{field}[2] = {prop_name}.Z; }}',
+        )
     if ue_type == "FLinearColor":
-        return (f'    if ({toggle}) {{ Element->{field}[0] = {prop_name}.R;'
-                f' Element->{field}[1] = {prop_name}.G;'
-                f' Element->{field}[2] = {prop_name}.B;'
-                f' Element->{field}[3] = {prop_name}.A; }}\n')
+        return _guarded_export(toggle,
+            f'{{ Element->{field}[0] = {prop_name}.R;'
+            f' Element->{field}[1] = {prop_name}.G;'
+            f' Element->{field}[2] = {prop_name}.B;'
+            f' Element->{field}[3] = {prop_name}.A; }}',
+        )
     if ue_type == "FString":
-        return (f'    if ({toggle} && !{prop_name}.IsEmpty())'
-                f' mjs_setString(Element->{field}, TCHAR_TO_UTF8(*{prop_name}));\n')
+        # MjSetString (Public/MuJoCo/Utils/MjUtils.h) internally skips empty
+        # FStrings, so the previous `!X.IsEmpty()` guard collapses into the
+        # helper. Toggle-guard stays.
+        return _guarded_export(toggle, f'MjSetString(Element->{field}, {prop_name});')
     if ue_type == "bool":
-        return f'    if ({toggle}) Element->{field} = {prop_name} ? 1 : 0;\n'
-    return f'    if ({toggle}) Element->{field} = {prop_name};\n'
+        return _guarded_export(toggle, f'Element->{field} = {prop_name} ? 1 : 0;')
+    return _guarded_export(toggle, f'Element->{field} = {prop_name};')
 
 
 # --- unit conversion ----------------------------------------------------------
@@ -425,25 +522,21 @@ def _emit_unit_conversion_export(
         return ""
     inner = "\n".join(branches)
     if is_array:
-        return (
-            f"    if ({toggle})\n"
-            f"    {{\n"
+        body = (
             f"        for (int32 i = 0; i < {prop_name}.Num(); ++i)\n"
             f"        {{\n"
             f"            float V = {prop_name}[i];\n"
             f"{inner}\n"
             f"            Element->{mjs_field}[i] = (mjtNum)V;\n"
             f"        }}\n"
-            f"    }}\n"
         )
-    return (
-        f"    if ({toggle})\n"
-        f"    {{\n"
-        f"        float V = {prop_name};\n"
-        f"{inner}\n"
-        f"        Element->{mjs_field} = (mjtNum)V;\n"
-        f"    }}\n"
-    )
+    else:
+        body = (
+            f"        float V = {prop_name};\n"
+            f"{inner}\n"
+            f"        Element->{mjs_field} = (mjtNum)V;\n"
+        )
+    return _guarded_export(toggle, body, multiline=True)
 
 
 def _emit_unit_conversion_block(
@@ -493,12 +586,7 @@ def _apply(per_elem: str) -> str:
     if not branches:
         return ""
     body_lines = "\n".join(branches)
-    return (
-        f"    if ({toggle})\n"
-        f"    {{\n"
-        f"{body_lines}\n"
-        f"    }}\n"
-    )
+    return _guarded_export(toggle, f"{body_lines}\n", multiline=True)
 
 
 def _emit_target_collation_import(prop_name: str, absorbs_attrs: Sequence[str]) -> str:
@@ -524,28 +612,22 @@ def _emit_target_collation_import(prop_name: str, absorbs_attrs: Sequence[str])
 
 
 def _emit_target_collation_export(prop_name: str, mjs_field: str) -> str:
-    return (
-        f'    if (!{prop_name}.IsEmpty()) '
-        f'mjs_setString(Element->{mjs_field}, TCHAR_TO_UTF8(*{prop_name}));\n'
-    )
+    # MjSetString skips empty FStrings internally; preserves the existing
+    # "only write when set" semantics without the explicit guard.
+    return f'    MjSetString(Element->{mjs_field}, {prop_name});\n'
 
 
 def _emit_double_vec_export(attr: str, prop_name: str, mjs_field: str) -> str:
     """Emit export for an attr whose mjs counterpart is a ``mjDoubleVec*``
     (std::vector<double>*) — used by mjsKey for qpos/qvel/act/ctrl/mpos/mquat.
 
-    The mjs side requires ``->clear()`` + ``->push_back()`` per element, not
-    the standard ``Element->X[i] = X[i]`` pattern. Pre-condition: Element
-    field is non-null (mjs allocates the vector pointers on element create).
+    Delegates to ``MjSetDoubleVec`` (Public/MuJoCo/Utils/MjUtils.h) which
+    handles the ``clear() + push_back`` boilerplate. The helper accepts
+    a null Dest so we don't need the ``&& Element->X`` guard at the
+    call site.
     """
     toggle = override_toggle_name(prop_name)
-    return (
-        f"    if ({toggle} && Element->{mjs_field})\n"
-        f"    {{\n"
-        f"        Element->{mjs_field}->clear();\n"
-        f"        for (float V : {prop_name}) Element->{mjs_field}->push_back((double)V);\n"
-        f"    }}\n"
-    )
+    return f"    if ({toggle}) MjSetDoubleVec(Element->{mjs_field}, {prop_name});\n"
 
 
 def _emit_data_packed_export(attr: str, packed_def: Dict[str, Any],
@@ -568,7 +650,6 @@ def _emit_data_packed_export(attr: str, packed_def: Dict[str, Any],
     target = packed_def.get("target", "data")
     condition = packed_def.get("condition")
     toggle = override_toggle_name(prop_name)
-    inner_cond = toggle if condition is None else f"({condition}) && {toggle}"
 
     export_op_name = packed_def.get("export_op")
     op_body = ""
@@ -580,23 +661,23 @@ def _emit_data_packed_export(attr: str, packed_def: Dict[str, Any],
             )
         op_body = op["per_elem"]
 
+    def _wrap(body: str) -> str:
+        return _guarded_export(toggle, body,
+                               extra_cond=condition,
+                               extra_cond_first=True,
+                               multiline=True)
+
     if "slot" in packed_def:
         slot = packed_def["slot"]
         if op_body:
-            return (
-                f"    if ({inner_cond})\n"
-                f"    {{\n"
+            body = (
                 f"        float V = (float){prop_name};\n"
                 f"        {op_body}\n"
                 f"        Element->{target}[{slot}] = (mjtNum)V;\n"
-                f"    }}\n"
             )
-        return (
-            f"    if ({inner_cond})\n"
-            f"    {{\n"
-            f"        Element->{target}[{slot}] = (mjtNum){prop_name};\n"
-            f"    }}\n"
-        )
+            return _wrap(body)
+        body = f"        Element->{target}[{slot}] = (mjtNum){prop_name};\n"
+        return _wrap(body)
     if "slot_range" in packed_def:
         start, end = packed_def["slot_range"]
         count = end - start
@@ -605,27 +686,26 @@ def _emit_data_packed_export(attr: str, packed_def: Dict[str, Any],
                 f"mjs_data_packed_attrs '{attr}': slot_range requires UE TArray; got {ue_type}"
             )
         if op_body:
-            return (
-                f"    if ({inner_cond})\n"
-                f"    {{\n"
+            body = (
                 f"        for (int32 i = 0; i < {prop_name}.Num() && i < {count}; ++i)\n"
                 f"        {{\n"
                 f"            float V = {prop_name}[i];\n"
                 f"            {op_body}\n"
                 f"            Element->{target}[{start} + i] = (mjtNum)V;\n"
                 f"        }}\n"
-                f"    }}\n"
             )
-        return (
-            f"    if ({inner_cond})\n"
-            f"    {{\n"
+            return _wrap(body)
+        body = (
             f"        for (int32 i = 0; i < {prop_name}.Num() && i < {count}; ++i)\n"
             f"            Element->{target}[{start} + i] = (mjtNum){prop_name}[i];\n"
-            f"    }}\n"
         )
+        return _wrap(body)
     raise RuntimeError(f"mjs_data_packed_attrs '{attr}': must specify either 'slot' or 'slot_range'")
 
 
+# `_resolve_value_map` lives in `_codegen_core`.
+
+
 def _emit_xml_enum_import(attr: str, enum_def: Dict[str, Any]) -> str:
     """Emit the XML-string -> URLab-enum mapping inside CODEGEN_IMPORT.
 
@@ -640,29 +720,90 @@ def _emit_xml_enum_import(attr: str, enum_def: Dict[str, Any]) -> str:
         }
     """
     ue_prop = enum_def["ue_property"]
-    value_map: Dict[str, Sequence[str]] = enum_def["value_map"]
+    value_map: Dict[str, Sequence[str]] = _resolve_value_map(attr, enum_def)
     has_toggle = enum_def.get("has_override_toggle", True)
     ue_enum_type = enum_def.get("ue_enum_type", "")
     lines: List[str] = []
     lines.append(f'    {{ // xml_enum: {attr} -> {ue_enum_type or ue_prop}')
     lines.append(f'        FString S = Node->GetAttribute(TEXT("{attr}"));')
     lines.append("        S = S.ToLower();")
+    # Toggle fires only when a known XML value matched. A typo'd value
+    # (e.g. ``type="hindge"``) used to silently set the toggle on the
+    # UE-default enum value — misleadingly signalling "user overrode this"
+    # while actually masking the typo.
+    if has_toggle:
+        lines.append("        bool bMatched = false;")
     branches: List[str] = []
     for xml_val, mapping in value_map.items():
-        ue_enum_member = mapping[0] if isinstance(mapping, list) else mapping
+        ue_enum_member = mapping[0]
         prefix = "else if " if branches else "if      "
+        match_assign = " bMatched = true;" if has_toggle else ""
         branches.append(
-            f'        {prefix}(S == TEXT("{xml_val}")) {ue_prop} = '
-            f'{ue_enum_type}::{ue_enum_member};'
+            f'        {prefix}(S == TEXT("{xml_val}")) '
+            f'{{ {ue_prop} = {ue_enum_type}::{ue_enum_member};{match_assign} }}'
         )
     lines.extend(branches)
     if has_toggle:
         toggle = override_toggle_name(ue_prop)
-        lines.append(f"        if (!S.IsEmpty()) {toggle} = true;")
+        lines.append(f"        if (bMatched) {toggle} = true;")
     lines.append("    }")
     return "\n".join(lines) + "\n"
 
 
+def emit_enum_switch(
+    discriminator: str,
+    enum_name: str,
+    cases: Sequence[Tuple[Any, str]],
+    *,
+    indent: str = "    ",
+    default_break: bool = False,
+    default_body: Optional[str] = None,
+) -> str:
+    """Emit ``switch (discriminator) { case Enum::M: <body>; break; ... }``.
+
+    ``cases`` is a sequence of ``(member_or_members, body)`` pairs:
+      - When ``member_or_members`` is a ``str``, the case label + body +
+        break collapse onto one line: ``case Enum::M: <body>; break;``.
+      - When it is a sequence of strings (multi-key group, e.g. objtype
+        dispatch's BODY/XBODY → ``mjOBJ_BODY``), the case labels stack
+        and the body / break go on separate indented lines:
+          ``case Enum::A:``
+          ``case Enum::B:``
+          ``    <body>;``
+          ``    break;``
+
+    ``default_break=True`` appends a single-line ``default: break;``.
+    ``default_body`` (when set) appends a multi-line ``default:`` arm
+    with that body. ``default_break`` is ignored if ``default_body`` is
+    given.
+
+    All lines are prefixed with ``indent``; case labels use
+    ``indent + 4 spaces``; multi-line bodies use ``indent + 8 spaces``.
+    Result ends with a trailing newline.
+    """
+    inner = indent + "    "
+    body_indent = inner + "    "
+    lines: List[str] = []
+    lines.append(f"{indent}switch ({discriminator})")
+    lines.append(f"{indent}{{")
+    for members, body in cases:
+        if isinstance(members, str):
+            lines.append(f"{inner}case {enum_name}::{members}: {body}; break;")
+            continue
+        for m in members:
+            lines.append(f"{inner}case {enum_name}::{m}:")
+        lines.append(f"{body_indent}{body};")
+        lines.append(f"{body_indent}break;")
+    if default_body is not None:
+        lines.append(f"{inner}default:")
+        lines.append(f"{body_indent}{default_body};")
+        lines.append(f"{body_indent}break;")
+    elif default_break:
+        lines.append(f"{inner}default: break;")
+    lines.append(f"{indent}}}")
+    return "\n".join(lines) + "\n"
+
+
 def _emit_xml_enum_export(attr: str, enum_def: Dict[str, Any]) -> str:
     """Emit the URLab-enum -> mjt* / mjsField mapping inside CODEGEN_EXPORT.
 
@@ -677,386 +818,535 @@ def _emit_xml_enum_export(attr: str, enum_def: Dict[str, Any]) -> str:
     """
     ue_prop = enum_def["ue_property"]
     ue_enum_type = enum_def.get("ue_enum_type", "")
-    value_map: Dict[str, Sequence[str]] = enum_def["value_map"]
+    value_map: Dict[str, Sequence[str]] = _resolve_value_map(attr, enum_def)
     mjs_field = enum_def.get("mjs_field")
     if not mjs_field:
         return ""
     mjs_cast = enum_def.get("mjs_cast", "")
     has_toggle = enum_def.get("has_override_toggle", True)
     indent = "        " if has_toggle else "    "
-    lines: List[str] = []
-    if has_toggle:
-        toggle = override_toggle_name(ue_prop)
-        lines.append(f"    if ({toggle})")
-        lines.append("    {")
-    lines.append(f"{indent}switch ({ue_prop})")
-    lines.append(f"{indent}{{")
+    cases: List[Tuple[Any, str]] = []
     for _, mapping in value_map.items():
         if not isinstance(mapping, list) or len(mapping) < 2:
             raise RuntimeError(
                 f"xml_enum {attr}: value_map entries must be [UE_enum, mj_const]; got {mapping!r}"
             )
         ue_enum_member, mj_const = mapping[0], mapping[1]
-        cast_lhs = f"Element->{mjs_field}"
         cast_expr = f"({mjs_cast}){mj_const}" if mjs_cast else mj_const
-        lines.append(
-            f"{indent}    case {ue_enum_type}::{ue_enum_member}: {cast_lhs} = {cast_expr}; break;"
-        )
-    lines.append(f"{indent}    default: break;")
-    lines.append(f"{indent}}}")
+        cases.append((ue_enum_member, f"Element->{mjs_field} = {cast_expr}"))
+    switch_body = emit_enum_switch(
+        ue_prop, ue_enum_type, cases,
+        indent=indent, default_break=True,
+    )
     if has_toggle:
-        lines.append("    }")
-    return "\n".join(lines) + "\n"
+        toggle = override_toggle_name(ue_prop)
+        return _guarded_export(toggle, switch_body, multiline=True)
+    return switch_body
 
 
-def _canon_property_block(canon_name: str, canon_def: Dict[str, Any], category_label: str) -> str:
+def _canon_property_block(canon_name: str, canon_def: Dict[str, Any],
+                          category_label: str,
+                          hide_from_details: bool = False) -> str:
     prop = canon_def.get("emits_property")
     if not prop:
         return ""
     return _emit_uproperty(prop["name"], prop["type"], category_label,
-                           sub_category=canon_name.replace("_", " ").title())
+                           sub_category=canon_name.replace("_", " ").title(),
+                           hide_from_details=hide_from_details)
 
 
-def _canon_import_block(canon_name: str, canon_def: Dict[str, Any], element_name: str = "") -> str:
+# ---------------------------------------------------------------------------
+# Canonicalisation registry
+#
+# Five canonicalisations collapse multiple raw MJCF attrs into one URLab
+# property: spatial_pose (pos -> Pos), orientation (quat/euler/etc -> Quat),
+# fromto_decompose (fromto -> Pos/Quat/size), body_sleep_policy (sleep ->
+# SleepPolicy enum), actuator_transmission (joint/tendon/site/... ->
+# TransmissionType + names). The previous design hard-coded each as an
+# if-branch in _canon_import_block / _canon_export_block; a new rule
+# entry without a matching branch silently dropped its emission. The
+# registry below is the single source of truth.
+# ---------------------------------------------------------------------------
+
+
+def _canon_import_body_sleep_policy(canon_def, element_name) -> str:
+    return (
+        '    { // canonicalize body.sleep -> EMjBodySleepPolicy + bOverride_SleepPolicy\n'
+        '        FString S = Node->GetAttribute(TEXT("sleep"));\n'
+        '        S = S.ToLower();\n'
+        '        if      (S == TEXT("never"))   { SleepPolicy = EMjBodySleepPolicy::Never;      bOverride_SleepPolicy = true; }\n'
+        '        else if (S == TEXT("allowed")) { SleepPolicy = EMjBodySleepPolicy::Allowed;    bOverride_SleepPolicy = true; }\n'
+        '        else if (S == TEXT("init"))    { SleepPolicy = EMjBodySleepPolicy::InitAsleep; bOverride_SleepPolicy = true; }\n'
+        '    }\n'
+    )
+
+
+def _canon_import_actuator_transmission(canon_def, element_name) -> str:
+    return (
+        '    { // canonicalize actuator transmission attrs\n'
+        '        FString TrnTarget;\n'
+        '        if (MjXmlUtils::ReadAttrString(Node, TEXT("joint"), TrnTarget))\n'
+        '        {\n'
+        '            TransmissionType = EMjActuatorTrnType::Joint;\n'
+        '            TargetName = TrnTarget;\n'
+        '        }\n'
+        '        if (MjXmlUtils::ReadAttrString(Node, TEXT("jointinparent"), TrnTarget))\n'
+        '        {\n'
+        '            TransmissionType = EMjActuatorTrnType::JointInParent;\n'
+        '            TargetName = TrnTarget;\n'
+        '        }\n'
+        '        if (MjXmlUtils::ReadAttrString(Node, TEXT("tendon"), TrnTarget))\n'
+        '        {\n'
+        '            TransmissionType = EMjActuatorTrnType::Tendon;\n'
+        '            TargetName = TrnTarget;\n'
+        '        }\n'
+        '        if (MjXmlUtils::ReadAttrString(Node, TEXT("site"), TrnTarget))\n'
+        '        {\n'
+        '            TransmissionType = EMjActuatorTrnType::Site;\n'
+        '            TargetName = TrnTarget;\n'
+        '        }\n'
+        '        if (MjXmlUtils::ReadAttrString(Node, TEXT("body"), TrnTarget))\n'
+        '        {\n'
+        '            TransmissionType = EMjActuatorTrnType::Body;\n'
+        '            TargetName = TrnTarget;\n'
+        '        }\n'
+        '        MjXmlUtils::ReadAttrString(Node, TEXT("refsite"), RefSite);\n'
+        '        // SliderSite must run AFTER the main-target reads so it\n'
+        '        // overrides the transmission type while preserving the\n'
+        '        // TargetName captured above (per MJCF: slider-crank carries\n'
+        '        // a main target attr PLUS slidersite).\n'
+        '        if (MjXmlUtils::ReadAttrString(Node, TEXT("slidersite"), SliderSite))\n'
+        '        {\n'
+        '            TransmissionType = EMjActuatorTrnType::SliderCrank;\n'
+        '        }\n'
+        '    }\n'
+    )
+
+
+def _canon_import_fromto_decompose(canon_def, element_name) -> str:
+    helper = canon_def["import_helper"]
+    cat_spec = canon_def.get("category_specifics", {}).get(element_name, {})
+    z_slot_check = cat_spec.get("z_slot_check", "false")
+    return (
+        f'    {{ // canonicalize fromto -> Pos/Quat/size half-length\n'
+        f'        FVector FTPos; FQuat FTQuat; float FTHalf = 0.f;\n'
+        f'        if ({helper}(Node, FTPos, FTQuat, FTHalf))\n'
+        f'        {{\n'
+        f'            Pos = FTPos; bOverride_Pos = true;\n'
+        f'            Quat = FTQuat; bOverride_Quat = true;\n'
+        f'            const bool bFTZSlot = {z_slot_check};\n'
+        f'            const int32 FTSlot = bFTZSlot ? 2 : 1;\n'
+        f'            while (size.Num() <= FTSlot) {{ size.Add(-1.0f); }}\n'
+        f'            size[FTSlot] = FTHalf;\n'
+        f'            bOverride_size = true;\n'
+        f'        }}\n'
+        f'    }}\n'
+    )
+
+
+def _canon_import_orientation(canon_def, element_name) -> str:
     prop = canon_def.get("emits_property")
     helper = canon_def["import_helper"]
-    if canon_name == "body_sleep_policy":
-        # MJCF body.sleep is a string ("never"/"allowed"/"init"); map to URLab's
-        # EMjBodySleepPolicy. Absent attr leaves SleepPolicy at its default
-        # (Default = let the global option decide).
-        return (
-            '    { // canonicalize body.sleep -> EMjBodySleepPolicy\n'
-            '        FString S = Node->GetAttribute(TEXT("sleep"));\n'
-            '        S = S.ToLower();\n'
-            '        if      (S == TEXT("never"))   SleepPolicy = EMjBodySleepPolicy::Never;\n'
-            '        else if (S == TEXT("allowed")) SleepPolicy = EMjBodySleepPolicy::Allowed;\n'
-            '        else if (S == TEXT("init"))    SleepPolicy = EMjBodySleepPolicy::InitAsleep;\n'
-            '    }\n'
-        )
-    if canon_name == "actuator_transmission":
-        # MJCF actuators encode transmission via {joint,tendon,site,body,
-        # jointinparent} (target name) + optional {slidersite, refsite}.
-        # Collapse into URLab's TransmissionType enum + Target/Slider/RefSite
-        # strings. First match wins (matches the legacy ordering).
-        return (
-            '    { // canonicalize actuator transmission attrs\n'
-            '        FString TrnTarget;\n'
-            '        if (MjXmlUtils::ReadAttrString(Node, TEXT("joint"), TrnTarget))\n'
-            '        {\n'
-            '            TransmissionType = EMjActuatorTrnType::Joint;\n'
-            '            TargetName = TrnTarget;\n'
-            '        }\n'
-            '        if (MjXmlUtils::ReadAttrString(Node, TEXT("jointinparent"), TrnTarget))\n'
-            '        {\n'
-            '            TransmissionType = EMjActuatorTrnType::JointInParent;\n'
-            '            TargetName = TrnTarget;\n'
-            '        }\n'
-            '        if (MjXmlUtils::ReadAttrString(Node, TEXT("tendon"), TrnTarget))\n'
-            '        {\n'
-            '            TransmissionType = EMjActuatorTrnType::Tendon;\n'
-            '            TargetName = TrnTarget;\n'
-            '        }\n'
-            '        if (MjXmlUtils::ReadAttrString(Node, TEXT("slidersite"), SliderSite))\n'
-            '        {\n'
-            '            TransmissionType = EMjActuatorTrnType::SliderCrank;\n'
-            '        }\n'
-            '        if (MjXmlUtils::ReadAttrString(Node, TEXT("site"), TrnTarget))\n'
-            '        {\n'
-            '            TransmissionType = EMjActuatorTrnType::Site;\n'
-            '            TargetName = TrnTarget;\n'
-            '        }\n'
-            '        MjXmlUtils::ReadAttrString(Node, TEXT("refsite"), RefSite);\n'
-            '        if (MjXmlUtils::ReadAttrString(Node, TEXT("body"), TrnTarget))\n'
-            '        {\n'
-            '            TransmissionType = EMjActuatorTrnType::Body;\n'
-            '            TargetName = TrnTarget;\n'
-            '        }\n'
-            '    }\n'
-        )
-    if canon_name == "fromto_decompose":
-        # MJCF's `fromto="x1 y1 z1 x2 y2 z2"` is decomposed into URLab's
-        # canonical Pos/Quat plus a half-length scalar that lands in either
-        # size[1] or size[2] of the codegen-owned TArray, depending on shape
-        # Type. Slots before the half-length slot are filled with the
-        # ``-1.0f`` sentinel so the per-attr export skips them (preserves
-        # the parent default class's radius / earlier-slot defaults — the
-        # fromto-only XML form does NOT specify the radius).
-        cat_spec = canon_def.get("category_specifics", {}).get(element_name, {})
-        z_slot_check = cat_spec.get("z_slot_check", "false")
-        return (
-            f'    {{ // canonicalize fromto -> Pos/Quat/size half-length\n'
-            f'        FVector FTPos; FQuat FTQuat; float FTHalf = 0.f;\n'
-            f'        if ({helper}(Node, FTPos, FTQuat, FTHalf))\n'
-            f'        {{\n'
-            f'            Pos = FTPos; bOverride_Pos = true;\n'
-            f'            Quat = FTQuat; bOverride_Quat = true;\n'
-            f'            const bool bFTZSlot = {z_slot_check};\n'
-            f'            const int32 FTSlot = bFTZSlot ? 2 : 1;\n'
-            f'            while (size.Num() <= FTSlot) {{ size.Add(-1.0f); }}\n'
-            f'            size[FTSlot] = FTHalf;\n'
-            f'            bOverride_size = true;\n'
-            f'        }}\n'
-            f'    }}\n'
-        )
-    if canon_name == "orientation":
-        # OrientationToMjQuat(Node, CompilerSettings, double OutQuat[4]) -> bool
-        # MjToUERotation applies the wxyz->xyzw reorder AND the X/Z sign-flip
-        # for the RHS-to-LHS handedness conversion. Storing the result in the
-        # UPROPERTY gives a valid UE-frame rotation usable by SetRelativeRotation
-        # and visible in the details panel.
-        return (
-            f'    {{ // canonicalize orientation (quat/euler/axisangle/xyaxes/zaxis)\n'
-            f'        double TmpQuat[4] = {{1.0, 0.0, 0.0, 0.0}};\n'
-            f'        if ({helper}(Node, CompilerSettings, TmpQuat))\n'
-            f'        {{\n'
-            f'            {prop["name"]} = MjUtils::MjToUERotation(TmpQuat);\n'
-            f'            {override_toggle_name(prop["name"])} = true;\n'
-            f'        }}\n'
-            f'    }}\n'
-        )
-    if canon_name == "spatial_pose":
-        # ReadVec3InMeters(Node, key, OutVec, bOutHasField)
-        return (
-            f'    {helper}(Node, TEXT("pos"), {prop["name"]}, {override_toggle_name(prop["name"])});\n'
-        )
+    return (
+        f'    {{ // canonicalize orientation (quat/euler/axisangle/xyaxes/zaxis)\n'
+        f'        double TmpQuat[4] = {{1.0, 0.0, 0.0, 0.0}};\n'
+        f'        if ({helper}(Node, CompilerSettings, TmpQuat))\n'
+        f'        {{\n'
+        f'            {prop["name"]} = MjUtils::MjToUERotation(TmpQuat);\n'
+        f'            {override_toggle_name(prop["name"])} = true;\n'
+        f'        }}\n'
+        f'    }}\n'
+    )
+
+
+def _canon_import_spatial_pose(canon_def, element_name) -> str:
+    prop = canon_def.get("emits_property")
+    helper = canon_def["import_helper"]
+    return (
+        f'    {helper}(Node, TEXT("pos"), {prop["name"]}, {override_toggle_name(prop["name"])});\n'
+    )
+
+
+def _canon_export_body_sleep_policy(canon_def) -> str:
+    return _guarded_export(
+        "bOverride_SleepPolicy",
+        "        Element->sleep = static_cast<mjtSleepPolicy>(static_cast<uint8>(SleepPolicy));\n",
+        multiline=True,
+    )
+
+
+def _canon_export_actuator_transmission(canon_def) -> str:
+    return (
+        '    MjSetString(Element->target, TargetName);\n'
+        '    switch (TransmissionType)\n'
+        '    {\n'
+        '        case EMjActuatorTrnType::Joint:          Element->trntype = mjTRN_JOINT;         break;\n'
+        '        case EMjActuatorTrnType::JointInParent:  Element->trntype = mjTRN_JOINTINPARENT; break;\n'
+        '        case EMjActuatorTrnType::SliderCrank:    Element->trntype = mjTRN_SLIDERCRANK;   break;\n'
+        '        case EMjActuatorTrnType::Tendon:         Element->trntype = mjTRN_TENDON;        break;\n'
+        '        case EMjActuatorTrnType::Site:           Element->trntype = mjTRN_SITE;          break;\n'
+        '        case EMjActuatorTrnType::Body:           Element->trntype = mjTRN_BODY;          break;\n'
+        '        default:                                 Element->trntype = mjTRN_UNDEFINED;     break;\n'
+        '    }\n'
+        '    if (TransmissionType == EMjActuatorTrnType::SliderCrank && !SliderSite.IsEmpty())\n'
+        '    {\n'
+        '        MjSetStringRaw(Element->slidersite, SliderSite);\n'
+        '    }\n'
+        '    if (TransmissionType == EMjActuatorTrnType::Site && !RefSite.IsEmpty())\n'
+        '    {\n'
+        '        MjSetStringRaw(Element->refsite, RefSite);\n'
+        '    }\n'
+    )
+
+
+def _canon_export_fromto_decompose(canon_def) -> str:
+    # fromto resolves to Pos/Quat/Size on import — those properties carry
+    # the values through to export via their own canonicalisations.
     return ""
 
 
-def _canon_export_block(canon_name: str, canon_def: Dict[str, Any]) -> str:
+def _canon_export_orientation(canon_def) -> str:
     prop = canon_def.get("emits_property")
     helper = canon_def.get("export_helper")
-    if canon_name == "body_sleep_policy":
-        # Only write to mjsBody.sleep when the user chose a non-Default policy.
-        # Default lets the global AAMjManager option decide.
-        return (
-            '    if (SleepPolicy != EMjBodySleepPolicy::Default)\n'
-            '    {\n'
-            '        Element->sleep = static_cast<mjtSleepPolicy>(static_cast<uint8>(SleepPolicy));\n'
-            '    }\n'
-        )
-    if canon_name == "actuator_transmission":
-        # Write the URLab transmission state back to mjsActuator fields. Strings
-        # are only written when non-empty (mjs_setString must not be called on
-        # an empty FString — its TCHAR_TO_UTF8 buffer would dangle).
-        return (
-            '    if (!TargetName.IsEmpty()) mjs_setString(Element->target, TCHAR_TO_UTF8(*TargetName));\n'
-            '    switch (TransmissionType)\n'
-            '    {\n'
-            '        case EMjActuatorTrnType::Joint:          Element->trntype = mjTRN_JOINT;         break;\n'
-            '        case EMjActuatorTrnType::JointInParent:  Element->trntype = mjTRN_JOINTINPARENT; break;\n'
-            '        case EMjActuatorTrnType::SliderCrank:    Element->trntype = mjTRN_SLIDERCRANK;   break;\n'
-            '        case EMjActuatorTrnType::Tendon:         Element->trntype = mjTRN_TENDON;        break;\n'
-            '        case EMjActuatorTrnType::Site:           Element->trntype = mjTRN_SITE;          break;\n'
-            '        case EMjActuatorTrnType::Body:           Element->trntype = mjTRN_BODY;          break;\n'
-            '        default:                                 Element->trntype = mjTRN_UNDEFINED;     break;\n'
-            '    }\n'
-            '    if (TransmissionType == EMjActuatorTrnType::SliderCrank && !SliderSite.IsEmpty())\n'
-            '    {\n'
-            '        mjs_setString(Element->slidersite, TCHAR_TO_UTF8(*SliderSite));\n'
-            '    }\n'
-            '    if (TransmissionType == EMjActuatorTrnType::Site && !RefSite.IsEmpty())\n'
-            '    {\n'
-            '        mjs_setString(Element->refsite, TCHAR_TO_UTF8(*RefSite));\n'
-            '    }\n'
-        )
-    if canon_name == "fromto_decompose":
-        # fromto resolves to Pos/Quat/Size on import — those properties carry
-        # the values through to export via their own canonicalisations. No
-        # export-side emission needed here.
-        return ""
-    if canon_name == "orientation":
-        return (
-            f'    if ({override_toggle_name(prop["name"])})\n'
-            f'    {{\n'
-            f'        double TmpQuat[4];\n'
-            f'        {helper}({prop["name"]}, TmpQuat);\n'
-            f'        Element->quat[0] = TmpQuat[0]; Element->quat[1] = TmpQuat[1];\n'
-            f'        Element->quat[2] = TmpQuat[2]; Element->quat[3] = TmpQuat[3];\n'
-            f'    }}\n'
-        )
-    if canon_name == "spatial_pose":
-        return (
-            f'    if ({override_toggle_name(prop["name"])})\n'
-            f'    {{\n'
-            f'        double TmpPos[3];\n'
-            f'        {helper}({prop["name"]}, TmpPos);\n'
-            f'        Element->pos[0] = TmpPos[0]; Element->pos[1] = TmpPos[1]; Element->pos[2] = TmpPos[2];\n'
-            f'    }}\n'
-        )
-    return ""
+    return _guarded_export(
+        override_toggle_name(prop["name"]),
+        f'        double TmpQuat[4];\n'
+        f'        {helper}({prop["name"]}, TmpQuat);\n'
+        f'        Element->quat[0] = TmpQuat[0]; Element->quat[1] = TmpQuat[1];\n'
+        f'        Element->quat[2] = TmpQuat[2]; Element->quat[3] = TmpQuat[3];\n',
+        multiline=True,
+    )
 
 
-def emit_schema_for_attrs(
-    attrs: Sequence[str],
-    rules: Dict[str, Any],
-    element_name: str,
-    category_label: str,
-    *,
-    apply_canonicalizations: bool = True,
-    mjs_fields: set | None = None,
-    consumed_by_setto: set | None = None,
-) -> EmittedSchema:
-    """Emit UPROPERTY + import/export blocks for a list of schema attrs.
+def _canon_export_spatial_pose(canon_def) -> str:
+    prop = canon_def.get("emits_property")
+    helper = canon_def.get("export_helper")
+    return _guarded_export(
+        override_toggle_name(prop["name"]),
+        f'        double TmpPos[3];\n'
+        f'        {helper}({prop["name"]}, TmpPos);\n'
+        f'        Element->pos[0] = TmpPos[0]; Element->pos[1] = TmpPos[1]; Element->pos[2] = TmpPos[2];\n',
+        multiline=True,
+    )
 
-    Applies global exclusions, element exclusions, canonicalisation
-    absorption, and type mappings from `codegen_rules.json`.
 
-    Pass ``apply_canonicalizations=False`` when emitting for a subclass
-    in a category whose base class already owns the canonicalised
-    properties (e.g. site/tendon/equality subtypes in multi_uclass
-    layout, or geom subtypes that share base canonicalisations).
-    Raw canonicalised attrs (pos/quat/euler/etc.) are still excluded
-    so they don't re-appear as per-attr UPROPERTYs.
+@dataclass(frozen=True)
+class Canonicalisation:
+    """One canonicalisation's import + export emitters. Looked up by
+    canon name from `codegen_rules.json[canonicalizations]`. The
+    importer takes (canon_def, element_name) and returns C++ text;
+    the exporter takes (canon_def) and returns C++ text."""
+    name:   str
+    import_emitter: Callable[[Dict[str, Any], str], str]
+    export_emitter: Callable[[Dict[str, Any]], str]
+
+
+_CANONICALISATIONS: Dict[str, Canonicalisation] = {
+    c.name: c for c in [
+        Canonicalisation("body_sleep_policy",
+                         _canon_import_body_sleep_policy,
+                         _canon_export_body_sleep_policy),
+        Canonicalisation("actuator_transmission",
+                         _canon_import_actuator_transmission,
+                         _canon_export_actuator_transmission),
+        Canonicalisation("fromto_decompose",
+                         _canon_import_fromto_decompose,
+                         _canon_export_fromto_decompose),
+        Canonicalisation("orientation",
+                         _canon_import_orientation,
+                         _canon_export_orientation),
+        Canonicalisation("spatial_pose",
+                         _canon_import_spatial_pose,
+                         _canon_export_spatial_pose),
+    ]
+}
 
-    ``consumed_by_setto`` is the set of schema attrs that will be packed
-    into the mjs spec by an ``mjs_setTo*`` call prepended to the export
-    block (see ``_emit_setto_call``). For those attrs, we suppress the
-    fallback "(skipped: mjs has no field)" comment to keep the emitted
-    file uncluttered.
-    """
-    global_excl = set(rules.get("global_exclusions", []))
-    type_map: Dict[str, str] = rules.get("type_mappings", {})
-    default_type: str = rules.get("default_type", "float")
-    renames: Dict[str, str] = rules.get("property_renames", {})
 
-    elem_rules: Dict[str, Any] = rules.get("element_rules", {}).get(element_name, {})
-    elem_excl = set(elem_rules.get("exclude_attrs", []))
-    elem_canons: List[str] = elem_rules.get("applies_canonicalizations", [])
-    canonicalizations: Dict[str, Any] = rules.get("canonicalizations", {})
+def _canon_import_block(canon_name: str, canon_def: Dict[str, Any], element_name: str = "") -> str:
+    entry = _CANONICALISATIONS.get(canon_name)
+    if entry is None:
+        _diag_add(
+            f"[diagnostic] canonicalization '{canon_name}' has a rule entry "
+            f"but no registry entry in _CANONICALISATIONS; per-element XML "
+            f"import for absorbed attrs will be silently dropped.",
+            source="canon_dispatch",
+        )
+        return ""
+    return entry.import_emitter(canon_def, element_name)
 
-    # All attrs absorbed by an applied canonicalisation are silently excluded
-    # from per-attr emission (the canonicalisation owns them collectively).
-    canon_absorbed: set = set()
-    for c in elem_canons:
-        cd = canonicalizations.get(c)
-        if cd:
-            canon_absorbed.update(cd.get("absorbs_attrs", []))
 
-    # If we're emitting a subtype and the base already owns the
-    # canonicalisations, suppress the canonicalised property block here.
-    # Raw absorbed attrs still excluded so they don't fall through to
-    # per-attr emission.
-    if not apply_canonicalizations:
-        elem_canons = []
+def _canon_export_block(canon_name: str, canon_def: Dict[str, Any]) -> str:
+    entry = _CANONICALISATIONS.get(canon_name)
+    if entry is None:
+        _diag_add(
+            f"[diagnostic] canonicalization '{canon_name}' has a rule entry "
+            f"but no registry entry in _CANONICALISATIONS; per-element mjs "
+            f"export for absorbed attrs will be silently dropped.",
+            source="canon_dispatch",
+        )
+        return ""
+    return entry.export_emitter(canon_def)
 
-    props = ""
-    imports = ""
-    exports = ""
 
-    # 1) Canonicalisations first (so canonical properties appear at the top).
+# ---------------------------------------------------------------------------
+# Property-block emitters
+#
+# emit_schema_for_attrs used to inline both the canonicalisation property
+# loop and the per-attr property loop. They're factored out here so the
+# orchestration in emit_schema_for_attrs reads as "props + imports +
+# exports" instead of one 400-line function.
+# ---------------------------------------------------------------------------
+
+
+def _emit_canon_property_block(
+    elem_canons: List[str],
+    canonicalizations: Dict[str, Any],
+    category_label: str,
+    hidden_canons: Set[str],
+) -> str:
+    """Emit canonicalisation-owned UPROPERTY blocks at the top of the
+    component so the canonical authoring surface (Pos / Quat / etc)
+    appears first in the Details panel."""
+    out = ""
     for c in elem_canons:
         cd = canonicalizations.get(c)
         if not cd:
             continue
-        props += _canon_property_block(c, cd, category_label)
+        out += _canon_property_block(
+            c, cd, category_label,
+            hide_from_details=(c in hidden_canons),
+        )
+    return out
+
 
-    # 2) Per-attribute properties. Attrs handled by xml_enum_attrs use a hand-
-    # declared URLab enum UPROPERTY; skip default per-attr UPROPERTY emission
-    # for those (codegen only owns the import/export mapping, not the decl).
-    xml_enum_attr_names: set = set(elem_rules.get("xml_enum_attrs", {}).keys())
+def _emit_per_attr_property_block(
+    attrs: Sequence[str],
+    elem_rules: Dict[str, Any],
+    type_map: Dict[str, str],
+    default_type: str,
+    renames: Dict[str, str],
+    category_label: str,
+    *,
+    global_excl: Set[str],
+    elem_excl: Set[str],
+    canon_absorbed: Set[str],
+    xml_enum_attr_names: Set[str],
+    hidden_attrs: Set[str],
+) -> str:
+    """Emit per-attr UPROPERTY blocks (the bulk of the component's
+    Details panel). Attrs handled by xml_enum_attrs are skipped — those
+    use a hand-declared URLab enum UPROPERTY; codegen owns only the
+    import/export mapping.
+
+    ``property_units`` lowers to UE-native ``Units="m"`` meta on numeric
+    scalars/structs; on TArray/FString/FLinearColor it lowers to a
+    custom ``MjUnit="m"`` meta tag the URLab detail customization reads
+    at runtime (UHT rejects Units on those types)."""
     prop_meta: Dict[str, str] = elem_rules.get("property_meta", {})
-    for attr in attrs:
-        if attr in global_excl or attr in elem_excl or attr in canon_absorbed:
-            continue
-        if attr in xml_enum_attr_names:
-            continue
+    property_defaults: Dict[str, str] = elem_rules.get("property_defaults", {})
+    property_units: Dict[str, str] = elem_rules.get("property_units", {})
+    out = ""
+    for attr in iter_category_attrs(attrs, global_excl=global_excl,
+                                     elem_excl=elem_excl,
+                                     canon_absorbed=canon_absorbed,
+                                     extra_excl=xml_enum_attr_names):
         ue_type = type_map.get(attr, default_type)
-        prop_name = renames.get(attr, pascal_case(attr))
-        props += _emit_uproperty(
+        prop_name = renames.get(attr, attr)
+        attr_meta_parts: List[str] = []
+        if prop_meta.get(attr):
+            attr_meta_parts.append(prop_meta[attr])
+        unit = property_units.get(attr) or property_units.get(prop_name)
+        if unit:
+            if _ue_type_accepts_units_meta(ue_type):
+                attr_meta_parts.append(f'Units="{unit}"')
+            else:
+                attr_meta_parts.append(f'MjUnit="{unit}"')
+        out += _emit_uproperty(
             prop_name, ue_type, category_label,
-            extra_meta=prop_meta.get(attr),
+            extra_meta=", ".join(attr_meta_parts) if attr_meta_parts else None,
+            default_override=property_defaults.get(attr) or property_defaults.get(prop_name),
+            hide_from_details=(attr in hidden_attrs or prop_name in hidden_attrs),
+        )
+    return out
+
+
+def _emit_xml_enum_property_decls(
+    elem_rules: Dict[str, Any],
+    category_label: str,
+    *,
+    apply_canonicalizations: bool,
+) -> str:
+    """Emit UPROPERTY decls for xml_enum_attrs entries that opt in via
+    ``emit_property_decl: true``. URLab usually hand-declares the
+    UE-side enum properties (e.g. ``EMjJointType Type`` in MjJoint.h);
+    the codegen-owned path is reserved for cases where the rule author
+    wants the decl + initializer to live in the rule JSON. Only fires
+    on the BASE class emission (subclasses would shadow the base decl
+    and fail UHT).
+
+    Per-rule ``property_default`` lets the enum decl take a real
+    initializer like ``EMjGainType::Fixed`` instead of the ``{}``
+    fallback. A typo'd default (``"Fxed"``) surfaces as a codegen
+    diagnostic, not a misleading UHT compile error far downstream."""
+    if not apply_canonicalizations:
+        return ""
+    out = ""
+    for attr, enum_def in elem_rules.get("xml_enum_attrs", {}).items():
+        if not enum_def.get("emit_property_decl"):
+            continue
+        ue_prop = enum_def.get("ue_property")
+        ue_enum_type = enum_def.get("ue_enum_type")
+        if not ue_prop or not ue_enum_type:
+            continue
+        prop_default = enum_def.get("property_default")
+        if prop_default:
+            value_map = enum_def.get("value_map", {})
+            valid_members = {m[0] for m in value_map.values()
+                             if isinstance(m, (list, tuple)) and m}
+            if valid_members and prop_default not in valid_members:
+                _diag_add(
+                    f"[diagnostic] xml_enum '{attr}' property_default "
+                    f"'{prop_default}' is not in value_map UE members "
+                    f"({sorted(valid_members)}); UPROPERTY default will "
+                    f"fail UHT compile.",
+                    source="property_default",
+                )
+        full_default = (
+            f"{ue_enum_type}::{prop_default}" if prop_default else None
+        )
+        out += _emit_uproperty(
+            ue_prop, ue_enum_type, category_label,
+            extra_meta=enum_def.get("property_meta"),
+            default_override=full_default,
         )
+    return out
+
+
+def _emit_imports_block(ctx: EmissionContext) -> str:
+    """Emit the ``ImportFromXml`` body — every read-from-XML line. Order:
+    common imports prefix (name/class), xml_enum imports, per-attr
+    imports, canon imports, SetRelativeTransform driver, target-
+    collation imports, unit-conversion imports."""
+    attrs            = ctx.attrs
+    elem_rules       = ctx.elem_rules
+    type_map         = ctx.type_map
+    default_type     = ctx.default_type
+    renames          = ctx.renames
+    global_excl      = ctx.global_excl
+    elem_excl        = ctx.elem_excl
+    canon_absorbed   = ctx.canon_absorbed
+    elem_canons      = ctx.elem_canons
+    canonicalizations = ctx.canonicalizations
+    apply_canon      = ctx.apply_canonicalizations
+    xml_enum_attr_names = ctx.xml_enum_attr_names
+
+    imports = ""
+
+    # 2a) Common imports prefix (name / class) — emits at the top.
+    _COMMON_IMPORT_FIELDS = {"name": "MjName", "class": "MjClassName"}
+    common_imports_list = elem_rules.get("common_imports", []) if apply_canon else []
+    if common_imports_list:
+        common_block = ""
+        for entry in common_imports_list:
+            if isinstance(entry, str):
+                attr = entry
+                ue_field = _COMMON_IMPORT_FIELDS.get(attr, attr)
+            else:
+                attr = entry["attr"]
+                ue_field = entry.get("ue_field", _COMMON_IMPORT_FIELDS.get(attr, attr))
+            common_block += (
+                f'    MjXmlUtils::ReadAttrString(Node, TEXT("{attr}"), {ue_field});\n'
+            )
+        imports = common_block + imports
 
-    # 3a) XML enum attr imports run FIRST so downstream emission can depend
-    # on the resolved enum value (e.g. fromto_decompose reads Type to pick
-    # the size[] slot for the half-length write). URLab enums (EMjGeomType,
-    # EMjJointType, etc.) are hand-declared; codegen owns the string<->enum
-    # string map.
-    #
-    # Emission policy: emit xml_enum_attrs[attr] at this level if
-    #   - attr is in the current attrs list (subtype-specific case like
-    #     dcmotor.input — fires on the subtype's emission), OR
-    #   - apply_canonicalizations=True (base-class emission) AND attr is in
-    #     global_exclusions (always-emit-at-base case like joint.type — the
-    #     attr is excluded from per-attr UPROPERTY emission but xml_enum
-    #     owns its import/export).
+    # 3a) XML enum imports first (downstream emission can depend on the
+    # resolved enum value, e.g. fromto_decompose reads Type).
     xml_enum_attrs: Dict[str, Any] = elem_rules.get("xml_enum_attrs", {})
-    def _should_emit_xml_enum(attr: str) -> bool:
-        if attr in attrs:
-            return True
-        if apply_canonicalizations and attr in global_excl:
-            return True
-        return False
     for attr, enum_def in xml_enum_attrs.items():
-        if _should_emit_xml_enum(attr):
+        if _should_emit_xml_enum_at_this_layer(
+            attr, attrs, global_excl, apply_canonicalizations=apply_canon,
+        ):
             imports += _emit_xml_enum_import(attr, enum_def)
 
-    # 3b) Per-attribute import lines run BEFORE the canonicalisations so a
-    # canon like fromto_decompose can override individual slots (e.g.
-    # ``size[1] = halflength``) on top of the XML-provided values without
-    # being clobbered by a later per-attr read.
-    for attr in attrs:
-        if attr in global_excl or attr in elem_excl or attr in canon_absorbed:
-            continue
-        if attr in xml_enum_attr_names:
-            continue
+    # 3b) Per-attr imports.
+    vec3_convert_map: Dict[str, str] = elem_rules.get("vec3_convert", {})
+    for attr in iter_category_attrs(attrs, global_excl=global_excl,
+                                     elem_excl=elem_excl,
+                                     canon_absorbed=canon_absorbed,
+                                     extra_excl=xml_enum_attr_names):
         ue_type = type_map.get(attr, default_type)
-        prop_name = renames.get(attr, pascal_case(attr))
-        imports += _emit_import_line(attr, prop_name, ue_type)
+        prop_name = renames.get(attr, attr)
+        imports += _emit_import_line(attr, prop_name, ue_type,
+                                     vec3_convert=vec3_convert_map.get(attr))
 
-    # 3c) Canonicalisations now run AFTER per-attr imports.
+    # 3c) Canon imports run AFTER per-attr so canons (fromto) can
+    # override individual slots.
     for c in elem_canons:
         cd = canonicalizations.get(c)
         if cd:
-            imports += _canon_import_block(c, cd, element_name=element_name)
-
-    # 3d) Drive the UE component transform from the codegen-owned Pos/Quat
-    # UPROPERTYs. Every category that applies spatial_pose / orientation is
-    # USceneComponent-derived (body, frame, geom, site, camera, inertial,
-    # flexcomp) — pushing the just-read Pos/Quat onto the relative transform
-    # is what makes the SCS component visible in the right place. Pos is
-    # already in UE cm (ReadVec3InMeters applies MjToUEPosition) and Quat is
-    # already in UE frame (orientation canon applies MjToUERotation), so a
-    # straight assign is the right call. Skipped when apply_canonicalizations
-    # is False (subclass emission — base class already drove the transform).
-    if apply_canonicalizations:
+            imports += _canon_import_block(c, cd, element_name=ctx.element_name)
+
+    # 3d) Drive the UE component transform from the codegen-owned
+    # Pos/Quat UPROPERTYs.
+    if apply_canon:
         if "spatial_pose" in elem_canons:
             imports += "    if (bOverride_Pos)  SetRelativeLocation(Pos);\n"
         if "orientation" in elem_canons:
             imports += "    if (bOverride_Quat) SetRelativeRotation(Quat);\n"
 
-    # 4c) Target collations (e.g. mjsEquality.name1 absorbs body1/joint1/
-    # tendon1/flex on the XML side). UE property is hand-declared (Obj1/Obj2
-    # with dynamic GetOptions); codegen owns the first-non-empty import +
-    # mjs_setString export.
+    # 4c) Target-collation imports.
     target_collations: Dict[str, Any] = elem_rules.get("target_collations", {})
-    if apply_canonicalizations:
+    if apply_canon:
         for prop_name, coll_def in target_collations.items():
             imports += _emit_target_collation_import(
                 prop_name, coll_def.get("absorbs_attrs", [])
             )
 
-    # 4d) Unit conversions (e.g. joint.range: XML deg/m -> UPROPERTY UE
-    # deg/cm). Runs AFTER the default per-attr ReadAttr* so the converter
-    # operates on the freshly-read value. Each conversion entry is a
-    # data-driven dict; the converter ops live in _UNIT_CONVERT_OPS in the
-    # generator. Adding a new conversion is one Python dict entry — schema/
-    # rules edits stay data-only.
+    # 4d) Unit-conversion imports.
     unit_conversions: Dict[str, Any] = elem_rules.get("unit_conversion", {})
     for attr, conv_list in unit_conversions.items():
-        if attr not in attrs and not apply_canonicalizations:
+        if attr not in attrs and not apply_canon:
             continue
-        prop_name = renames.get(attr, pascal_case(attr))
+        prop_name = renames.get(attr, attr)
         ue_type = type_map.get(attr, default_type)
         is_array = ue_type.startswith("TArray")
-        imports += _emit_unit_conversion_block(prop_name, conv_list, side="import", is_array=is_array)
-
-    # 5) Canonicalisation export lines (first — so they set the canonical
-    # MJ struct fields before any per-attr tweaks). Some elements opt out of
-    # specific canon exports via ``canon_export_skip`` (e.g. body's pos/quat
-    # are written by MjSpecWrapper::CreateBody from the UE transform, not
-    # from the Pos/Quat UPROPERTYs, so the spatial_pose/orientation export
-    # for body would overwrite the transform-based values with stale data).
+        imports += _emit_unit_conversion_block(
+            prop_name, conv_list, side="import", is_array=is_array,
+        )
+
+    return imports
+
+
+def _emit_exports_block(ctx: EmissionContext) -> str:
+    """Emit the ``ExportTo`` body — every write-back-to-mjs line. Order:
+    canon exports, xml_enum exports, per-attr exports, data-packed
+    exports, target-collation exports, unit-conversion exports, double-
+    vec exports."""
+    attrs            = ctx.attrs
+    elem_rules       = ctx.elem_rules
+    type_map         = ctx.type_map
+    default_type     = ctx.default_type
+    renames          = ctx.renames
+    global_excl      = ctx.global_excl
+    elem_excl        = ctx.elem_excl
+    canon_absorbed   = ctx.canon_absorbed
+    elem_canons      = ctx.elem_canons
+    canonicalizations = ctx.canonicalizations
+    apply_canon      = ctx.apply_canonicalizations
+    xml_enum_attr_names = ctx.xml_enum_attr_names
+    mjs_fields       = ctx.mjs_fields
+    mjs_array_caps   = ctx.mjs_array_caps or {}
+    consumed_by_setto = ctx.consumed_by_setto
+    attr_to_mjs_field = ctx.attr_to_mjs_field
+
+    exports = ""
+    target_collations: Dict[str, Any] = elem_rules.get("target_collations", {})
+    unit_conversions: Dict[str, Any] = elem_rules.get("unit_conversion", {})
+    vec3_convert_map: Dict[str, str] = elem_rules.get("vec3_convert", {})
+
+    # 5) Canon exports first (set canonical mjs fields before per-attr
+    # tweaks). canon_export_skip opts an element out per-canon (e.g.
+    # body's pos/quat are written by MjSpecWrapper from the transform).
     canon_export_skip = set(elem_rules.get("canon_export_skip", []))
     for c in elem_canons:
         if c in canon_export_skip:
@@ -1065,166 +1355,174 @@ def _should_emit_xml_enum(attr: str) -> bool:
         if cd:
             exports += _canon_export_block(c, cd)
 
-    # 5b) XML enum attr exports (URLab enum -> mjt* via switch). Same emission
-    # policy as imports (see 3a): subtype-only attrs emit on the subtype that
-    # actually has the attr, globally-excluded "type"-like attrs emit on base.
+    # 5b) XML enum exports — same layer policy as imports (3a).
+    xml_enum_attrs: Dict[str, Any] = elem_rules.get("xml_enum_attrs", {})
     for attr, enum_def in xml_enum_attrs.items():
-        if _should_emit_xml_enum(attr):
+        if _should_emit_xml_enum_at_this_layer(
+            attr, attrs, global_excl, apply_canonicalizations=apply_canon,
+        ):
             exports += _emit_xml_enum_export(attr, enum_def)
 
-    # 6) Per-attribute export lines. Attrs handled by mjs_data_packed_attrs,
-    # mjs_double_vec_attrs, xml_enum_attrs, or unit_conversion are suppressed
-    # from the default per-attr emission — they each get their own
-    # specialised export block above or below.
+    # 6) Per-attr exports.
     data_packed_attrs: Dict[str, Any] = elem_rules.get("mjs_data_packed_attrs", {})
     double_vec_attrs: List[str] = elem_rules.get("mjs_double_vec_attrs", [])
     double_vec_set: set = set(double_vec_attrs)
-    # Per-attr export-side slot sentinels. Used by the fromto canon for `size`
-    # — slots set to the sentinel value are skipped so the parent default's
-    # radius / earlier-slot value is preserved.
     sentinel_skip: Dict[str, str] = elem_rules.get("sentinel_skip_export", {})
-    # Per-attr "import but don't export to mjs" list. Used when an mjsX
-    # field exists for an attr but URLab never registers the referenced
-    # asset in the spec (e.g. geom.material -> mjsGeom.material; URLab
-    # materials are UE-side assets, NOT mjsMaterial spec elements). The
-    # codegen import still populates the UPROPERTY so consumers can read
-    # the name for UE-side material lookups.
     export_skip_attrs: set = set(elem_rules.get("export_skip_attrs", []))
     unit_convert_export_attrs: set = {
         a for a, convs in unit_conversions.items()
-        if any(c.get("export_op") for c in convs)
+        if any(cv.get("export_op") for cv in convs)
     }
-    # Per-element mjs field-name overrides. Used when MuJoCo's struct field
-    # name differs enough from the schema attr name that the auto-resolver
-    # in _resolve_mjs_field can't bridge it (e.g. camera "target" ->
-    # mjsCamera.targetbody, geom "shellinertia" -> mjsGeom.typeinertia).
-    attr_to_mjs_field: Dict[str, str] = elem_rules.get("attr_to_mjs_field", {})
-    # Attrs absorbed by target_collations get a per-attr UPROPERTY on the
-    # import side (so the user can read them e.g. for the site-mode
-    # discriminator in equality), but on the export side their value
-    # already flows into the target spec via the collated Obj1/Obj2 ->
-    # name1/name2 writes. Skip per-attr export emission to avoid a
-    # misleading "(skipped: mjs has no field)" comment.
     target_collation_absorbed: set = set()
-    if apply_canonicalizations:
+    if apply_canon:
         for prop_name, coll_def in target_collations.items():
             target_collation_absorbed.update(coll_def.get("absorbs_attrs", []))
-    for attr in attrs:
-        if attr in global_excl or attr in elem_excl or attr in canon_absorbed:
-            continue
-        if attr in xml_enum_attr_names:
-            continue
-        if attr in data_packed_attrs and apply_canonicalizations:
-            continue
-        if attr in double_vec_set:
-            continue
-        if attr in unit_convert_export_attrs:
-            continue
-        if attr in export_skip_attrs:
-            continue
-        if attr in target_collation_absorbed:
-            continue
+    export_extra_excl: set = set(xml_enum_attr_names)
+    export_extra_excl |= double_vec_set
+    export_extra_excl |= unit_convert_export_attrs
+    export_extra_excl |= export_skip_attrs
+    export_extra_excl |= target_collation_absorbed
+    if apply_canon:
+        export_extra_excl |= set(data_packed_attrs.keys())
+    for attr in iter_category_attrs(attrs, global_excl=global_excl,
+                                     elem_excl=elem_excl,
+                                     canon_absorbed=canon_absorbed,
+                                     extra_excl=export_extra_excl):
         ue_type = type_map.get(attr, default_type)
-        prop_name = renames.get(attr, pascal_case(attr))
+        prop_name = renames.get(attr, attr)
+        # Cap export loops against the destination C array size so a
+        # narrower mjs field can't overrun.
+        resolved_field = _resolve_mjs_field(
+            attr, mjs_fields, attr_to_mjs_field,
+        ) if mjs_fields else (attr_to_mjs_field or {}).get(attr, attr)
+        array_cap = mjs_array_caps.get(resolved_field)
         exports += _emit_export_line(
             attr, prop_name, ue_type,
             mjs_fields=mjs_fields,
             slot_sentinel=sentinel_skip.get(attr),
             attr_to_mjs_field=attr_to_mjs_field,
             consumed_by_setto=consumed_by_setto,
+            vec3_convert=vec3_convert_map.get(attr),
+            array_cap=array_cap,
         )
 
-    # 6b) Data-packed exports (e.g. mjsEquality.data[]). Run AFTER per-attr
-    # so any earlier mjsField writes are in place when these pack values in.
-    if apply_canonicalizations:
+    # 6b) Data-packed exports.
+    if apply_canon:
         for attr, packed_def in data_packed_attrs.items():
             ue_type = type_map.get(attr, default_type)
-            prop_name = renames.get(attr, pascal_case(attr))
-            exports += _emit_data_packed_export(attr, packed_def, ue_type, prop_name)
+            prop_name = renames.get(attr, attr)
+            exports += _emit_data_packed_export(
+                attr, packed_def, ue_type, prop_name,
+            )
 
-    # 6d) Target collation exports (e.g. Obj1 -> mjs_setString(name1)).
-    if apply_canonicalizations:
+    # 6d) Target-collation exports.
+    if apply_canon:
         for prop_name, coll_def in target_collations.items():
-            exports += _emit_target_collation_export(prop_name, coll_def["mjs_field"])
+            exports += _emit_target_collation_export(
+                prop_name, coll_def["mjs_field"],
+            )
 
-    # 6e) Unit-conversion exports (replace default per-attr export with a
-    # converting write — e.g. joint.range: UE deg -> mjs rad).
+    # 6e) Unit-conversion exports.
     for attr, conv_list in unit_conversions.items():
-        if attr not in attrs and not apply_canonicalizations:
+        if attr not in attrs and not apply_canon:
             continue
-        prop_name = renames.get(attr, pascal_case(attr))
+        prop_name = renames.get(attr, attr)
         ue_type = type_map.get(attr, default_type)
         is_array = ue_type.startswith("TArray")
-        # Resolve mjs field name (e.g. attr "solreflimit" -> "solref_limit").
-        mjs_field = attr
         if mjs_fields:
             mjs_field = _resolve_mjs_field(attr, mjs_fields, attr_to_mjs_field)
-        exports += _emit_unit_conversion_export(prop_name, mjs_field, conv_list, is_array=is_array)
+        else:
+            mjs_field = (attr_to_mjs_field or {}).get(attr, attr)
+        exports += _emit_unit_conversion_export(
+            prop_name, mjs_field, conv_list, is_array=is_array,
+        )
 
-    # 6c) Double-vec exports (mjsKey.qpos/qvel/etc — mjDoubleVec* not
-    # directly assignable, requires ->clear()+->push_back()).
+    # 6c) Double-vec exports.
     for attr in double_vec_attrs:
-        prop_name = renames.get(attr, pascal_case(attr))
-        # mjs_field default = attr name; resolved via _resolve_mjs_field if mjs_fields is known.
-        mjs_field = attr
+        prop_name = renames.get(attr, attr)
         if mjs_fields:
             mjs_field = _resolve_mjs_field(attr, mjs_fields, attr_to_mjs_field)
+        else:
+            mjs_field = (attr_to_mjs_field or {}).get(attr, attr)
         exports += _emit_double_vec_export(attr, prop_name, mjs_field)
 
-    return EmittedSchema(properties_h=props, imports_cpp=imports, exports_cpp=exports)
+    return exports
 
 
-def emit_xml_passthrough_body(
+def emit_schema_for_attrs(
     attrs: Sequence[str],
     rules: Dict[str, Any],
     element_name: str,
-) -> str:
-    """Emit the body of a `BuildSchemaAttrsXml() const -> FString` method.
-
-    For categories with ``xml_passthrough_emission: true`` in their codegen
-    rule, the spec-registration path goes through an MJCF re-parse
-    (mj_parseXMLString + mjs_attach) instead of the mjs_addX API — flexcomp
-    is the canonical example because MuJoCo expands the flexcomp macro in
-    the XML parser, not the spec compiler. To keep new schema attrs
-    flowing end-to-end without hand-edits, the codegen emits a single
-    method that walks every override-tracked UPROPERTY and writes its
-    value as an ``name="value"`` XML attribute string. The hand-rolled
-    BuildFlexcompXml just calls this helper for the main attrs and
-    handles sub-elements (which the codegen doesn't model yet) itself.
+    category_label: str,
+    *,
+    apply_canonicalizations: bool = True,
+    mjs_fields: set | None = None,
+    consumed_by_setto: set | None = None,
+    mjs_array_caps: Dict[str, int] | None = None,
+) -> EmittedSchema:
+    """Emit UPROPERTY + import/export blocks for a list of schema attrs.
+
+    Applies global exclusions, element exclusions, canonicalisation
+    absorption, and type mappings from `codegen_rules.json`.
 
-    Attribute emission rules per UE type:
-      - float   -> ` X="%f"`
-      - int     -> ` X="%d"`
-      - bool    -> ` X="true"` / ` X="false"`
-      - FString -> ` X="..."` (skipped when empty)
-      - TArray<float/int/double> -> space-separated joined list
-      - FLinearColor -> ` X="r g b a"`
-    Canonicalised attrs (pos/quat) round-trip through MjUtils::UEToMjPosition
-    + ::UEToMjRotation (matching their export_helper in canonicalizations).
-    xml_enum_attrs write the XML-side string value selected by the enum.
-
-    Output is the indented C++ statements (no signature, no braces) — the
-    caller wraps them in a method body.
+    Pass ``apply_canonicalizations=False`` when emitting for a subclass
+    in a category whose base class already owns the canonicalised
+    properties (e.g. site/tendon/equality subtypes in multi_uclass
+    layout, or geom subtypes that share base canonicalisations).
+    Raw canonicalised attrs (pos/quat/euler/etc.) are still excluded
+    so they don't re-appear as per-attr UPROPERTYs.
+
+    ``consumed_by_setto`` is the set of schema attrs that will be packed
+    into the mjs spec by an ``mjs_setTo*`` call prepended to the export
+    block (see ``_emit_setto_call``). For those attrs, we suppress the
+    fallback "(skipped: mjs has no field)" comment to keep the emitted
+    file uncluttered.
     """
-    global_excl = set(rules.get("global_exclusions", []))
-    elem_rules = rules.get("element_rules", {}).get(element_name, {})
-    elem_excl = set(elem_rules.get("exclude_attrs", []))
-    elem_canons: List[str] = elem_rules.get("applies_canonicalizations", [])
-    canonicalizations = rules.get("canonicalizations", {})
-    canon_absorbed: set = set()
-    for c in elem_canons:
-        cd = canonicalizations.get(c)
-        if cd:
-            canon_absorbed.update(cd.get("absorbs_attrs", []))
-    xml_enum_attrs: Dict[str, Any] = elem_rules.get("xml_enum_attrs", {})
-    type_map: Dict[str, str] = rules.get("type_mappings", {})
-    default_type: str = rules.get("default_type", "float")
-    renames: Dict[str, str] = rules.get("property_renames", {})
+    ctx = EmissionContext.from_call(
+        attrs, rules, element_name,
+        apply_canonicalizations=apply_canonicalizations,
+        mjs_fields=mjs_fields,
+        consumed_by_setto=consumed_by_setto,
+        mjs_array_caps=mjs_array_caps,
+    )
+    elem_rules = ctx.elem_rules
+    hidden_canons: set = set(elem_rules.get("hidden_canon_properties", []))
+    hidden_attrs:  set = set(elem_rules.get("hidden_attrs", []))
+
+    props = (
+        _emit_canon_property_block(
+            ctx.elem_canons, ctx.canonicalizations,
+            category_label, hidden_canons,
+        )
+        + _emit_per_attr_property_block(
+            attrs, elem_rules, ctx.type_map, ctx.default_type, ctx.renames,
+            category_label,
+            global_excl=ctx.global_excl, elem_excl=ctx.elem_excl,
+            canon_absorbed=ctx.canon_absorbed,
+            xml_enum_attr_names=ctx.xml_enum_attr_names,
+            hidden_attrs=hidden_attrs,
+        )
+        + _emit_xml_enum_property_decls(
+            elem_rules, category_label,
+            apply_canonicalizations=apply_canonicalizations,
+        )
+    )
 
-    lines: List[str] = []
-    lines.append("    FString Out;")
+    imports = _emit_imports_block(ctx)
+    exports = _emit_exports_block(ctx)
+
+    return EmittedSchema(properties_h=props, imports_cpp=imports, exports_cpp=exports)
 
-    # Canonicalisations first (pos -> "pos=\"x y z\"", quat -> "quat=...")
+
+def _emit_passthrough_canon_lines(
+    elem_canons: List[str],
+    canonicalizations: Dict[str, Any],
+) -> List[str]:
+    """Emit ``pos="x y z"`` / ``quat="w x y z"`` formatters for the
+    canonicalisation-owned UPROPERTYs. Round-trips through the same
+    UEToMjPosition / UEToMjRotation helpers the canon export blocks
+    use, so the XML matches the spec write exactly."""
+    lines: List[str] = []
     for c in elem_canons:
         cd = canonicalizations.get(c)
         if not cd:
@@ -1249,78 +1547,135 @@ def emit_xml_passthrough_body(
             lines.append(f"        MjUtils::UEToMjRotation({prop}, Tmp);")
             lines.append('        Out += FString::Printf(TEXT(" quat=\\"%f %f %f %f\\""), Tmp[0], Tmp[1], Tmp[2], Tmp[3]);')
             lines.append("    }")
+    return lines
 
-    # xml_enum_attrs: emit the XML attribute string for the chosen enum value.
+
+def _emit_passthrough_xml_enum_lines(
+    xml_enum_attrs: Dict[str, Any],
+) -> List[str]:
+    """Emit the per-attr if/else-if chain that picks the XML string for
+    the chosen enum value."""
+    lines: List[str] = []
     for attr, enum_def in xml_enum_attrs.items():
         ue_prop = enum_def["ue_property"]
         ue_enum = enum_def["ue_enum_type"]
-        value_map = enum_def["value_map"]
+        value_map = _resolve_value_map(attr, enum_def)
         toggle = override_toggle_name(ue_prop)
         lines.append(f"    if ({toggle})")
         lines.append("    {")
         first = True
         for xml_val, mapping in value_map.items():
-            ue_member = mapping[0] if isinstance(mapping, list) else mapping
+            ue_member = mapping[0]
             prefix = "if      " if first else "else if "
             first = False
             lines.append(f'        {prefix}({ue_prop} == {ue_enum}::{ue_member}) '
                          f'Out += TEXT(" {attr}=\\"{xml_val}\\"");')
         lines.append("    }")
+    return lines
 
-    # Per-attr lines
-    for attr in attrs:
-        if attr in global_excl or attr in elem_excl or attr in canon_absorbed:
-            continue
-        if attr in xml_enum_attrs:
-            continue
-        if attr == "name":
-            # Name is written by the caller (it knows the prefix/MjName logic).
-            continue
-        prop_name = renames.get(attr, pascal_case(attr))
+
+def _emit_passthrough_attr_line(
+    attr: str, prop_name: str, ue_type: str, toggle: str,
+) -> List[str]:
+    """Emit the per-type XML-attr formatter line(s) for a single attr.
+    Returns a list of indented C++ lines (one for scalars, multiple
+    for TArray loops). Unknown types fall through with a TODO comment
+    so the build still compiles."""
+    if ue_type == "FString":
+        return [f'    if ({toggle} && !{prop_name}.IsEmpty()) '
+                f'Out += FString::Printf(TEXT(" {attr}=\\"%s\\""), *{prop_name});']
+    if ue_type == "bool":
+        return [f'    if ({toggle}) '
+                f'Out += FString::Printf(TEXT(" {attr}=\\"%s\\""), '
+                f'{prop_name} ? TEXT("true") : TEXT("false"));']
+    if ue_type == "int32":
+        return [f'    if ({toggle}) '
+                f'Out += FString::Printf(TEXT(" {attr}=\\"%d\\""), {prop_name});']
+    if ue_type == "float":
+        return [f'    if ({toggle}) '
+                f'Out += FString::Printf(TEXT(" {attr}=\\"%f\\""), {prop_name});']
+    if ue_type == "FLinearColor":
+        return [f"    if ({toggle}) "
+                f'Out += FString::Printf(TEXT(" {attr}=\\"%f %f %f %f\\""), '
+                f'{prop_name}.R, {prop_name}.G, {prop_name}.B, {prop_name}.A);']
+    if ue_type.startswith("TArray<float") or ue_type.startswith("TArray<double"):
+        return [
+            f"    if ({toggle} && {prop_name}.Num() > 0)",
+            "    {",
+            f'        Out += TEXT(" {attr}=\\"");',
+            f"        for (int32 i = 0; i < {prop_name}.Num(); ++i)",
+            "        {",
+            '            if (i > 0) Out += TEXT(" ");',
+            f'            Out += FString::Printf(TEXT("%f"), {prop_name}[i]);',
+            "        }",
+            '        Out += TEXT("\\"");',
+            "    }",
+        ]
+    if ue_type.startswith("TArray<int"):
+        return [
+            f"    if ({toggle} && {prop_name}.Num() > 0)",
+            "    {",
+            f'        Out += TEXT(" {attr}=\\"");',
+            f"        for (int32 i = 0; i < {prop_name}.Num(); ++i)",
+            "        {",
+            '            if (i > 0) Out += TEXT(" ");',
+            f'            Out += FString::Printf(TEXT("%d"), {prop_name}[i]);',
+            "        }",
+            '        Out += TEXT("\\"");',
+            "    }",
+        ]
+    return [f'    // TODO(xml_passthrough): unsupported UE type "{ue_type}" for attr "{attr}"']
+
+
+def emit_xml_passthrough_body(
+    attrs: Sequence[str],
+    rules: Dict[str, Any],
+    element_name: str,
+) -> str:
+    """Emit the body of a `BuildSchemaAttrsXml() const -> FString` method.
+
+    For categories with ``xml_passthrough_emission: true`` in their codegen
+    rule, the spec-registration path goes through an MJCF re-parse
+    (mj_parseXMLString + mjs_attach) instead of the mjs_addX API — flexcomp
+    is the canonical example because MuJoCo expands the flexcomp macro in
+    the XML parser, not the spec compiler. To keep new schema attrs
+    flowing end-to-end without hand-edits, the codegen emits a single
+    method that walks every override-tracked UPROPERTY and writes its
+    value as an ``name="value"`` XML attribute string. The hand-rolled
+    BuildFlexcompXml just calls this helper for the main attrs and
+    handles sub-elements (which the codegen doesn't model yet) itself.
+
+    Output is the indented C++ statements (no signature, no braces) —
+    the caller wraps them in a method body. Per-type emission lives in
+    ``_emit_passthrough_attr_line``; canon + xml_enum live in their own
+    helpers.
+    """
+    global_excl = set(rules.get("global_exclusions", []))
+    elem_rules = rules.get("element_rules", {}).get(element_name, {})
+    elem_excl = set(elem_rules.get("exclude_attrs", []))
+    elem_canons: List[str] = elem_rules.get("applies_canonicalizations", [])
+    canonicalizations = rules.get("canonicalizations", {})
+    canon_absorbed: set = compute_canon_absorbed(elem_canons, canonicalizations)
+    xml_enum_attrs: Dict[str, Any] = elem_rules.get("xml_enum_attrs", {})
+    type_map: Dict[str, str] = rules.get("type_mappings", {})
+    default_type: str = rules.get("default_type", "float")
+    renames: Dict[str, str] = rules.get("property_renames", {})
+
+    lines: List[str] = ["    FString Out;"]
+    lines.extend(_emit_passthrough_canon_lines(elem_canons, canonicalizations))
+    lines.extend(_emit_passthrough_xml_enum_lines(xml_enum_attrs))
+
+    # ``name`` is written by the caller (it knows the prefix / MjName
+    # logic).
+    passthrough_extra_excl = set(xml_enum_attrs) | {"name"}
+    for attr in iter_category_attrs(attrs, global_excl=global_excl,
+                                     elem_excl=elem_excl,
+                                     canon_absorbed=canon_absorbed,
+                                     extra_excl=passthrough_extra_excl):
+        prop_name = renames.get(attr, attr)
         ue_type = type_map.get(attr, default_type)
         toggle = override_toggle_name(prop_name)
-
-        if ue_type == "FString":
-            lines.append(f'    if ({toggle} && !{prop_name}.IsEmpty()) '
-                         f'Out += FString::Printf(TEXT(" {attr}=\\"%s\\""), *{prop_name});')
-        elif ue_type == "bool":
-            lines.append(f'    if ({toggle}) '
-                         f'Out += FString::Printf(TEXT(" {attr}=\\"%s\\""), '
-                         f'{prop_name} ? TEXT("true") : TEXT("false"));')
-        elif ue_type == "int32":
-            lines.append(f'    if ({toggle}) '
-                         f'Out += FString::Printf(TEXT(" {attr}=\\"%d\\""), {prop_name});')
-        elif ue_type == "float":
-            lines.append(f'    if ({toggle}) '
-                         f'Out += FString::Printf(TEXT(" {attr}=\\"%f\\""), {prop_name});')
-        elif ue_type == "FLinearColor":
-            lines.append(f"    if ({toggle}) "
-                         f'Out += FString::Printf(TEXT(" {attr}=\\"%f %f %f %f\\""), '
-                         f'{prop_name}.R, {prop_name}.G, {prop_name}.B, {prop_name}.A);')
-        elif ue_type.startswith("TArray<float") or ue_type.startswith("TArray<double"):
-            lines.append(f"    if ({toggle} && {prop_name}.Num() > 0)")
-            lines.append("    {")
-            lines.append(f'        Out += TEXT(" {attr}=\\"");')
-            lines.append(f"        for (int32 i = 0; i < {prop_name}.Num(); ++i)")
-            lines.append("        {")
-            lines.append('            if (i > 0) Out += TEXT(" ");')
-            lines.append(f'            Out += FString::Printf(TEXT("%f"), {prop_name}[i]);')
-            lines.append("        }")
-            lines.append('        Out += TEXT("\\"");')
-            lines.append("    }")
-        elif ue_type.startswith("TArray<int"):
-            lines.append(f"    if ({toggle} && {prop_name}.Num() > 0)")
-            lines.append("    {")
-            lines.append(f'        Out += TEXT(" {attr}=\\"");')
-            lines.append(f"        for (int32 i = 0; i < {prop_name}.Num(); ++i)")
-            lines.append("        {")
-            lines.append('            if (i > 0) Out += TEXT(" ");')
-            lines.append(f'            Out += FString::Printf(TEXT("%d"), {prop_name}[i]);')
-            lines.append("        }")
-            lines.append('        Out += TEXT("\\"");')
-            lines.append("    }")
-        else:
-            lines.append(f'    // TODO(xml_passthrough): unsupported UE type "{ue_type}" for attr "{attr}"')
+        lines.extend(_emit_passthrough_attr_line(attr, prop_name, ue_type, toggle))
 
     lines.append("    return Out;")
     return "\n".join(lines) + "\n"
@@ -1398,31 +1753,11 @@ class URLAB_API {ClassName} : public {BaseClassName}
 # Boundary-tag injection
 # ---------------------------------------------------------------------------
 
-def _make_tag_pair(tag: str) -> Tuple[str, str]:
-    return (f"// --- CODEGEN_{tag}_START ---", f"// --- CODEGEN_{tag}_END ---")
-
-
-def inject_between_tags(file_text: str, tag: str, new_inner: str) -> Tuple[str, bool]:
-    """Replace the content between ``// --- CODEGEN_<tag>_START ---`` and
-    ``// --- CODEGEN_<tag>_END ---`` with ``new_inner``. Returns
-    (new_text, was_found). If the tags are missing, returns the input
-    unchanged with was_found=False — callers decide whether that's
-    fatal."""
-    start, end = _make_tag_pair(tag)
-    pattern = re.compile(
-        r"(" + re.escape(start) + r")(.*?)(" + re.escape(end) + r")",
-        re.DOTALL,
-    )
-    matched = [False]
-
-    def repl(m: re.Match) -> str:
-        matched[0] = True
-        # Preserve the start/end lines with a newline before the content.
-        body = "\n" + new_inner.rstrip() + "\n    "
-        return m.group(1) + body + m.group(3)
-
-    new_text = pattern.sub(repl, file_text)
-    return new_text, matched[0]
+# Text-injection machinery lives in `_codegen_inject`. The symbols
+# (`_make_tag_pair`, `_strip_cpp_braces_in_strings`,
+# `_find_matching_brace`, `_check_brace_balance`,
+# `inject_between_tags`, `_inject_tags_into_cpp`) are re-exported
+# through the import block at the top of this file.
 
 
 # ---------------------------------------------------------------------------
@@ -1446,35 +1781,28 @@ def codegen_owned_property_names(category: str, attrs: List[str], rules: Dict[st
     elem_excl = set(elem_rules.get("exclude_attrs", []))
     elem_canons: List[str] = elem_rules.get("applies_canonicalizations", [])
     canonicalizations: Dict[str, Any] = rules.get("canonicalizations", {})
-    canon_absorbed: set = set()
+    canon_absorbed: set = compute_canon_absorbed(elem_canons, canonicalizations)
     owned: set = set()
     for c in elem_canons:
         cdef = canonicalizations.get(c)
         if cdef:
-            canon_absorbed.update(cdef.get("absorbs_attrs", []))
             prop = cdef.get("emits_property")
             if prop:
                 owned.add(prop["name"])
-    for a in attrs:
-        if a in global_excl or a in elem_excl or a in canon_absorbed:
-            continue
-        owned.add(renames.get(a, pascal_case(a)))
+    for a in iter_category_attrs(attrs, global_excl=global_excl,
+                                  elem_excl=elem_excl,
+                                  canon_absorbed=canon_absorbed):
+        owned.add(renames.get(a, a))
     return owned
 
 
-def all_owned_decl_names(props: set) -> set:
-    """Codegen-owned property names plus their ``bOverride_X`` toggles."""
-    return props | {f"bOverride_{p}" for p in props}
 
 
 # ---------------------------------------------------------------------------
 # Pipeline: emit subclass files
 # ---------------------------------------------------------------------------
 
-@dataclass
-class FileWrite:
-    path: str
-    content: str
+# `FileWrite` lives in `_codegen_core`.
 
 
 def _resolve_include_path(public_root: str, file_relpath: str) -> str:
@@ -1486,6 +1814,91 @@ def _resolve_include_path(public_root: str, file_relpath: str) -> str:
     return rel.replace(os.sep, "/")
 
 
+_BANNER_TEMPLATE_INCLUDES = {
+    "MuJoCo/Utils/MjXmlUtils.h",
+    "MuJoCo/Utils/MjOrientationUtils.h",
+    "MuJoCo/Utils/MjUtils.h",
+    "XmlNode.h",
+    "mujoco/mjspec.h",
+}
+
+
+def _audit_banner_safety(source_path: str, subtype: Dict[str, Any]) -> None:
+    """Scan an existing source file the codegen is ABOUT to overwrite via
+    ``fully_emitted: true`` and emit a diagnostic if it carries content
+    the template wouldn't reproduce (extra includes beyond the template
+    set, extra method definitions, ctor extras not in ``extra_constructor``).
+
+    Non-fatal — the rewrite still proceeds. The diagnostic exists so a
+    rule author who flips ``fully_emitted: true`` without running the
+    audit helper sees a loud warning the next time the codegen runs.
+    """
+    try:
+        with open(source_path, "r", encoding="utf-8") as f:
+            text = f.read()
+    except FileNotFoundError:
+        return
+    except OSError as e:
+        _diag_add(
+            f"[diagnostic] banner_overwrite: could not read '{source_path}' "
+            f"({e}) — skipping the pre-overwrite audit. The rewrite still "
+            f"proceeds, but any hand-edits in the existing file may be lost "
+            f"unnoticed.",
+            source="banner_overwrite",
+        )
+        return
+
+    rel = os.path.basename(source_path)
+    own_h = os.path.splitext(rel)[0] + ".h"
+
+    # Extra includes beyond the template set.
+    extras = []
+    for inc in re.findall(r'#include\s+"([^"]+)"', text):
+        if inc.endswith(own_h) or inc in _BANNER_TEMPLATE_INCLUDES:
+            continue
+        extras.append(inc)
+    if extras:
+        _diag_add(
+            f"[diagnostic] fully_emitted: true on '{subtype.get('class_name', rel)}' "
+            f"is about to OVERWRITE {rel} which has non-template includes "
+            f"{extras}. Run Scripts/codegen/_audit_banner_candidates.py "
+            f"before flipping, or revert fully_emitted on this subtype.",
+            source="banner_overwrite",
+        )
+
+    # Extra ctor lines beyond what the rule provides via extra_constructor.
+    # The ctor body can contain its own brace-bearing statements (range-for
+    # loops, init lists, nested ifs); use the brace-aware extractor so the
+    # diagnostic doesn't truncate at the first inner ``}`` and report a
+    # spurious "non-template ctor content".
+    expected = {subtype.get("extra_constructor", "").strip()}
+    expected.discard("")
+    class_name = subtype.get("class_name", "")
+    if class_name:
+        sig = re.search(
+            rf'{re.escape(class_name)}::{re.escape(class_name)}\(\)\s*\{{',
+            text,
+        )
+        if sig:
+            open_idx = sig.end() - 1
+            close_idx = _find_matching_brace(text, open_idx)
+            if close_idx is not None:
+                body = text[open_idx + 1:close_idx]
+                stripped = re.sub(r"(Type\s*=\s*EMj\w+::\w+;|//[^\n]*|\s+)", "", body)
+                for e in expected:
+                    # The expected extras may themselves contain whitespace —
+                    # strip to compare.
+                    stripped = stripped.replace(re.sub(r"\s+", "", e), "")
+                if stripped:
+                    _diag_add(
+                        f"[diagnostic] fully_emitted: true on "
+                        f"'{class_name}' is about to OVERWRITE the ctor in "
+                        f"{rel} — non-template ctor content remains after "
+                        f"stripping Type+extra_constructor: {stripped!r}.",
+                        source="banner_overwrite",
+                    )
+
+
 def _subclass_constructor_body(
     category: str,
     subtype: Dict[str, Any],
@@ -1558,8 +1971,9 @@ def _emit_setto_call(
         return ""
     if not mjspec or fn_name not in mjspec.get("setto_functions", {}):
         raise RuntimeError(
-            f"mjs_setTo* function '{fn_name}' not found in mjspec snapshot. "
-            f"Rebuild Scripts/mjspec_snapshot.json."
+            f"mjs_setTo* function '{fn_name}' not found in the projected "
+            f"mjspec. Rebuild the introspect snapshot via "
+            f"`python Scripts/codegen/build_introspect_snapshot.py`."
         )
     sig = mjspec["setto_functions"][fn_name]
     if not sig["params"]:
@@ -1684,6 +2098,32 @@ def _mjs_fields_for(cat_rules: Dict[str, Any], mjspec: Dict[str, Any] | None) ->
     return set(mjspec.get("structs", {}).get(struct_name, []))
 
 
+def _mjs_array_caps_for(
+    cat_rules: Dict[str, Any],
+    mjspec: Dict[str, Any] | None,
+) -> Dict[str, int]:
+    """Pull {field_name: c_array_dim} for a category's mjs_struct from the
+    introspect snapshot, scoped to fixed-size C arrays only (skips scalars,
+    dynamic vectors, pointers). The TArray export loop uses these caps to
+    emit ``FMath::Min(prop.Num(), cap)`` so a future MuJoCo array-shrink
+    (e.g. ``gainprm[10]`` → ``gainprm[6]``) doesn't overrun memory.
+    """
+    if not mjspec:
+        return {}
+    struct_name = cat_rules.get("mjs_struct")
+    if not struct_name:
+        return {}
+    introspect = mjspec.get("introspect") or {}
+    struct_table = introspect.get("structs", {})
+    fields = struct_table.get(struct_name, {}).get("fields", [])
+    caps: Dict[str, int] = {}
+    for f in fields:
+        dim = f.get("array_dim")
+        if isinstance(dim, int) and dim > 1:
+            caps[f["name"]] = dim
+    return caps
+
+
 def emit_subclass_files(
     category: str,
     cat_rules: Dict[str, Any],
@@ -1705,6 +2145,7 @@ def emit_subclass_files(
     base_header_relpath = cat_rules["base_class_header"]
     mjs_struct = cat_rules["mjs_struct"]
     mjs_fields = _mjs_fields_for(cat_rules, mjspec)
+    mjs_array_caps = _mjs_array_caps_for(cat_rules, mjspec)
     type_enum = cat_rules.get("type_enum_name")
     pub_dir = os.path.join(public_root, cat_rules.get("subclass_directory_pub", ""))
     priv_dir = os.path.join(private_root, cat_rules.get("subclass_directory_priv", ""))
@@ -1726,12 +2167,12 @@ def emit_subclass_files(
 
         emitted = emit_schema_for_attrs(
             attrs, rules, element_name=category,
-            category_label=base_class.removeprefix("U"),
+            category_label=base_class.removeprefix("UMj"),
             apply_canonicalizations=False,  # base owns the canon block
             mjs_fields=mjs_fields,
             consumed_by_setto=setto_consumed,
+            mjs_array_caps=mjs_array_caps,
         )
-        owned = codegen_owned_property_names(category, attrs, rules)
 
         # Per-subtype ``mjs_setTo*`` preset call. Codegen owns this snippet — it
         # lands at the TOP of the CODEGEN_EXPORT block so every regeneration
@@ -1760,21 +2201,69 @@ def emit_subclass_files(
         header_path = os.path.join(pub_dir, header_filename)
         source_path = os.path.join(priv_dir, class_file_stem + ".cpp")
 
-        if os.path.exists(header_path):
+        # ``fully_emitted: true`` on a subtype rule means codegen owns the
+        # entire file (header + source). Existing files get overwritten
+        # from the template every regen — equivalent to "delete and let
+        # the codegen recreate", but stable across runs. Use this for
+        # pure-stub subclasses (no extra includes, no extra methods, ctor
+        # only sets Type). The ``Scripts/codegen/_audit_banner_candidates.py``
+        # helper identifies the safe subset.
+        fully_emitted = bool(subtype.get("fully_emitted"))
+
+        if os.path.exists(header_path) and not fully_emitted:
             # Existing subclass file — inject between pre-prepped tags.
+            # Diagnose missing markers loudly so a typo in the marker pair
+            # (or a forgotten pre-prep step on a hand-rolled subclass)
+            # surfaces here rather than as wrong runtime behaviour.
             with open(header_path, "r", encoding="utf-8") as f:
                 h_text = f.read()
-            h_text, _ = inject_between_tags(h_text, "PROPERTIES", emitted.properties_h)
+            h_text, _ = _inject_or_diag(
+                h_text, "PROPERTIES", emitted.properties_h,
+                host_path=header_path, host_kind="subclass header",
+                block_label="per-subtype UPROPERTY block",
+                diag_source="subclass_inject",
+            )
             writes.append(FileWrite(path=header_path, content=h_text))
 
             if os.path.exists(source_path):
                 with open(source_path, "r", encoding="utf-8") as f:
                     c_text = f.read()
-                c_text, _ = inject_between_tags(c_text, "IMPORT", emitted.imports_cpp)
-                c_text, _ = inject_between_tags(c_text, "EXPORT", emitted.exports_cpp)
+                c_text, _ = _inject_or_diag(
+                    c_text, "IMPORT", emitted.imports_cpp,
+                    host_path=source_path, host_kind="subclass source",
+                    block_label="per-subtype import block",
+                    diag_source="subclass_inject",
+                )
+                c_text, _ = _inject_or_diag(
+                    c_text, "EXPORT", emitted.exports_cpp,
+                    host_path=source_path, host_kind="subclass source",
+                    block_label="per-subtype export block",
+                    diag_source="subclass_inject",
+                )
                 writes.append(FileWrite(path=source_path, content=c_text))
+            else:
+                _diag_add(
+                    f"[diagnostic] subclass source '{source_path}' does not "
+                    f"exist alongside its header; import + export blocks "
+                    f"could not be injected.",
+                    source="subclass_inject",
+                )
         else:
-            # Brand-new subclass file — emit from template.
+            # Brand-new file OR ``fully_emitted: true`` → write the whole
+            # file from the template.
+            #
+            # Safety check on the ``fully_emitted`` overwrite path: if the
+            # existing file carries non-template content (extra includes
+            # beyond the template, hand-written methods, ctor extras not
+            # covered by ``extra_constructor``), the rewrite would silently
+            # destroy it. The audit-helper script
+            # (Scripts/codegen/_audit_banner_candidates.py) is the
+            # expected pre-flip gate; this is a defence-in-depth diagnostic
+            # for cases where someone flips ``fully_emitted: true`` without
+            # running the helper first.
+            if fully_emitted and os.path.exists(source_path):
+                _audit_banner_safety(source_path, subtype)
+
             include_path = _resolve_include_path(public_root, header_path)
             ctor_body = _subclass_constructor_body(category, subtype, type_enum)
 
@@ -1825,24 +2314,17 @@ def emit_base_class_injection(
     base_class = cat_rules["base_class_name"]
     base_header_relpath = cat_rules["base_class_header"]
     mjs_fields = _mjs_fields_for(cat_rules, mjspec)
-    # Categories that opt out of schema-driven UPROPERTY/import/export
-    # emission entirely. Used for hand-rolled components that aren't yet
-    # ready for full codegen ownership (e.g. UMjFlexcomp). The snapshot
-    # still includes the element's schema attrs so the rest of the
-    # pipeline (e.g. completeness tests) can see them, but no C++ is
-    # emitted from this rule.
-    if cat_rules.get("disable_schema_emission"):
-        return writes
+    mjs_array_caps = _mjs_array_caps_for(cat_rules, mjspec)
     common_attrs = schema_attrs(schema, cat_rules["schema_common_block"])
     # Extras: attrs that URLab puts on the base class even though MJCF
     # schema lists them per-subtype (e.g. gainprm/biasprm/dynprm).
     common_attrs = list(common_attrs) + cat_rules.get("schema_common_extra_attrs", [])
 
     emitted = emit_schema_for_attrs(
-        common_attrs, rules, element_name=category, category_label=base_class.removeprefix("U"),
+        common_attrs, rules, element_name=category, category_label=base_class.removeprefix("UMj"),
         mjs_fields=mjs_fields,
+        mjs_array_caps=mjs_array_caps,
     )
-    owned = codegen_owned_property_names(category, common_attrs, rules)
 
     header_path = os.path.join(public_root, base_header_relpath)
     rel = os.path.relpath(header_path, public_root)
@@ -1851,21 +2333,68 @@ def emit_base_class_injection(
     if os.path.exists(header_path):
         with open(header_path, "r", encoding="utf-8") as f:
             h_text = f.read()
-        new_h, ok = inject_between_tags(h_text, "PROPERTIES", emitted.properties_h)
+        new_h, ok = _inject_or_diag(
+            h_text, "PROPERTIES", emitted.properties_h,
+            host_path=header_path, host_kind="base header",
+            block_label=f"category '{category}' UPROPERTY block",
+            diag_source="base_inject",
+        )
         if ok:
             writes.append(FileWrite(path=header_path, content=new_h))
+    else:
+        _diag_add(
+            f"[diagnostic] category '{category}' has a rule but base header "
+            f"'{header_path}' does not exist; codegen emitted no writes for "
+            f"this category.",
+            source="base_inject",
+        )
 
     if os.path.exists(source_path):
         with open(source_path, "r", encoding="utf-8") as f:
             c_text = f.read()
-        c_text, ok_i = inject_between_tags(c_text, "IMPORT", emitted.imports_cpp)
-        c_text, ok_e = inject_between_tags(c_text, "EXPORT", emitted.exports_cpp)
+        c_text, ok_i = _inject_or_diag(
+            c_text, "IMPORT", emitted.imports_cpp,
+            host_path=source_path, host_kind="base source",
+            block_label=f"category '{category}' import block",
+            diag_source="base_inject",
+        )
+        # ``xml_passthrough_emission`` routes per-attr export through the
+        # XML_PASSTHROUGH block, NOT the EXPORT block — the mjs-side
+        # ``Element->field = ...`` writes don't apply (flexcomp).
+        # ``disable_mjs_export_emission`` covers categories that have no
+        # corresponding mjs struct so the export block can't compile even
+        # if injected (inertial — writes through ParentBody->ipos/iquat
+        # in a hand-rolled RegisterToSpec). Both flags suppress EXPORT
+        # injection AND its missing-marker diagnostic.
+        skip_export = (
+            cat_rules.get("xml_passthrough_emission")
+            or cat_rules.get("disable_mjs_export_emission")
+        )
+        ok_e = False
+        if not skip_export:
+            c_text, ok_e = _inject_or_diag(
+                c_text, "EXPORT", emitted.exports_cpp,
+                host_path=source_path, host_kind="base source",
+                block_label=f"category '{category}' export block",
+                diag_source="base_inject",
+            )
         ok_x = False
         if cat_rules.get("xml_passthrough_emission"):
             xml_body = emit_xml_passthrough_body(common_attrs, rules, element_name=category)
-            c_text, ok_x = inject_between_tags(c_text, "XML_PASSTHROUGH", xml_body)
+            c_text, ok_x = _inject_or_diag(
+                c_text, "XML_PASSTHROUGH", xml_body,
+                host_path=source_path, host_kind="base source",
+                block_label=f"category '{category}' xml_passthrough block",
+                diag_source="base_inject",
+            )
         if ok_i or ok_e or ok_x:
             writes.append(FileWrite(path=source_path, content=c_text))
+    else:
+        _diag_add(
+            f"[diagnostic] category '{category}' base source '{source_path}' "
+            f"does not exist; import + export blocks could not be injected.",
+            source="base_inject",
+        )
 
     return writes
 
@@ -1914,6 +2443,7 @@ def emit_multi_uclass(
 
     base_class = cat_rules["base_class_name"]
     mjs_fields = _mjs_fields_for(cat_rules, mjspec)
+    mjs_array_caps = _mjs_array_caps_for(cat_rules, mjspec)
     common_attrs = set(schema_attrs(schema, cat_rules["schema_common_block"]))
     common_attrs |= set(cat_rules.get("schema_common_extra_attrs", []))
 
@@ -1923,15 +2453,36 @@ def emit_multi_uclass(
         attrs = [a for a in per_type_attrs if a not in common_attrs]
         emitted = emit_schema_for_attrs(
             attrs, rules, element_name=category,
-            category_label=base_class.removeprefix("U"),
+            category_label=base_class.removeprefix("UMj"),
             apply_canonicalizations=False,  # base owns the canon block
             mjs_fields=mjs_fields,
+            mjs_array_caps=mjs_array_caps,
         )
         type_tag = subtype["enum_value"]
-        h_text, _ = inject_between_tags(h_text, f"SUBCLASS_{type_tag}_PROPERTIES", emitted.properties_h)
+        # Diagnose missing per-subtype marker pairs ONLY when there's a
+        # non-empty body to inject. Subtypes whose schema attrs are entirely
+        # in the common set (or whose mjs export is handled elsewhere) emit
+        # nothing per-subtype; not pre-prepping a marker pair for those is
+        # legitimate, so suppress the diagnostic.
+        h_text, _ = _inject_or_diag(
+            h_text, f"SUBCLASS_{type_tag}_PROPERTIES", emitted.properties_h,
+            host_path=header_path, host_kind="multi_uclass base header",
+            block_label=f"subtype '{sub_key}' UPROPERTY block",
+            diag_source="multi_uclass_inject",
+        )
         if c_text:
-            c_text, _ = inject_between_tags(c_text, f"SUBCLASS_{type_tag}_IMPORT", emitted.imports_cpp)
-            c_text, _ = inject_between_tags(c_text, f"SUBCLASS_{type_tag}_EXPORT", emitted.exports_cpp)
+            c_text, _ = _inject_or_diag(
+                c_text, f"SUBCLASS_{type_tag}_IMPORT", emitted.imports_cpp,
+                host_path=source_path, host_kind="multi_uclass base source",
+                block_label=f"subtype '{sub_key}' import block",
+                diag_source="multi_uclass_inject",
+            )
+            c_text, _ = _inject_or_diag(
+                c_text, f"SUBCLASS_{type_tag}_EXPORT", emitted.exports_cpp,
+                host_path=source_path, host_kind="multi_uclass base source",
+                block_label=f"subtype '{sub_key}' export block",
+                diag_source="multi_uclass_inject",
+            )
 
     # Rewrite the merged writes (drop stale base writes; replace with merged).
     writes = [w for w in writes if w.path != header_path and w.path != source_path]
@@ -1951,12 +2502,32 @@ def _view_field_type_cpp(c_type: str) -> str:
 
 
 _MJ_M_MACRO_RE = re.compile(r"MJ_M\(\s*(\w+)\s*\)")
+# Any MJ_*( ... ) macro that survives _expand_stride is an unhandled
+# stride shape — emit invalid C++ in MjBind.h. The guard fires a
+# diagnostic so a MuJoCo bump that introduces new stride macros (e.g.
+# MJ_D(...) for mjData, MJ_V(...) for variable-size views) surfaces
+# loudly rather than landing as a compile error far downstream.
+_RESIDUAL_MJ_MACRO_RE = re.compile(r"\bMJ_[A-Z]\(\s*\w+\s*\)")
 
 
 def _expand_stride(stride: str) -> str:
     """Convert mjxmacro stride expressions to plain C++ expressions usable
-    in MjBind.h. ``MJ_M(nuser_jnt)`` -> ``m->nuser_jnt``."""
-    return _MJ_M_MACRO_RE.sub(r"m->\1", stride)
+    in MjBind.h. ``MJ_M(nuser_jnt)`` -> ``m->nuser_jnt``.
+
+    Fires a diagnostic if any ``MJ_*(...)`` macro survives the
+    substitution — that's an unhandled stride shape we'd otherwise
+    silently emit into MjBind.h verbatim."""
+    expanded = _MJ_M_MACRO_RE.sub(r"m->\1", stride)
+    residual = _RESIDUAL_MJ_MACRO_RE.findall(expanded)
+    if residual:
+        _diag_add(
+            f"[diagnostic] _expand_stride saw stride '{stride}' with "
+            f"unhandled MuJoCo macro(s) {residual}. Result '{expanded}' "
+            f"will land in MjBind.h verbatim and likely fail to compile. "
+            f"Teach _expand_stride about the new macro shape.",
+            source="expand_stride",
+        )
+    return expanded
 
 
 def emit_view_structs(rules: Dict[str, Any], mjxmacro: Dict[str, Any]) -> Dict[str, Tuple[str, str]]:
@@ -1993,27 +2564,58 @@ def emit_view_structs(rules: Dict[str, Any], mjxmacro: Dict[str, Any]) -> Dict[s
             bind_lines.append(f"    const int {var} = {expr};")
 
         # mjModel pointer fields.
+        #
+        # Stride-1 ``int`` entries (e.g. ``geom_type``, ``body_mocapid``,
+        # ``actuator_trntype``) are emitted as DEREFERENCED scalars rather
+        # than pointers — there's exactly one int per element, and the
+        # consumer always wants the value, not the pointer. Matches the
+        # ergonomic shape the hand-written MjBind.h had before activation.
+        # Mat-vec entries (``geom_size`` stride 3, ``jnt_solref`` stride
+        # mjNREF, etc.) stay as pointers since consumers index into them.
         for block in include_blocks:
             entries = mjxmacro["mjmodel_pointers"].get(block, [])
             index_var = block_index_var.get(block, "id")
             for entry in entries:
                 name = entry["name"]
-                cpp_type = _view_field_type_cpp(entry["type"])
                 view_field = renames.get(name, name)
                 stride = _expand_stride(entry["stride"])
-                fields_lines.append(f"    {cpp_type} {view_field};")
-                bind_lines.append(
-                    f"    {view_field} = m->{name} + {index_var} * {stride};"
-                )
+                is_scalar_int = (entry["type"] == "int" and entry["stride"] == "1")
+                if is_scalar_int:
+                    fields_lines.append(f"    int {view_field};")
+                    bind_lines.append(
+                        f"    {view_field} = m->{name}[{index_var}];"
+                    )
+                else:
+                    cpp_type = _view_field_type_cpp(entry["type"])
+                    fields_lines.append(f"    {cpp_type} {view_field};")
+                    bind_lines.append(
+                        f"    {view_field} = m->{name} + {index_var} * {stride};"
+                    )
 
         # mjData fields (explicitly listed per view).
+        #
+        # Each spec dict has ``index_var`` + ``stride``. Some fields don't
+        # fit the simple ``d->src + index_var * stride`` formula — e.g.
+        # SensorView::sensordata, where the right slice depends on a
+        # bounds-checked sensor_adr lookup. For those, set ``bind_override``
+        # in the rule to a C++ statement that assigns the field; the field
+        # decl is still emitted at the standard cpp_type. This kills the
+        # fragile "codegen emits wrong code, hand-written override after
+        # the marker" pattern.
         for data_name, spec in data_fields.items():
+            # ``_note_*`` keys document the data_fields entries; skip them.
+            if data_name.startswith("_note_"):
+                continue
             cpp_type = "mjtNum*"
             view_field = renames.get(data_name, data_name)
-            index_var = spec.get("index_var", "id")
-            stride = _expand_stride(spec.get("stride", "1"))
             fields_lines.append(f"    {cpp_type} {view_field};")
-            bind_lines.append(f"    {view_field} = d->{data_name} + {index_var} * {stride};")
+            bind_override = spec.get("bind_override")
+            if bind_override:
+                bind_lines.append(f"    {bind_override}")
+            else:
+                index_var = spec.get("index_var", "id")
+                stride = _expand_stride(spec.get("stride", "1"))
+                bind_lines.append(f"    {view_field} = d->{data_name} + {index_var} * {stride};")
 
         fields_block = "\n".join(fields_lines) + ("\n" if fields_lines else "")
         bind_block = "\n".join(bind_lines) + ("\n" if bind_lines else "")
@@ -2037,8 +2639,18 @@ def emit_bind_h_injection(
     views = emit_view_structs(rules, mjxmacro)
     new_text = text
     for view_name, (fields_block, bind_block) in views.items():
-        new_text, _ = inject_between_tags(new_text, f"VIEW_{view_name}_FIELDS", fields_block)
-        new_text, _ = inject_between_tags(new_text, f"VIEW_{view_name}_BIND", bind_block)
+        new_text, _ = _inject_or_diag(
+            new_text, f"VIEW_{view_name}_FIELDS", fields_block,
+            host_path=bind_h_path, host_kind="MjBind.h",
+            block_label=f"view struct '{view_name}' fields block (struct will ship empty)",
+            diag_source="bind_inject",
+        )
+        new_text, _ = _inject_or_diag(
+            new_text, f"VIEW_{view_name}_BIND", bind_block,
+            host_path=bind_h_path, host_kind="MjBind.h",
+            block_label=f"view '{view_name}' Bind() body",
+            diag_source="bind_inject",
+        )
 
     with open(bind_h_path, "r", encoding="utf-8") as f:
         original = f.read()
@@ -2048,12 +2660,55 @@ def emit_bind_h_injection(
 
 
 # ---------------------------------------------------------------------------
-# Pipeline: schema-for-tests header (T3)
+# Pipeline: schema-for-tests header
 # ---------------------------------------------------------------------------
 
+def kept_props_for_category(
+    cat: str, cat_rules: Dict[str, Any],
+    schema: Dict[str, Any], rules: Dict[str, Any],
+) -> List[str]:
+    """Return the ordered prop names the codegen will emit for one
+    category. Order matches the canonical emission order: canon-
+    owned properties first, then per-attr in schema order. xml_enum
+    attrs lower to their ``ue_property`` because that's the hand-
+    declared UPROPERTY the completeness test resolves against."""
+    elem_rules = rules.get("element_rules", {}).get(cat, {})
+    elem_excl = set(elem_rules.get("exclude_attrs", []))
+    elem_canons: List[str] = elem_rules.get("applies_canonicalizations", [])
+    canonicalizations = rules.get("canonicalizations", {})
+    canon_absorbed: set = compute_canon_absorbed(elem_canons, canonicalizations)
+    global_excl = set(rules.get("global_exclusions", []))
+    renames: Dict[str, str] = rules.get("property_renames", {})
+    xml_enum_attrs = elem_rules.get("xml_enum_attrs", {})
+
+    attrs = list(schema_attrs(schema, cat_rules["schema_common_block"]))
+    # ``schema_common_extra_attrs`` covers UPROPERTYs schema attrs miss
+    # (frame's pos/quat/childclass come via this path because frame.attrs
+    # is empty in the snapshot).
+    for a in cat_rules.get("schema_common_extra_attrs", []):
+        if a not in attrs:
+            attrs.append(a)
+
+    kept: List[str] = []
+    for c in elem_canons:
+        cd = canonicalizations.get(c)
+        if cd and cd.get("emits_property"):
+            kept.append(cd["emits_property"]["name"])
+    for a in iter_category_attrs(attrs, global_excl=global_excl,
+                                   elem_excl=elem_excl,
+                                   canon_absorbed=canon_absorbed):
+        if a in xml_enum_attrs:
+            kept.append(xml_enum_attrs[a]["ue_property"])
+            continue
+        kept.append(renames.get(a, a))
+    return kept
+
+
 def emit_schema_for_tests(schema: Dict[str, Any], rules: Dict[str, Any]) -> FileWrite:
     """Generate a C++ header that exposes schema attribute lists per
-    category to URLab automation tests."""
+    category to URLab automation tests. Every category emits a
+    ``CatProps()`` accessor returning a static ``TArray<FString>`` of
+    the codegen-owned UPROPERTY names."""
     lines: List[str] = [
         COPYRIGHT_BLOCK,
         "#pragma once",
@@ -2068,64 +2723,8 @@ def emit_schema_for_tests(schema: Dict[str, Any], rules: Dict[str, Any]) -> File
         "{",
     ]
 
-    categories: Dict[str, Any] = rules.get("categories", {})
-    global_excl = set(rules.get("global_exclusions", []))
-    type_map = rules.get("type_mappings", {})
-    default_type = rules.get("default_type", "float")
-    renames: Dict[str, str] = rules.get("property_renames", {})
-
-    for cat, cat_rules in categories.items():
-        elem_rules = rules.get("element_rules", {}).get(cat, {})
-        elem_excl = set(elem_rules.get("exclude_attrs", []))
-        elem_canons: List[str] = elem_rules.get("applies_canonicalizations", [])
-        canonicalizations = rules.get("canonicalizations", {})
-        canon_absorbed: set = set()
-        for c in elem_canons:
-            cd = canonicalizations.get(c)
-            if cd:
-                canon_absorbed.update(cd.get("absorbs_attrs", []))
-
-        # If schema emission is disabled for this category, the codegen
-        # produces no UPROPERTYs from the schema — so the completeness
-        # test shouldn't expect any. Emit an empty list and skip the
-        # rest of the per-attr accumulation. The category is still
-        # tracked (entry function still emitted) so call sites compile.
-        if cat_rules.get("disable_schema_emission"):
-            ident = cat.replace(".", "_")
-            lines.append(f"    inline const TArray<FString>& {ident.title()}Props()")
-            lines.append("    {")
-            lines.append("        static const TArray<FString> P{};")
-            lines.append("        return P;")
-            lines.append("    }")
-            lines.append("")
-            continue
-
-        attrs = list(schema_attrs(schema, cat_rules["schema_common_block"]))
-        # Extras live outside the schema block (e.g. frame.attrs doesn't
-        # exist in the snapshot; we provide pos/quat/childclass via
-        # schema_common_extra_attrs). Include them so the T3 completeness
-        # test asserts on the full property surface.
-        for a in cat_rules.get("schema_common_extra_attrs", []):
-            if a not in attrs:
-                attrs.append(a)
-        # xml_enum_attrs map schema-attr -> hand-declared UE enum UPROPERTY
-        # (e.g. camera.mode -> TrackingMode). Use the ue_property name in
-        # the schema-for-tests output so the completeness test resolves
-        # the actual UPROPERTY on the class.
-        xml_enum_attrs = elem_rules.get("xml_enum_attrs", {})
-        kept_props: List[str] = []
-        for c in elem_canons:
-            cd = canonicalizations.get(c)
-            if cd and cd.get("emits_property"):
-                kept_props.append(cd["emits_property"]["name"])
-        for a in attrs:
-            if a in global_excl or a in elem_excl or a in canon_absorbed:
-                continue
-            if a in xml_enum_attrs:
-                kept_props.append(xml_enum_attrs[a]["ue_property"])
-                continue
-            kept_props.append(renames.get(a, pascal_case(a)))
-
+    for cat, cat_rules in rules.get("categories", {}).items():
+        kept_props = kept_props_for_category(cat, cat_rules, schema, rules)
         ident = cat.replace(".", "_")
         joined = ", ".join(f'TEXT("{p}")' for p in kept_props)
         lines.append(f"    inline const TArray<FString>& {ident.title()}Props()")
@@ -2147,220 +2746,1286 @@ def emit_schema_for_tests(schema: Dict[str, Any], rules: Dict[str, Any]) -> File
 # Top-level orchestration
 # ---------------------------------------------------------------------------
 
-def collect_all_writes(
-    schema: Dict[str, Any],
-    rules: Dict[str, Any],
-    mjxmacro: Dict[str, Any],
-    public_root: str,
-    private_root: str,
-    bind_h_path: str,
-    mjspec: Dict[str, Any] | None = None,
-) -> List[FileWrite]:
-    """Walk every category in `codegen_rules.json` and produce the full set
-    of file writes the codegen would perform. No I/O side effects."""
-    writes: List[FileWrite] = []
-    categories: Dict[str, Any] = rules.get("categories", {})
+@dataclass
+class PhaseContext:
+    """All state a codegen emission phase needs. Phases mutate
+    ``writes`` and surface diagnostics through ``_diag_add``."""
+    schema:        Dict[str, Any]
+    rules:         Dict[str, Any]
+    mjxmacro:      Dict[str, Any]
+    mjspec:        Optional[Dict[str, Any]]
+    public_root:   str
+    private_root:  str
+    bind_h_path:   str
+    writes:        List["FileWrite"]
 
-    for cat, cat_rules in categories.items():
-        layout = cat_rules.get("layout", "no_subclasses")
-        if layout == "single_uclass_per_file":
-            # Base class injection + N subclass files.
-            writes.extend(emit_base_class_injection(cat, cat_rules, schema, rules, public_root, private_root, mjspec=mjspec))
-            writes.extend(emit_subclass_files(cat, cat_rules, schema, rules, public_root, private_root, mjspec=mjspec))
-        elif layout == "multi_uclass":
-            writes.extend(emit_multi_uclass(cat, cat_rules, schema, rules, public_root, private_root, mjspec=mjspec))
-        elif layout == "no_subclasses":
-            writes.extend(emit_base_class_injection(cat, cat_rules, schema, rules, public_root, private_root, mjspec=mjspec))
 
-    bind_write = emit_bind_h_injection(rules, mjxmacro, bind_h_path)
-    if bind_write:
-        writes.append(bind_write)
+@dataclass
+class EmissionPhase:
+    """One step in the codegen pipeline. Phases are run in order; each
+    either appends to ``ctx.writes`` (emit phases) or has side effects
+    only (the diagnostics phase). The registry below makes adding a
+    phase a one-line insert rather than another `writes.extend(...)`
+    block in ``collect_all_writes``.
+    """
+    name: str
+    fn:   Callable[["PhaseContext"], None]
 
-    writes.append(emit_schema_for_tests(schema, rules))
 
-    # Diagnostics: surface schema/snapshot drift that the rules don't yet
-    # account for. Stdout, not fatal — the codegen has emitted what it can;
-    # the operator reads these to know what manual rule edits are needed.
-    _emit_drift_diagnostics(schema, rules, mjspec)
+# ---------------------------------------------------------------------------
+# Synthetic categories: whole-file banner-mode emission
+# ---------------------------------------------------------------------------
+#
+# "Synthetic" because the USTRUCT doesn't correspond to one entry in the
+# MJCF schema (which describes XML element shapes). Instead it mirrors a
+# C struct laid out in mjxmacro.h (MJOPTION_FIELDS, MJSTATISTIC_FIELDS,
+# MJVISUAL_*_FIELDS) — the runtime-only state URLab exposes as USTRUCT
+# blueprint properties.
+#
+# Files are emitted in BANNER mode (no // --- CODEGEN_*_START/END ---
+# tags). The whole .h / .cpp pair is codegen-owned; hand-edits get
+# clobbered on next regen.
+#
+# Rule shape (under top-level ``synthetic_categories`` in codegen_rules.json):
+#   "MjStatistic": {
+#       "ue_struct_name":   "FMjStatistic",
+#       "mjxmacro_block":   "MJSTATISTIC_FIELDS",
+#       "public_header":    "MuJoCo/Generated/MjStatistic.h",
+#       "private_source":   "MuJoCo/Generated/MjStatistic.cpp",
+#       "category_label":   "MuJoCo|Statistic",
+#       "exclude_fields":   [],
+#       "per_field_meta":   { "extent": "ClampMin=0.0" },
+#       "urlab_extra_fields": [],   // empty for pure mirrors
+#   }
+
+
+_CPP_TO_UE_TYPE = {
+    "int":     "int32",
+    "float":   "float",
+    "mjtNum":  "double",
+    "mjtByte": "uint8",
+}
 
-    return writes
 
+def _ue_field_name(c_name: str) -> str:
+    """snake_case mjxmacro name -> PascalCase UE field. Preserves URLab's
+    naming convention from the hand-written MjSimOptions.h (ls_iterations
+    -> LsIterations, ccd_iterations -> CCD_Iterations is a deliberate
+    one-off — synthetic_categories rules can override via field_renames)."""
+    parts = c_name.split("_")
+    return "".join(p[:1].upper() + p[1:] for p in parts if p)
 
-def _emit_drift_diagnostics(schema: Dict[str, Any], rules: Dict[str, Any],
-                            mjspec: Dict[str, Any] | None) -> None:
-    """Print diagnostics that flag schema or mjspec drift the rules don't
-    cover. Run after every codegen pass so a MuJoCo version bump or a
-    schema-snapshot regen surfaces:
-
-      - new top-level schema elements with no category in codegen_rules.json
-      - new actuator subtypes (entries in schema['actuator_types']) with no
-        rule under categories.actuator.subtypes
-      - new sensor subtypes ditto
-      - new mjs_setTo* function parameters not mapped by subtype_setto rules
-      - schema attrs whose UE type would fall back to default_type ("float")
-        because they're missing from type_mappings (catches when MuJoCo
-        adds a new attr like ``cellcount`` that we want a richer type for)
-
-    Each item prints a one-line ``[diagnostic] ...`` hint with the file +
-    rule path the operator should edit. None of these are fatal because
-    the codegen is still useful in the partially-covered state.
-    """
-    categories = rules.get("categories", {})
-    msgs: List[str] = []
-
-    # 1) Top-level schema elements with no category coverage.
-    known_blocks = {c.get("schema_common_block", "").split(".", 1)[0]
-                    for c in categories.values()}
-    known_blocks.discard("")
-    # Items like asset/contact/equality are nested containers that aren't
-    # categories themselves but whose children are bucketed elsewhere — they
-    # don't need their own category, so we ignore them here.
-    container_keys = {
-        "_meta", "actuator_common", "actuator_types", "sensor_common",
-        "sensor_types", "sensor_extras", "tendon", "equality", "contact",
-        "asset", "compiler", "option", "keyframe", "default",
-    }
-    # Schema elements URLab intentionally does NOT codegen (yet). Each
-    # entry comes with a one-line reason so the rule-file reader knows
-    # why it's silenced. New MuJoCo elements that aren't here will trigger
-    # a diagnostic.
-    unmodeled: Dict[str, str] = rules.get("intentionally_unmodeled_elements", {})
-    for key in schema:
-        if key.startswith("_") or key in container_keys or key in known_blocks:
+
+# `_VEC3_CONVERT_FMT` lives in `_codegen_core`.
+
+
+def _emit_synth_mirror_properties(
+    fields: List[Dict[str, Any]],
+    def_: Dict[str, Any],
+    category_label: str,
+) -> Tuple[List[str], List[Dict[str, Any]]]:
+    """Emit the UPROPERTY block for the mjxmacro mirror fields and
+    return the parallel ``fields_meta`` list. ``fields_meta`` carries
+    the per-field shape (c_name, ue_name, ue_type, is_vec, count) the
+    out-of-line ApplyTo emitters need."""
+    exclude = set(def_.get("exclude_fields", []))
+    field_renames: Dict[str, str] = def_.get("field_renames", {})
+    field_types: Dict[str, str] = def_.get("field_types", {})
+    all_fields_type: Optional[str] = def_.get("all_fields_type")
+    field_defaults: Dict[str, str] = def_.get("field_defaults", {})
+
+    prop_lines: List[str] = []
+    fields_meta: List[Dict[str, Any]] = []
+    for f in fields:
+        c_name = f["name"]
+        if c_name in exclude:
             continue
-        if key in unmodeled:
+        ue_name = field_renames.get(c_name, _ue_field_name(c_name))
+        c_type = f["type"]
+        is_vec = f.get("is_vec", False)
+        # Accept both str ("3") and int (3) — the mjxmacro snapshot
+        # currently emits strings, but a future shape bump that yields
+        # ints must NOT silently collapse vec3 fields to ``TArray<float>``.
+        raw_count = f.get("count", 1)
+        if isinstance(raw_count, int):
+            count = raw_count
+        elif isinstance(raw_count, str) and raw_count.isdigit():
+            count = int(raw_count)
+        else:
+            count = 1
+
+        if c_name in field_types:
+            ue_type = field_types[c_name]
+        elif all_fields_type:
+            ue_type = all_fields_type
+        elif is_vec and count == 3:
+            ue_type = "FVector"
+        elif is_vec and count == 4:
+            ue_type = "FLinearColor"
+        elif is_vec:
+            ue_type = "TArray<float>"
+        else:
+            ue_type = _CPP_TO_UE_TYPE.get(c_type, "float")
+
+        default_override = field_defaults.get(ue_name) or field_defaults.get(c_name)
+
+        cat = f"MuJoCo|{category_label}"
+        toggle = override_toggle_name(ue_name)
+        default = default_override if default_override else _attr_default_value(ue_type)
+        prop_lines.append(
+            f'    UPROPERTY(EditAnywhere, Category = "{cat}", meta=(InlineEditConditionToggle))\n'
+            f'    bool {toggle} = false;\n\n'
+            f'    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "{cat}", meta=(EditCondition="{toggle}"))\n'
+            f'    {ue_type} {ue_name} = {default};\n\n'
+        )
+        fields_meta.append({
+            "c_name":   c_name,
+            "ue_name":  ue_name,
+            "ue_type":  ue_type,
+            "is_vec":   is_vec,
+            "count":    count,
+        })
+    return prop_lines, fields_meta
+
+
+def _emit_synth_extras_properties(
+    urlab_extras: List[Dict[str, Any]],
+    category_label: str,
+) -> Tuple[List[str], List[Dict[str, Any]]]:
+    """Emit UPROPERTY blocks for the URLab-extra fields (non-mjxmacro
+    additions like ``MemoryMB`` on MjOption). Returns prop_lines +
+    extras_meta. Each extra carries optional ``has_override``,
+    ``edit_condition``, ``meta`` knobs that the mirror loop above
+    doesn't surface."""
+    prop_lines: List[str] = []
+    extras_meta: List[Dict[str, Any]] = []
+    for extra in urlab_extras:
+        ue_name = extra["name"]
+        ue_type = extra["type"]
+        default = extra.get("default", _attr_default_value(ue_type))
+        sub_category = extra.get("sub_category")
+        has_override = extra.get("has_override", True)
+        edit_condition = extra.get("edit_condition")
+        meta_extra = extra.get("meta")
+        cat = f"MuJoCo|{category_label}"
+        if sub_category:
+            cat = f"{cat}|{sub_category}"
+        if has_override:
+            toggle = override_toggle_name(ue_name)
+            meta_inner = f'EditCondition="{toggle}"'
+            if meta_extra:
+                meta_inner = f'{meta_inner}, {meta_extra}'
+            prop_lines.append(
+                f'    UPROPERTY(EditAnywhere, Category = "{cat}", meta=(InlineEditConditionToggle))\n'
+                f'    bool {toggle} = false;\n\n'
+                f'    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "{cat}", meta=({meta_inner}))\n'
+                f'    {ue_type} {ue_name} = {default};\n\n'
+            )
+        else:
+            meta_parts = []
+            if edit_condition:
+                meta_parts.append(f'EditCondition="{edit_condition}"')
+            if meta_extra:
+                meta_parts.append(meta_extra)
+            meta_str = ", ".join(meta_parts)
+            meta_clause = f', meta=({meta_str})' if meta_str else ""
+            prop_lines.append(
+                f'    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "{cat}"{meta_clause})\n'
+                f'    {ue_type} {ue_name} = {default};\n\n'
+            )
+        extras_meta.append({"ue_name": ue_name, "type": ue_type,
+                            "has_override": has_override})
+    return prop_lines, extras_meta
+
+
+def _emit_synth_inline_apply_template(
+    fields_meta: List[Dict[str, Any]],
+    field_skip_apply: Set[str],
+) -> str:
+    """Emit the templated inline ``ApplyTo`` body for pure-mirror
+    synthetic structs (mjStatistic etc.). The shape mirrors the
+    out-of-line ApplyToSpec but lives in the header so it's reusable
+    against any compatible C struct (mjStatistic, mjVisualRgba)."""
+    apply_lines: List[str] = []
+    for fm in fields_meta:
+        c_name = fm["c_name"]
+        ue_name = fm["ue_name"]
+        if c_name in field_skip_apply:
             continue
-        msgs.append(
-            f"[diagnostic] schema has top-level element '{key}' but no "
-            f"category in codegen_rules.json (rules['categories']). "
-            f"Either add a category entry, list '{key}' in "
-            f"container_keys, or add an "
-            f"'intentionally_unmodeled_elements' entry explaining why "
-            f"it's skipped."
+        if fm["ue_type"] == "FLinearColor":
+            apply_lines.append(
+                f"    Dst.{c_name}[0] = static_cast<decltype(Dst.{c_name}[0])>({ue_name}.R);\n"
+                f"    Dst.{c_name}[1] = static_cast<decltype(Dst.{c_name}[1])>({ue_name}.G);\n"
+                f"    Dst.{c_name}[2] = static_cast<decltype(Dst.{c_name}[2])>({ue_name}.B);\n"
+                f"    Dst.{c_name}[3] = static_cast<decltype(Dst.{c_name}[3])>({ue_name}.A);"
+            )
+        elif fm["ue_type"] == "FVector":
+            apply_lines.append(
+                f"    Dst.{c_name}[0] = static_cast<decltype(Dst.{c_name}[0])>({ue_name}.X);\n"
+                f"    Dst.{c_name}[1] = static_cast<decltype(Dst.{c_name}[1])>({ue_name}.Y);\n"
+                f"    Dst.{c_name}[2] = static_cast<decltype(Dst.{c_name}[2])>({ue_name}.Z);"
+            )
+        elif fm["is_vec"]:
+            apply_lines.append(
+                f"    for (int32 I = 0; I < {ue_name}.Num() && I < {fm['count']}; ++I)\n"
+                f"        Dst.{c_name}[I] = static_cast<decltype(Dst.{c_name}[0])>({ue_name}[I]);"
+            )
+        else:
+            apply_lines.append(f"    Dst.{c_name} = static_cast<decltype(Dst.{c_name})>({ue_name});")
+    return (
+        f"    /** @brief Mirror every field into a runtime C struct. */\n"
+        f"    template <typename TDst>\n"
+        f"    void ApplyTo(TDst& Dst) const\n"
+        f"    {{\n"
+        f"{chr(10).join(apply_lines)}\n"
+        f"    }}\n"
+    )
+
+
+def _emit_synthetic_struct_files(
+    cat_name: str, def_: Dict[str, Any], mjxmacro: Dict[str, Any],
+    public_root: str, private_root: str,
+) -> List["FileWrite"]:
+    """Emit the .h + .cpp pair for one synthetic_categories entry.
+
+    Currently supports the mjxmacro_block source (X-macro field list).
+    """
+    block_name = def_.get("mjxmacro_block")
+    if not block_name:
+        return []
+    fields = mjxmacro.get("struct_fields", {}).get(block_name, [])
+    if not fields:
+        _diag_add(
+            f"[diagnostic] synthetic_categories['{cat_name}'] references "
+            f"mjxmacro_block='{block_name}' which is empty or missing in "
+            f"the snapshot. Skipping emission.",
+            source="synthetic_categories",
         )
+        return []
 
-    # 2) Actuator subtypes not mapped.
-    act_cat = categories.get("actuator", {})
-    known_act_subs = {s.get("key") for s in act_cat.get("subtypes", [])}
-    for name in schema.get("actuator_types", {}):
-        if name not in known_act_subs:
-            msgs.append(
-                f"[diagnostic] schema actuator subtype '{name}' has no "
-                f"entry in categories.actuator.subtypes. Add a subtype "
-                f"+ subtype_setto rule pair so codegen emits the UMj"
-                f"{name.title()}Actuator class."
+    ue_struct = def_["ue_struct_name"]
+    category_label = def_.get("category_label", "MuJoCo")
+    field_apply_mode: Dict[str, str] = def_.get("field_apply_to_spec_mode", {})
+    field_skip_apply: set = set(def_.get("field_skip_apply", []))
+    public_relpath = def_["public_header"]
+    private_relpath = def_.get("private_source")
+    extra_includes: List[str] = def_.get("include_directives", [])
+    urlab_extras: List[Dict[str, Any]] = def_.get("urlab_extra_fields", [])
+    apply_extras_function: Optional[str] = def_.get("apply_extras_function")
+    emit_apply_methods: bool = bool(def_.get("emit_apply_methods", False))
+
+    mirror_lines, fields_meta = _emit_synth_mirror_properties(
+        fields, def_, category_label,
+    )
+    extras_lines, _extras_meta = _emit_synth_extras_properties(
+        urlab_extras, category_label,
+    )
+    properties_block = "".join(mirror_lines + extras_lines)
+    c_struct = def_.get("c_struct_type", block_name.lower().replace("_fields", ""))
+
+    # ---------- header content ----------
+    extra_includes_block = "".join(f'#include "{inc}"\n' for inc in extra_includes)
+    header_basename = os.path.basename(public_relpath).replace('.h', '')
+    public_path = os.path.join(public_root, public_relpath.replace("/", os.sep))
+
+    # Method decls in the header. If emit_apply_methods=True, declare
+    # ApplyToSpec / ApplyOverridesToModel as forward decls (definitions
+    # land in the .cpp). Otherwise emit an inline-templated ApplyTo for
+    # pure-mirror structs (mjStatistic etc.).
+    if emit_apply_methods:
+        method_decls = (
+            f"    /** @brief Writes every mirror field plus URLab extras into the "
+            f"runtime spec. Used by articulations to set child spec options. */\n"
+            f"    void ApplyToSpec(mjSpec* Spec) const;\n\n"
+            f"    /** @brief Applies only override-toggled mirror fields to a "
+            f"compiled model (post-compile override path). */\n"
+            f"    void ApplyOverridesToModel(mjModel* Model) const;\n"
+        )
+    else:
+        method_decls = _emit_synth_inline_apply_template(
+            fields_meta, field_skip_apply,
+        )
+
+    # Forward-decl the extras function so the codegen-emitted .cpp can
+    # call it without #including a hand-written header. Operator writes
+    # the body in *Extras.cpp.
+    extras_forward_decl = ""
+    if apply_extras_function and urlab_extras:
+        extras_forward_decl = (
+            f"// Forward-decl: hand-written body lives in "
+            f"{os.path.basename(public_relpath).replace('.h', '')}Extras.cpp.\n"
+            f"struct {ue_struct};\n"
+            f"void {apply_extras_function}("
+            f"mjOption* Opt, const {ue_struct}& Self, "
+            f"mjSpec* Spec, mjModel* Model = nullptr);\n\n"
+        )
+
+    header_content = (
+        f"{COPYRIGHT_BLOCK}\n"
+        f"#pragma once\n\n"
+        f"#include \"CoreMinimal.h\"\n"
+        f"{extra_includes_block}"
+        f"#include \"{header_basename}.generated.h\"\n\n"
+        f"{extras_forward_decl}"
+        f"// Mirror of MuJoCo's ``{c_struct}`` ({block_name}). Codegen-owned —\n"
+        f"// hand-edits get clobbered on next regen. Add fields by editing\n"
+        f"// codegen_rules.json[synthetic_categories.{cat_name}].\n"
+        f"USTRUCT(BlueprintType)\n"
+        f"struct URLAB_API {ue_struct}\n"
+        f"{{\n"
+        f"    GENERATED_BODY()\n\n"
+        f"{properties_block}"
+        f"{method_decls}"
+        f"}};\n"
+    )
+
+    writes: List[FileWrite] = [FileWrite(path=public_path, content=header_content)]
+
+    if emit_apply_methods and private_relpath:
+        cpp_content = _emit_synth_apply_methods_cpp(
+            ue_struct=ue_struct,
+            fields_meta=fields_meta,
+            field_apply_mode=field_apply_mode,
+            field_skip_apply=field_skip_apply,
+            apply_extras_function=apply_extras_function,
+            urlab_extras=urlab_extras,
+            public_relpath=public_relpath,
+            header_basename=header_basename,
+        )
+        private_path = os.path.join(private_root, private_relpath.replace("/", os.sep))
+        writes.append(FileWrite(path=private_path, content=cpp_content))
+
+    return writes
+
+
+def _emit_synth_apply_methods_cpp(
+    *,
+    ue_struct: str,
+    fields_meta: List[Dict[str, Any]],
+    field_apply_mode: Dict[str, str],
+    field_skip_apply: Set[str],
+    apply_extras_function: Optional[str],
+    urlab_extras: List[Dict[str, Any]],
+    public_relpath: str,
+    header_basename: str,
+) -> str:
+    """Emit the out-of-line ``ApplyToSpec`` and ``ApplyOverridesToModel``
+    bodies for a synthetic struct whose mirror writes go through mjSpec
+    + mjModel (mjOption is the canonical case).
+
+    Both methods gate every field write with ``bOverride_X``. The earlier
+    ``unconditional`` SPEC default silently clobbered MJCF-XML defaults
+    with whatever the UE struct happened to carry, even for fields the
+    user never authored. ``field_apply_mode`` now selects only the
+    per-FVector conversion flavour (``vec3_cm`` / ``vec3_y_flip`` /
+    ``pos``); the toggle guard is applied uniformly.
+    """
+    apply_to_spec_lines: List[str] = ["    if (!Spec) return;"]
+    apply_to_model_lines: List[str] = ["    if (!Model) return;"]
+    for fm in fields_meta:
+        c_name = fm["c_name"]
+        ue_name = fm["ue_name"]
+        ue_type = fm["ue_type"]
+        if c_name in field_skip_apply:
+            continue
+        toggle = override_toggle_name(ue_name)
+        spec_mode = field_apply_mode.get(c_name, "")
+        if ue_type == "FVector":
+            # Strip a legacy "guarded_" prefix; the guard is now uniform.
+            conv = spec_mode.replace("guarded_", "") if spec_mode.startswith("guarded_") else spec_mode
+            if conv not in _VEC3_CONVERT_FMT:
+                conv = "vec3_y_flip"
+            stmt_spec_inner = _VEC3_CONVERT_FMT[conv].format(
+                c=f"Spec->option.{c_name}", ue=ue_name,
+            )
+            stmt_model_inner = _VEC3_CONVERT_FMT[conv].format(
+                c=f"Model->opt.{c_name}", ue=ue_name,
             )
+        elif ue_type.startswith("EMj"):
+            stmt_spec_inner = f"Spec->option.{c_name} = static_cast<int>({ue_name});"
+            stmt_model_inner = f"Model->opt.{c_name} = static_cast<int>({ue_name});"
+        else:
+            stmt_spec_inner = f"Spec->option.{c_name} = {ue_name};"
+            stmt_model_inner = f"Model->opt.{c_name} = {ue_name};"
+
+        apply_to_spec_lines.append(f"    if ({toggle}) {{ {stmt_spec_inner} }}")
+        apply_to_model_lines.append(f"    if ({toggle}) {{ {stmt_model_inner} }}")
+
+    if apply_extras_function and urlab_extras:
+        # Spec / Model already null-checked by the early return at the
+        # top of each body, so the ternary is dead — pass the address
+        # directly.
+        apply_to_spec_lines.append(
+            f"    {apply_extras_function}(&Spec->option, *this, Spec);"
+        )
+        apply_to_model_lines.append(
+            f"    {apply_extras_function}(&Model->opt, *this, nullptr, Model);"
+        )
 
-    # 3) Sensor subtypes not mapped.
-    sens_cat = categories.get("sensor", {})
-    known_sens_subs = {s.get("key") for s in sens_cat.get("subtypes", [])}
-    for name in schema.get("sensor_types", []):
-        if name not in known_sens_subs:
-            msgs.append(
-                f"[diagnostic] schema sensor type '{name}' has no entry "
-                f"in categories.sensor.subtypes. Add it so the per-sensor "
-                f"UMjXSensor subclass is emitted."
+    return (
+        f"{COPYRIGHT_BLOCK}\n"
+        f"#include \"{os.path.dirname(public_relpath).replace(os.sep, '/')}/{header_basename}.h\"\n"
+        f"#include <mujoco/mujoco.h>\n\n"
+        f"void {ue_struct}::ApplyToSpec(mjSpec* Spec) const\n"
+        f"{{\n"
+        + "\n".join(apply_to_spec_lines) + "\n"
+        f"}}\n\n"
+        f"void {ue_struct}::ApplyOverridesToModel(mjModel* Model) const\n"
+        f"{{\n"
+        + "\n".join(apply_to_model_lines) + "\n"
+        f"}}\n"
+    )
+
+
+def _phase_synthetic_categories(ctx: PhaseContext) -> None:
+    """Run after the main category walk; emits whole-file synthetic structs."""
+    synth = ctx.rules.get("synthetic_categories", {})
+    for cat_name, def_ in synth.items():
+        # Skip _note_* sibling keys used for inline documentation.
+        if cat_name.startswith("_") or not isinstance(def_, dict):
+            continue
+        ctx.writes.extend(_emit_synthetic_struct_files(
+            cat_name, def_, ctx.mjxmacro, ctx.public_root, ctx.private_root,
+        ))
+
+
+# ---------------------------------------------------------------------------
+# Sensor switch + TagToType codegen
+# ---------------------------------------------------------------------------
+#
+# Replaces the hand-written switch + map in MjSensor.cpp with codegen
+# output driven by:
+#   - codegen_rules.json[categories.sensor.subtypes] for XML key + enum
+#     value + (optional) case_body_override
+#   - sensor_per_type (from build_mjcf_schema_snapshot.py's Sensor()
+#     scrape) for mj_type + static objtype/reftype literals
+#
+# Variable-objtype/reftype branches (frame*, geomdist, contact, plugin,
+# user) stay hand-written in the post-switch block — that block reads
+# UE-side ObjType / RefType properties and applies them after the
+# codegen case fires.
+
+def _emit_sensor_switch_block(cat_rules: Dict[str, Any],
+                             sensor_per_type: Dict[str, Any]) -> str:
+    """One ``case EMjSensorType::X: ...`` per subtype + the default fallback.
+    Lives between ``CODEGEN_SENSOR_TYPE_SWITCH_*`` markers in MjSensor.cpp.
+    """
+    lines: List[str] = []
+    type_enum = cat_rules.get("type_enum_name", "EMjSensorType")
+    for subtype in cat_rules.get("subtypes", []):
+        key = subtype["key"]
+        enum_value = subtype["enum_value"]
+        override = subtype.get("case_body_override")
+        if override:
+            lines.append(f"        case {type_enum}::{enum_value}:")
+            lines.append(f"            {override} break;")
+            continue
+        per = sensor_per_type.get(key, {})
+        mj_type = per.get("mj_type")
+        if not mj_type:
+            # The sensor switch will fall through to ``default:`` for this
+            # subtype at runtime — i.e. all framejerk/whatever sensors get
+            # the default mjSENS_ACCELEROMETER + mjOBJ_SITE substitution.
+            # Loud diagnostic so a regression in build_mjcf_schema_snapshot's
+            # _extract_sensor_per_type regex doesn't silently miscompile.
+            _diag_add(
+                f"[diagnostic] sensor subtype '{key}' has no entry in "
+                f"sensor_per_type snapshot — runtime will fall through to "
+                f"the default mjSENS_ACCELEROMETER + mjOBJ_SITE branch. "
+                f"Check the _extract_sensor_per_type regex against "
+                f"mujoco/src/user/user_objects.cc.",
+                source="sensor_scrape_miss",
             )
+            lines.append(f"        // (skipped: no mj_type for '{key}' in sensor_per_type)")
+            continue
+        stmts = [f"Element->type = {mj_type};"]
+        # Only emit static objtype/reftype when sensor_per_type carries a
+        # literal mjOBJ_X. "from_xml" and "computed" entries are handled
+        # by the post-switch block reading UE-side ObjType/RefType.
+        objtype = per.get("objtype")
+        if isinstance(objtype, str) and objtype.startswith("mjOBJ_"):
+            stmts.append(f"Element->objtype = {objtype};")
+        reftype = per.get("reftype")
+        if isinstance(reftype, str) and reftype.startswith("mjOBJ_"):
+            stmts.append(f"Element->reftype = {reftype};")
+        lines.append(f"        case {type_enum}::{enum_value}: "
+                     f"{' '.join(stmts)} break;")
+    default_per = sensor_per_type.get("accelerometer", {})
+    default_type = default_per.get("mj_type", "mjSENS_ACCELEROMETER")
+    default_objtype = default_per.get("objtype") or "mjOBJ_SITE"
+    if not (isinstance(default_objtype, str) and default_objtype.startswith("mjOBJ_")):
+        default_objtype = "mjOBJ_SITE"
+    lines.append(f"        default: Element->type = {default_type}; "
+                 f"Element->objtype = {default_objtype}; break;")
+    return "\n".join(lines) + "\n"
 
-    # 4) mjs_setTo* params with no UE-side mapping AND no explicit
-    # sentinel override. Params that have an entry in setto_param_defaults
-    # are intentionally non-schema-driven (the rule pins their value);
-    # skip them.
-    if mjspec and "setto_functions" in mjspec:
-        setto_defaults = rules.get("setto_param_defaults", {})
-        for cat_name, cat_rules in categories.items():
-            setto_block = cat_rules.get("subtype_setto") or {}
-            common_attrs = set(schema_attrs(
-                schema, cat_rules.get("schema_common_block", "")))
-            for sub_key, setto_def in setto_block.items():
-                if not isinstance(setto_def, dict):
-                    continue
-                fn_name = setto_def.get("call")
-                if not fn_name or fn_name not in mjspec["setto_functions"]:
-                    continue
-                sig = mjspec["setto_functions"][fn_name]
-                renames = setto_def.get("param_renames", {})
-                fn_defaults = set(setto_defaults.get(fn_name, {}).keys())
-                sub_attrs = set(schema_subtype_attrs(schema, cat_name, sub_key))
-                schema_attr_set = common_attrs | sub_attrs
-                for p in sig.get("params", []):
-                    pname = p["name"]
-                    ue_prop = renames.get(pname, pname)
-                    if ue_prop in schema_attr_set or pname in fn_defaults:
-                        continue
-                    msgs.append(
-                        f"[diagnostic] {fn_name} param '{pname}' is not "
-                        f"in the {cat_name}.{sub_key} schema attrs and "
-                        f"has no param_renames or setto_param_defaults "
-                        f"entry. Codegen passes the generic sentinel for "
-                        f"it. If MuJoCo added this param, decide whether "
-                        f"to expose it as a UE UPROPERTY (schema attr + "
-                        f"type_mapping) or pin its sentinel."
-                    )
 
-    # 5) Attrs that fall back to default_type. Cosmetic — works but the
-    # UPROPERTY shape may be wrong (e.g. float for an int).
-    type_map = rules.get("type_mappings", {})
-    default_type = rules.get("default_type", "float")
-    seen_untyped: set = set()
-    for cat_name, cat_rules in categories.items():
-        block = cat_rules.get("schema_common_block", "")
-        for attr in schema_attrs(schema, block):
-            if attr in type_map or attr in seen_untyped:
+def _emit_sensor_tag_to_type_block(cat_rules: Dict[str, Any]) -> str:
+    """One ``{TEXT(""), EMjSensorType::X}`` per subtype. Lives between
+    ``CODEGEN_SENSOR_TAG_TO_TYPE_*`` markers in MjSensor.cpp.
+    """
+    lines: List[str] = []
+    type_enum = cat_rules.get("type_enum_name", "EMjSensorType")
+    for subtype in cat_rules.get("subtypes", []):
+        # Some subtype XML keys collide with C++ enum members that diverge
+        # (e.g. `key=tendonactuatorfrc`, `enum_value=TendonActFrc`,
+        # mjSENS_TENDONACTFRC). The tag-to-type map uses the XML key
+        # verbatim so MJCF lookups match.
+        key = subtype["key"]
+        enum_value = subtype["enum_value"]
+        lines.append(f'        {{TEXT("{key}"), {type_enum}::{enum_value}}},')
+    return "\n".join(lines) + "\n"
+
+
+# ---------------------------------------------------------------------------
+# Generated enum headers
+# ---------------------------------------------------------------------------
+#
+# Some MuJoCo C enums (mjtIntegrator/mjtCone/mjtSolver/...) map to URLab
+# UENUMs that are blueprint-exposed. Hand-written copies diverge from the
+# C source over time, so this emitter generates them from the projected
+# mjspec dict's ``enums`` table (sourced from the introspect snapshot).
+#
+# Rule shape (under top-level ``generated_enums`` in codegen_rules.json):
+#   "MjOptionEnums": {
+#       "public_header": "MuJoCo/Generated/MjOptionEnums.h",
+#       "enums": [
+#         {
+#           "ue_name": "EMjIntegrator",
+#           "from_mj_enum": "mjtIntegrator",
+#           "underlying_type": "uint8",      // optional, defaults uint8
+#           "ue_member_from_mj": {
+#             "mjINT_EULER":        "Euler",
+#             "mjINT_RK4":          "RK4",
+#             ...
+#           },
+#           // Optional: members from C enum to skip in UE (e.g. internal
+#           // sentinels that aren't blueprint-meaningful).
+#           "exclude_mj_members": []
+#         }
+#       ]
+#   }
+
+
+def _emit_generated_enum_file(cat_name: str, def_: Dict[str, Any],
+                              mjspec: Optional[Dict[str, Any]],
+                              public_root: str) -> List["FileWrite"]:
+    """Emit a single banner-mode .h carrying one or more UENUMs.
+
+    Rule shape (per enum entry):
+      ``ue_name``           — UE enum class name (e.g. ``EMjGainType``)
+      ``from_mj_enum``      — MuJoCo source enum (e.g. ``mjtGain``)
+      ``ue_member_from_mj`` — opt-in mapping mj_const → UE member name
+      ``exclude_mj_members``— optional skip list within the source enum
+      ``extra_members``     — list of {"name": ...} URLab-extra
+                              members appended AFTER the mj-derived
+                              ones. Used by Camp B enums that carry
+                              URLab-specific values without a mjt
+                              counterpart (e.g. ``DcMotor`` for
+                              ``EMjGainType``).
+
+    Rule entries with ``"disabled": true`` are skipped entirely — used
+    while a hand-rolled enum is being staged for migration so the rule
+    can land in JSON without breaking the build.
+    """
+    if def_.get("disabled"):
+        return []
+    if not mjspec or "enums" not in mjspec:
+        _diag_add(
+            f"[diagnostic] generated_enums['{cat_name}'] needs an mjspec "
+            f"snapshot with an 'enums' key — run build_introspect_snapshot.py.",
+            source="generated_enums",
+        )
+        return []
+    enums = def_.get("enums", [])
+    if not enums:
+        return []
+
+    public_relpath = def_["public_header"]
+    header_basename = os.path.basename(public_relpath).replace(".h", "")
+
+    enum_blocks: List[str] = []
+    for entry in enums:
+        if entry.get("disabled"):
+            continue
+        ue_name = entry["ue_name"]
+        # ``from_mj_enum`` is optional when the URLab enum has no mjt
+        # counterpart at all (EMjFluidShape, EMjGeomInertia, etc) — the
+        # rule populates extra_members and skips the mjspec lookup.
+        mj_enum_name: Optional[str] = entry.get("from_mj_enum")
+        underlying = entry.get("underlying_type", "uint8")
+        ue_member_from_mj: Dict[str, str] = entry.get("ue_member_from_mj", {})
+        exclude: set = set(entry.get("exclude_mj_members", []))
+        extra_members: List[Dict[str, Any]] = entry.get("extra_members", [])
+        c_enum: Dict[str, int] = {}
+        if mj_enum_name:
+            c_enum = mjspec["enums"].get(mj_enum_name, {})
+            if not c_enum:
+                _diag_add(
+                    f"[diagnostic] generated_enums['{cat_name}']: C enum "
+                    f"'{mj_enum_name}' missing from the introspect snapshot. "
+                    f"Skipping '{ue_name}'.",
+                    source="generated_enums",
+                )
                 continue
-            # Only warn for attrs likely meant to be array/int/bool — the
-            # heuristic: numeric-shaped names with a plural or ending in
-            # "count"/"range"/"limit" probably aren't a float scalar.
-            if any(s in attr for s in ("count", "range", "limit", "coef",
-                                       "data", "params", "vert", "elem",
-                                       "size", "list", "ids")):
-                msgs.append(
-                    f"[diagnostic] attr '{attr}' (used by {cat_name}) "
-                    f"falls back to default_type ('{default_type}') — "
-                    f"add it to type_mappings if it should be TArray<...> "
-                    f"or int32."
+        elif not extra_members:
+            _diag_add(
+                f"[diagnostic] generated_enums['{cat_name}']: '{ue_name}' "
+                f"has neither from_mj_enum nor extra_members; nothing to "
+                f"emit.",
+                source="generated_enums",
+            )
+            continue
+        member_lines: List[str] = []
+        used_values: set = set()
+        # Iterate by (value, mj_const) so the emitted UENUM order is stable
+        # across libclang scrapes — the snapshot's dict insertion order is
+        # whatever the AST walk produced, which can drift on header edits.
+        for mj_const, value in sorted(c_enum.items(), key=lambda kv: (kv[1], kv[0])):
+            if mj_const in exclude:
+                continue
+            ue_member = ue_member_from_mj.get(mj_const)
+            if not ue_member:
+                continue  # silently skip; rule author opts in per-const
+            member_lines.append(f"    {ue_member} = {value},")
+            used_values.add(value)
+        # URLab-extra members run AFTER the mj-derived block. Values
+        # auto-increment past the highest used mj value so the resulting
+        # uint8 sequence stays contiguous + reproducible.
+        next_value = (max(used_values) + 1) if used_values else 0
+        for extra in extra_members:
+            name = extra.get("name")
+            if not name:
+                _diag_add(
+                    f"[diagnostic] generated_enums['{cat_name}'] '{ue_name}'"
+                    f" extra_members entry {extra!r} missing 'name'.",
+                    source="generated_enums",
                 )
-                seen_untyped.add(attr)
+                continue
+            value = extra.get("value", next_value)
+            member_lines.append(f"    {name} = {value},")
+            next_value = value + 1
+        if not member_lines:
+            continue
+        # Strip the trailing comma on the last member for cleaner C++.
+        member_lines[-1] = member_lines[-1].rstrip(",")
+        # Doc line: only mention the mj source when the rule pinned
+        # one. Pure-extras enums (no mjt counterpart) get a one-liner
+        # naming them as URLab-side.
+        doc = (
+            f"/** {ue_name} — mirror of MuJoCo's {mj_enum_name}. */\n"
+            if mj_enum_name
+            else f"/** {ue_name} — URLab editorial enum (no mjt counterpart). */\n"
+        )
+        enum_blocks.append(
+            doc
+            + f"UENUM(BlueprintType)\n"
+            f"enum class {ue_name} : {underlying}\n"
+            f"{{\n"
+            + "\n".join(member_lines) + "\n"
+            f"}};\n"
+        )
 
-    if msgs:
-        print(f"\n--- codegen diagnostics ({len(msgs)}) ---", file=sys.stderr)
-        for m in msgs:
-            print(m, file=sys.stderr)
-        print(
-            "(diagnostics are non-fatal; the codegen has emitted what it "
-            "can — fix the rules to silence them.)",
-            file=sys.stderr,
+    if not enum_blocks:
+        return []
+
+    content = (
+        f"{COPYRIGHT_BLOCK}\n"
+        f"#pragma once\n\n"
+        f"#include \"CoreMinimal.h\"\n"
+        f"#include \"{header_basename}.generated.h\"\n\n"
+        + "\n".join(enum_blocks)
+    )
+    public_path = os.path.join(public_root, public_relpath.replace("/", os.sep))
+    return [FileWrite(path=public_path, content=content)]
+
+
+def _phase_generated_enums(ctx: PhaseContext) -> None:
+    gen = ctx.rules.get("generated_enums", {})
+    for cat_name, def_ in gen.items():
+        # _note_* sibling keys document the surrounding entries; they
+        # appear as strings, not dicts, and must skip the emit path.
+        if cat_name.startswith("_") or not isinstance(def_, dict):
+            continue
+        ctx.writes.extend(_emit_generated_enum_file(
+            cat_name, def_, ctx.mjspec, ctx.public_root,
+        ))
+
+
+# ---------------------------------------------------------------------------
+# Articulation category registry
+# ---------------------------------------------------------------------------
+#
+# Emits a header with a compile-time table of every URLab-managed MuJoCo
+# category, listing the schema element name, UE base class, mjs struct,
+# and the layout shape. Consumers (diagnostic dumps, BP iteration,
+# test-time category enumeration) can iterate constexpr instead of
+# duplicating the category list in C++ source.
+
+# ---------------------------------------------------------------------------
+# Editor option helpers
+# ---------------------------------------------------------------------------
+#
+# Codegens the trivial ``TArray<FString> UMjX::GetYOptions() const`` editor
+# helpers that just forward to ``UMjComponent::GetSiblingComponentOptions``.
+# Wrappers with real per-class logic (UMjActuator::GetTargetNameOptions,
+# UMjSensor::Get(Target|Reference)NameOptions, UMjEquality::GetObjOptions)
+# stay hand-written in their owning .cpp files.
+#
+# Emission shape: each host .cpp file has a CODEGEN_EDITOR_OPTIONS_START/END
+# marker pair inside its ``#if WITH_EDITOR`` block. The phase injects the
+# class's wrappers between those markers — output stays INSIDE the host
+# .cpp, not a separate file. (Avoids the ``.gen.cpp`` UHT-collision name.)
+#
+# Rule shape (top-level ``editor_option_helpers.wrappers``):
+#   [
+#     {
+#       "class": "UMjActuator",
+#       "host_cpp": "MuJoCo/Components/Actuators/MjActuator.cpp",
+#       "method": "GetSliderSiteOptions",
+#       "filter_class": "UMjSite",
+#       "include_defaults": false
+#     },
+#     ...
+#   ]
+
+def _phase_editor_option_helpers(ctx: PhaseContext) -> None:
+    def_ = ctx.rules.get("editor_option_helpers")
+    if not def_:
+        return
+    wrappers = def_.get("wrappers", [])
+    if not wrappers:
+        return
+    # Group by host_cpp.
+    by_host: Dict[str, List[Dict[str, Any]]] = {}
+    for w in wrappers:
+        host = w.get("host_cpp")
+        if not host:
+            _diag_add(
+                f"[diagnostic] editor_option_helpers: missing host_cpp on "
+                f"wrapper {w!r}. Skipping.",
+                source="editor_option_helpers",
+            )
+            continue
+        by_host.setdefault(host, []).append(w)
+    # Inject per-file.
+    for host_relpath, group in by_host.items():
+        host_path = os.path.join(ctx.private_root, host_relpath.replace("/", os.sep))
+        if not os.path.exists(host_path):
+            _diag_add(
+                f"[diagnostic] editor_option_helpers: host_cpp '{host_relpath}' "
+                f"not found on disk. Skipping injection.",
+                source="editor_option_helpers",
+            )
+            continue
+        with open(host_path, "r", encoding="utf-8") as f:
+            text = f.read()
+        lines: List[str] = []
+        for w in group:
+            cls = w["class"]
+            method = w["method"]
+            filt = w["filter_class"]
+            inc_def = bool(w.get("include_defaults", False))
+            defaults_arg = ", true" if inc_def else ""
+            lines.append(
+                f"TArray<FString> {cls}::{method}() const {{ return "
+                f"UMjComponent::GetSiblingComponentOptions(this, "
+                f"{filt}::StaticClass(){defaults_arg}); }}"
+            )
+        body = "\n".join(lines) + "\n"
+        new_text, ok = inject_between_tags(text, "EDITOR_OPTIONS", body)
+        if not ok:
+            _diag_add(
+                f"[diagnostic] editor_option_helpers: host_cpp '{host_relpath}' "
+                f"missing the CODEGEN_EDITOR_OPTIONS_START/END marker pair. "
+                f"Add the markers inside the host's #if WITH_EDITOR block "
+                f"or remove the wrapper entries.",
+                source="editor_option_helpers",
+            )
+            continue
+        if new_text != text:
+            ctx.writes.append(FileWrite(path=host_path, content=new_text))
+
+
+def _phase_articulation_registry(ctx: PhaseContext) -> None:
+    cats = ctx.rules.get("categories", {})
+    if not cats:
+        return
+    out_relpath = "MuJoCo/Generated/MjArticulationRegistry.h"
+    entries: List[str] = []
+    for name in sorted(cats):
+        cat = cats[name]
+        base = cat.get("base_class_name", "")
+        mjs  = cat.get("mjs_struct", "")
+        layout = cat.get("layout", "no_subclasses")
+        n_subtypes = len(cat.get("subtypes", []))
+        entries.append(
+            f'    {{ TEXT("{name}"), TEXT("{base}"), TEXT("{mjs}"), '
+            f'TEXT("{layout}"), {n_subtypes} }},'
+        )
+    content = (
+        f"{COPYRIGHT_BLOCK}\n"
+        f"#pragma once\n\n"
+        f"#include \"CoreMinimal.h\"\n\n"
+        f"// Compile-time registry of every URLab-managed MuJoCo category.\n"
+        f"// Codegen-owned — driven by codegen_rules.json[categories].\n"
+        f"namespace MjArticulationRegistry\n"
+        f"{{\n"
+        f"    struct CategoryEntry\n"
+        f"    {{\n"
+        f"        const TCHAR* Name;          // schema element name, e.g. \"body\"\n"
+        f"        const TCHAR* BaseClassName; // UE base UClass, e.g. \"UMjBody\"\n"
+        f"        const TCHAR* MjsStruct;     // MuJoCo spec struct, e.g. \"mjsBody\"\n"
+        f"        const TCHAR* Layout;        // \"single_uclass_per_file\" / \"multi_uclass\" / \"no_subclasses\"\n"
+        f"        int          NumSubtypes;\n"
+        f"    }};\n\n"
+        f"    inline constexpr CategoryEntry kCategories[] = {{\n"
+        + "\n".join(entries) + "\n"
+        f"    }};\n"
+        f"    inline constexpr int kNumCategories = sizeof(kCategories) / sizeof(kCategories[0]);\n"
+        f"}}\n"
+    )
+    out_path = os.path.join(ctx.public_root, out_relpath.replace("/", os.sep))
+    ctx.writes.append(FileWrite(path=out_path, content=content))
+
+
+def _emit_objtype_dispatch_block(cat_rules: Dict[str, Any]) -> Optional[str]:
+    """Emit a switch on ``cat_rules['objtype_dispatch']`` that assigns
+    each case's ``expr`` to ``target``. Used by categories where mjs's
+    objtype field varies by enum value (today: MjEquality)."""
+    dispatch = cat_rules.get("objtype_dispatch")
+    if not dispatch:
+        return None
+    enum_name = cat_rules.get("type_enum_name")
+    if not enum_name:
+        _diag_add(
+            "[diagnostic] objtype_dispatch on category missing "
+            "'type_enum_name'; cannot emit case labels.",
+            source="objtype_dispatch",
+        )
+        return None
+    discriminator = dispatch["discriminator"]
+    target = dispatch["target"]
+    default_expr = dispatch.get("default", "mjOBJ_UNKNOWN")
+    cases = dispatch.get("cases", [])
+    if not cases:
+        _diag_add(
+            f"[diagnostic] objtype_dispatch '{discriminator}' has no cases; "
+            f"only the default branch would emit.",
+            source="objtype_dispatch",
+        )
+        return None
+
+    switch_cases: List[Tuple[Any, str]] = []
+    for i, case in enumerate(cases):
+        keys = case.get("keys", [])
+        if not keys:
+            _diag_add(
+                f"[diagnostic] objtype_dispatch case #{i} has empty "
+                f"'keys' list; would emit a body with no case label.",
+                source="objtype_dispatch",
+            )
+            return None
+        if "expr" not in case:
+            _diag_add(
+                f"[diagnostic] objtype_dispatch case #{i} (keys={keys}) "
+                f"missing 'expr'.",
+                source="objtype_dispatch",
+            )
+            return None
+        switch_cases.append((list(keys), f"{target} = {case['expr']}"))
+    return emit_enum_switch(
+        discriminator, enum_name, switch_cases,
+        indent="    ",
+        default_body=f"{target} = {default_expr}",
+    )
+
+
+def _inject_into_category_cpp(
+    rules: Dict[str, Any],
+    private_root: str,
+    writes: List["FileWrite"],
+    tag: str,
+    emit_fn,
+    *,
+    diag_source: str,
+) -> None:
+    """Shared scaffolding: walk every category, call ``emit_fn(cat_rules)``,
+    inject the result between ``CODEGEN_<tag>_START/END`` markers in the
+    category's base-class .cpp. ``emit_fn`` returns ``None`` to skip
+    (already diagnosed inside the emit_fn). Missing-file / missing-marker
+    diagnostics + the brace-balance check come from
+    ``_inject_tags_into_cpp`` so every injection path shares the same
+    safety net."""
+    for cat_name, cat_rules in rules.get("categories", {}).items():
+        body = emit_fn(cat_rules)
+        if body is None:
+            continue
+        base_header = cat_rules.get("base_class_header")
+        if not base_header:
+            _diag_add(
+                f"[diagnostic] category '{cat_name}' has codegen rule for "
+                f"{diag_source} but no 'base_class_header'; block not emitted.",
+                source=diag_source,
+            )
+            continue
+        cpp_path = os.path.join(
+            private_root, base_header.replace(".h", ".cpp"),
+        )
+        _inject_tags_into_cpp(
+            cpp_path, [(tag, body)], writes, diag_source=diag_source,
+        )
+
+
+def _phase_objtype_dispatch(ctx: PhaseContext) -> None:
+    """Inject objtype dispatch block; host .cpp must carry
+    ``CODEGEN_OBJTYPE_DISPATCH_START/END`` markers."""
+    _inject_into_category_cpp(
+        ctx.rules, ctx.private_root, ctx.writes,
+        tag="OBJTYPE_DISPATCH",
+        emit_fn=_emit_objtype_dispatch_block,
+        diag_source="objtype_dispatch",
+    )
+
+
+def _emit_geom_final_type_block(cat_rules: Dict[str, Any],
+                                element_rules: Dict[str, Any],
+                                mjspec: Optional[Dict[str, Any]] = None) -> Optional[str]:
+    """Emit MjGeom's FinalType resolution + mesh-name export. Specific
+    to geom because the mesh-name write depends on the COMPILE-TIME geom
+    type (UPROPERTY Type if overridden, else Default's type).
+
+    The value_map is pulled from ``element_rules[xml_enum_ref]`` via
+    ``_resolve_value_map``. Returns None (with a diagnostic) when the
+    xml_enum_ref entry is missing — silent failure here would drop the
+    block from MjGeom.cpp and only surface as a runtime mesh-bind error.
+    """
+    final = cat_rules.get("geom_final_type")
+    if not final:
+        return None
+    xml_enum_attrs = element_rules.get("xml_enum_attrs", {})
+    attr_key = final["xml_enum_ref"]
+    enum_def = xml_enum_attrs.get(attr_key)
+    if not enum_def:
+        _diag_add(
+            f"[diagnostic] geom_final_type references xml_enum '{attr_key}' "
+            f"that doesn't exist in element_rules; FinalType block will be "
+            f"empty and mesh-name export will not fire.",
+            source="geom_final_type",
+        )
+        return None
+    value_map = _resolve_value_map(attr_key, enum_def)
+    if not value_map:
+        _diag_add(
+            f"[diagnostic] geom_final_type xml_enum '{attr_key}' resolved "
+            f"to an empty value_map; FinalType switch would have no cases.",
+            source="geom_final_type",
+        )
+        return None
+    ue_prop = enum_def["ue_property"]
+    ue_enum = enum_def["ue_enum_type"]
+
+    override = final["override_field"]
+    default_lookup = final["default_lookup"]
+    fallback = final["default_fallback"]
+    name_field = final["name_field"]
+    name_for = final["name_export_for"]
+    name_target = final["name_target"]
+    name_setter = final["name_setter"]
+
+    # Placeholder for the FinalType init when override=true: any mj_const
+    # works because the switch below remaps. Pick the value_map's first
+    # entry for stability.
+    first_mj = next(iter(value_map.values()))[1]
+
+    switch_cases: List[Tuple[Any, str]] = [
+        (mapping[0], f"FinalType = {mapping[1]}")
+        for mapping in value_map.values()
+    ]
+    switch_body = emit_enum_switch(
+        ue_prop, ue_enum, switch_cases, indent="        ",
+    ).rstrip("\n")
+    lines: List[str] = []
+    lines.append(f"    int FinalType = {override}")
+    lines.append(f"        ? {first_mj}  /* placeholder; remapped below */")
+    lines.append(f"        : (Default ? {default_lookup} : {fallback});")
+    lines.append(f"    if ({override})")
+    lines.append("    {")
+    lines.append(switch_body)
+    lines.append("    }")
+    lines.append(f"    if (FinalType == {name_for} && !{name_field}.IsEmpty())")
+    lines.append("    {")
+    lines.append(f"        {name_setter}({name_target}, {name_field});")
+    lines.append("    }")
+    return "\n".join(lines) + "\n"
+
+
+def _phase_geom_final_type(ctx: PhaseContext) -> None:
+    """Inject geom_final_type block into MjGeom.cpp between the
+    CODEGEN_GEOM_FINAL_TYPE markers."""
+    element_rules_all = ctx.rules.get("element_rules", {})
+
+    def emit_fn(cat_rules):
+        # Look up element_rules entry via the category key already used by
+        # rules.categories. The shared helper passes cat_rules through;
+        # we recover the category name from the dict identity.
+        cat_name = next(
+            (k for k, v in ctx.rules.get("categories", {}).items()
+             if v is cat_rules),
+            None,
+        )
+        if cat_name is None:
+            return None
+        return _emit_geom_final_type_block(
+            cat_rules, element_rules_all.get(cat_name, {}), ctx.mjspec,
+        )
+
+    _inject_into_category_cpp(
+        ctx.rules, ctx.private_root, ctx.writes,
+        tag="GEOM_FINAL_TYPE",
+        emit_fn=emit_fn,
+        diag_source="geom_final_type",
+    )
+
+
+def _phase_sensor_codegen(ctx: PhaseContext) -> None:
+    """Inject codegen-emitted sensor switch + TagToType into MjSensor.cpp."""
+    sensor_rules = ctx.rules.get("categories", {}).get("sensor")
+    if not sensor_rules:
+        return
+    sensor_per_type = ctx.schema.get("sensor_per_type", {})
+    if not sensor_per_type:
+        _diag_add(
+            "[diagnostic] sensor_per_type missing from schema snapshot; "
+            "skipping sensor switch codegen (run "
+            "build_mjcf_schema_snapshot.py).",
+            source="sensor_codegen",
         )
+        return
+    cpp_path = os.path.join(
+        ctx.private_root, "MuJoCo", "Components", "Sensors", "MjSensor.cpp",
+    )
+    _inject_tags_into_cpp(
+        cpp_path,
+        [
+            ("SENSOR_TYPE_SWITCH", _emit_sensor_switch_block(sensor_rules, sensor_per_type)),
+            ("SENSOR_TAG_TO_TYPE", _emit_sensor_tag_to_type_block(sensor_rules)),
+        ],
+        ctx.writes,
+        diag_source="sensor_codegen",
+    )
+
+
+_KNOWN_LAYOUTS = {"single_uclass_per_file", "multi_uclass", "no_subclasses"}
+
+
+def _phase_categories(ctx: PhaseContext) -> None:
+    rules, schema = ctx.rules, ctx.schema
+    public_root, private_root = ctx.public_root, ctx.private_root
+    mjspec = ctx.mjspec
+    categories: Dict[str, Any] = rules.get("categories", {})
+    for cat, cat_rules in categories.items():
+        layout = cat_rules.get("layout", "no_subclasses")
+        if layout == "single_uclass_per_file":
+            ctx.writes.extend(emit_base_class_injection(cat, cat_rules, schema, rules, public_root, private_root, mjspec=mjspec))
+            ctx.writes.extend(emit_subclass_files(cat, cat_rules, schema, rules, public_root, private_root, mjspec=mjspec))
+        elif layout == "multi_uclass":
+            ctx.writes.extend(emit_multi_uclass(cat, cat_rules, schema, rules, public_root, private_root, mjspec=mjspec))
+        elif layout == "no_subclasses":
+            ctx.writes.extend(emit_base_class_injection(cat, cat_rules, schema, rules, public_root, private_root, mjspec=mjspec))
+        else:
+            _diag_add(
+                f"[diagnostic] category '{cat}' has unknown layout "
+                f"'{layout}'; expected one of {sorted(_KNOWN_LAYOUTS)}. "
+                f"Category emitted no writes.",
+                source="category_layout",
+            )
+
+
+def _phase_bind_h(ctx: PhaseContext) -> None:
+    bind_write = emit_bind_h_injection(ctx.rules, ctx.mjxmacro, ctx.bind_h_path)
+    if bind_write:
+        ctx.writes.append(bind_write)
+
+
+def _phase_schema_tests(ctx: PhaseContext) -> None:
+    ctx.writes.append(emit_schema_for_tests(ctx.schema, ctx.rules))
+
+
+# The 17 drift checks, _emit_drift_diagnostics, the hand-enum regexes,
+# _scan_hand_enums, _hand_enums_from_snapshot, _all_schema_attrs,
+# _collect_every_schema_attr, and _VALID_APPLY_MODES live in
+# _codegen_checks. The orchestrator re-exports them through the import
+# block at the top of this file.
+
+
+def _phase_diagnostics(ctx: PhaseContext) -> None:
+    rules, schema, mjspec, mjxmacro = ctx.rules, ctx.schema, ctx.mjspec, ctx.mjxmacro
+    _emit_drift_diagnostics(schema, rules, mjspec)
+    _check_hand_enum_drift(schema, rules, mjspec, ctx.public_root)
+    _check_type_shape_drift(schema, rules, mjspec)
+    _check_new_attr_typing(schema, rules)
+    _check_value_map_stale_mj_consts(rules, mjspec)
+    _check_orphan_rule_entries(schema, rules)
+    _check_compiler_attrs_coverage(schema, rules)
+    _check_mjxmacro_block_coverage(rules, mjxmacro)
+    _check_property_units_validity(rules)
+    _check_apply_mode_validity(rules)
+    _check_embedded_cpp_references(rules, mjspec)
+    _check_allowlist_staleness(schema, rules, mjspec)
+
+
+# Order matters: emit phases collect writes; diagnostics runs last and
+# scans the final rule set.
+EMISSION_PHASES: List[EmissionPhase] = [
+    EmissionPhase(name="categories",            fn=_phase_categories),
+    EmissionPhase(name="synthetic_categories",  fn=_phase_synthetic_categories),
+    EmissionPhase(name="generated_enums",       fn=_phase_generated_enums),
+    EmissionPhase(name="editor_option_helpers", fn=_phase_editor_option_helpers),
+    EmissionPhase(name="articulation_registry", fn=_phase_articulation_registry),
+    EmissionPhase(name="sensor_codegen",        fn=_phase_sensor_codegen),
+    EmissionPhase(name="objtype_dispatch",      fn=_phase_objtype_dispatch),
+    EmissionPhase(name="geom_final_type",       fn=_phase_geom_final_type),
+    EmissionPhase(name="bind_h",                fn=_phase_bind_h),
+    EmissionPhase(name="schema_tests",          fn=_phase_schema_tests),
+    EmissionPhase(name="diagnostics",           fn=_phase_diagnostics),
+]
+
+
+def collect_all_writes(
+    schema: Dict[str, Any],
+    rules: Dict[str, Any],
+    mjxmacro: Dict[str, Any],
+    public_root: str,
+    private_root: str,
+    bind_h_path: str,
+    mjspec: Dict[str, Any] | None = None,
+) -> List[FileWrite]:
+    """Drive every phase in ``EMISSION_PHASES`` and return the full set
+    of file writes the codegen would perform. No I/O side effects on
+    disk — diagnostics print to stderr at the end."""
+    _reset_diags()
+    writes: List[FileWrite] = []
+    ctx = PhaseContext(
+        schema=schema, rules=rules, mjxmacro=mjxmacro, mjspec=mjspec,
+        public_root=public_root, private_root=private_root,
+        bind_h_path=bind_h_path, writes=writes,
+    )
+    for phase in EMISSION_PHASES:
+        phase.fn(ctx)
+    _diag_flush()
+    return writes
+
+
+def _mjspec_from_introspect(
+    introspect: Dict[str, Any],
+) -> Dict[str, Any]:
+    """Project the libclang-scraped introspect snapshot into the
+    shape downstream code expects:
+
+      ``structs``: ``{struct_name: [field_name, ...]}``
+      ``enums``:   ``{enum_name: {member_name: int_value}}``
+      ``setto_functions``: ``{fn_name: {"params": [...], "return_type": ...}}``
+      ``introspect``: passthrough of the richer per-field tables for
+                      the drift checks that need them.
+
+    Every ``mjs_setTo*`` C signature takes the spec element as its
+    first arg; the codegen emit path always passes ``Element`` there,
+    so the projection drops that arg from the param list and the
+    downstream emitter enumerates only the schema-attr-bound trailing
+    args."""
+    structs_in = introspect.get("structs", {}) or {}
+    structs_out: Dict[str, List[str]] = {}
+    for name, entry in structs_in.items():
+        if isinstance(entry, dict):
+            fields = entry.get("fields", [])
+            structs_out[name] = [
+                f["name"] for f in fields
+                if isinstance(f, dict) and isinstance(f.get("name"), str)
+            ]
+
+    enums_in = introspect.get("enums", {}) or {}
+    enums_out: Dict[str, Dict[str, int]] = {}
+    for name, entry in enums_in.items():
+        if isinstance(entry, dict):
+            members = entry.get("members", {})
+            if isinstance(members, dict):
+                enums_out[name] = dict(members)
+
+    fns_in = introspect.get("functions", {}) or {}
+    setto_out: Dict[str, Dict[str, Any]] = {}
+    for name, sig in fns_in.items():
+        if not name.startswith("mjs_setTo"):
+            continue
+        if isinstance(sig, dict):
+            params = sig.get("params", [])
+            setto_out[name] = {
+                "params": params[1:] if params else [],
+                "return_type": sig.get("return_type", ""),
+            }
+
+    return {
+        "structs":          structs_out,
+        "enums":            enums_out,
+        "setto_functions":  setto_out,
+        "introspect": {
+            "functions":  fns_in,
+            "structs":    structs_in,
+            "defines":    introspect.get("defines", {}),
+            "hand_enums": introspect.get("hand_enums", {}),
+        },
+    }
 
 
 def apply_writes(writes: List[FileWrite], dry_run: bool, check_only: bool) -> int:
-    """Either apply, print diff, or check. Returns process exit code."""
+    """Either apply, print diff, or check. Returns process exit code.
+
+    Writes use ``newline="\\n"`` and a binary compare so a codegen run on
+    Windows produces the same bytes as one on Linux — otherwise Python's
+    default newline translation would silently bake ``\\r\\n`` into the
+    on-disk artifacts and ``--check`` would still pass because the
+    matching read would normalize it back."""
     changed = 0
     for w in writes:
-        new = w.content
-        old = None
+        new_bytes = w.content.encode("utf-8")
+        old_bytes: Optional[bytes] = None
         if os.path.exists(w.path):
-            with open(w.path, "r", encoding="utf-8") as f:
-                old = f.read()
-        if old == new:
+            with open(w.path, "rb") as f:
+                old_bytes = f.read()
+        if old_bytes == new_bytes:
             continue
         changed += 1
         rel = os.path.relpath(w.path, PLUGIN_ROOT)
         if dry_run or check_only:
-            print(f"==> WOULD WRITE: {rel} ({len(new)} bytes)")
-            if old is not None and dry_run:
+            print(f"==> WOULD WRITE: {rel} ({len(new_bytes)} bytes)")
+            if old_bytes is not None and dry_run:
+                old_text = old_bytes.decode("utf-8", errors="replace")
                 diff = difflib.unified_diff(
-                    old.splitlines(keepends=True),
-                    new.splitlines(keepends=True),
+                    old_text.splitlines(keepends=True),
+                    w.content.splitlines(keepends=True),
                     fromfile=rel + " (current)",
                     tofile=rel + " (codegen)",
                     n=2,
                 )
                 sys.stdout.writelines(diff)
         else:
+            # Atomic write: stage to a sibling tmp file, then os.replace.
+            # Without this, a Ctrl-C between truncate and write leaves a
+            # zero-/partial-byte file that UBT picks up, producing a UHT
+            # error far from "I interrupted codegen".
             os.makedirs(os.path.dirname(w.path), exist_ok=True)
-            with open(w.path, "w", encoding="utf-8") as f:
-                f.write(new)
+            tmp_path = w.path + ".codegen.tmp"
+            try:
+                with open(tmp_path, "wb") as f:
+                    f.write(new_bytes)
+                os.replace(tmp_path, w.path)
+            except BaseException:
+                if os.path.exists(tmp_path):
+                    try:
+                        os.remove(tmp_path)
+                    except OSError:
+                        pass
+                raise
             print(f"WROTE {rel}")
     if check_only and changed:
         return 1
@@ -2368,17 +4033,68 @@ def apply_writes(writes: List[FileWrite], dry_run: bool, check_only: bool) -> in
     return 0
 
 
+# Hand-written companion files that legitimately live in MuJoCo/Generated/
+# (their banner explicitly says "NOT codegen-managed"). Listed here so the
+# orphan walk doesn't flag them — anything else in Generated/ that the
+# codegen doesn't plan to write IS a real orphan.
+_HAND_WRITTEN_IN_GENERATED = {
+    "MjOptionGeneratedExtras.cpp",
+}
+
+
+def find_orphan_generated_files(
+    writes: List[FileWrite], public_root: str, private_root: str,
+) -> List[str]:
+    """Walk ``MuJoCo/Generated/`` under public + private roots and return
+    any file the codegen no longer plans to emit. A category removed from
+    rules leaves a stale ``Generated/MjFoo.h`` that UBT will pick up — the
+    user sees a confusing UHT error far from the JSON edit."""
+    expected = {os.path.normcase(os.path.abspath(w.path)) for w in writes}
+    orphans: List[str] = []
+    for root in (public_root, private_root):
+        gen_dir = os.path.join(root, "MuJoCo", "Generated")
+        if not os.path.isdir(gen_dir):
+            continue
+        for dirpath, _dirs, files in os.walk(gen_dir):
+            for fname in files:
+                if not (fname.endswith(".h") or fname.endswith(".cpp")):
+                    continue
+                if fname in _HAND_WRITTEN_IN_GENERATED:
+                    continue
+                p = os.path.normcase(os.path.abspath(os.path.join(dirpath, fname)))
+                if p not in expected:
+                    orphans.append(os.path.join(dirpath, fname))
+    return sorted(orphans)
+
+
 def main() -> int:
     parser = argparse.ArgumentParser(description="URLab UE component codegen.")
     parser.add_argument("--schema", default=DEFAULT_SCHEMA)
     parser.add_argument("--mjxmacro", default=DEFAULT_MJXMACRO)
-    parser.add_argument("--mjspec", default=DEFAULT_MJSPEC)
+    parser.add_argument("--introspect", default=DEFAULT_INTROSPECT,
+                        help="Clang-AST scrape carrying mjsX struct fields, "
+                             "mjt* enums, mjs_setTo* signatures, and EMj* "
+                             "hand-enum members. Built by "
+                             "build_introspect_snapshot.py (requires libclang).")
     parser.add_argument("--rules", default=DEFAULT_RULES)
     parser.add_argument("--public-root", default=DEFAULT_PUBLIC_ROOT)
     parser.add_argument("--private-root", default=DEFAULT_PRIVATE_ROOT)
     parser.add_argument("--bind-h", default=DEFAULT_BIND_H)
-    parser.add_argument("--dry-run", action="store_true", help="print diffs, write nothing")
-    parser.add_argument("--check", action="store_true", help="non-zero exit if writes would change anything")
+    parser.add_argument("--dry-run", action="store_true",
+                        help="print diffs, write nothing")
+    parser.add_argument("--check", action="store_true",
+                        help="non-zero exit if writes would change anything")
+    parser.add_argument("--strict", dest="strict",
+                        action=argparse.BooleanOptionalAction, default=True,
+                        help="treat drift diagnostics as fatal (exit 2). "
+                             "Default on; --no-strict makes diagnostics "
+                             "print but not fail the run.")
+    parser.add_argument("--require-introspect", dest="require_introspect",
+                        action=argparse.BooleanOptionalAction, default=True,
+                        help="hard-fail when the clang-AST introspect "
+                             "snapshot is missing. Default on; "
+                             "--no-require-introspect lets the codegen run "
+                             "against an existing (possibly stale) snapshot.")
     args = parser.parse_args()
 
     with open(args.schema, "r", encoding="utf-8") as f:
@@ -2387,13 +4103,90 @@ def main() -> int:
         mjxmacro = json.load(f)
     with open(args.rules, "r", encoding="utf-8") as f:
         rules = json.load(f)
+
     mjspec: Dict[str, Any] | None = None
-    if os.path.exists(args.mjspec):
-        with open(args.mjspec, "r", encoding="utf-8") as f:
-            mjspec = json.load(f)
+    if not os.path.exists(args.introspect):
+        msg = (
+            f"[codegen] introspect snapshot missing at {args.introspect}. "
+            f"Run `python Scripts/codegen/build_introspect_snapshot.py` "
+            f"(requires libclang). Pass --no-require-introspect to bypass."
+        )
+        if args.require_introspect:
+            print(msg, file=sys.stderr)
+            return 3
+        print(f"[codegen] warning: {msg}", file=sys.stderr)
+    else:
+        with open(args.introspect, "r", encoding="utf-8") as f:
+            introspect = json.load(f)
+        mjspec = _mjspec_from_introspect(introspect)
+        # Post-projection invariants: a libclang scrape shape change can
+        # silently empty the projection, which would neuter the type-shape
+        # drift check and drop TArray array-cap clamps from emitted code.
+        # Fail loud rather than ship a degraded but green-on---check run.
+        if args.require_introspect:
+            for key in ("enums", "structs", "setto_functions"):
+                if not mjspec.get(key):
+                    print(
+                        f"[codegen] introspect projection produced empty "
+                        f"'{key}'. The libclang scrape shape may have "
+                        f"drifted — rebuild the snapshot or fix "
+                        f"_mjspec_from_introspect.",
+                        file=sys.stderr,
+                    )
+                    return 3
 
-    writes = collect_all_writes(schema, rules, mjxmacro, args.public_root, args.private_root, args.bind_h, mjspec=mjspec)
-    return apply_writes(writes, args.dry_run, args.check)
+    writes = collect_all_writes(
+        schema, rules, mjxmacro,
+        args.public_root, args.private_root, args.bind_h, mjspec=mjspec,
+    )
+
+    orphans = find_orphan_generated_files(
+        writes, args.public_root, args.private_root,
+    )
+    for orphan in orphans:
+        rel = os.path.relpath(orphan, PLUGIN_ROOT)
+        _diag_add(
+            f"[diagnostic] orphan generated file: {rel}. The codegen no "
+            f"longer plans to emit this path — a category or subtype was "
+            f"likely removed from codegen_rules.json. Delete the file "
+            f"(UBT will otherwise pick it up).",
+            source="orphan_generated_file",
+        )
+    # Snapshot the diag SOURCE tags before flushing, so we can detect
+    # destructive-overwrite warnings (banner_overwrite) and refuse to
+    # write under --strict — otherwise apply_writes would destroy the
+    # hand edits before the strict check fires.
+    banner_overwrite_pending = any(
+        d.source == "banner_overwrite" for d in _DIAGS_BUFFER.pending
+    )
+    _diag_flush()
+
+    fired = _DIAGS_BUFFER.fired_count
+    is_destructive_run = not args.dry_run and not args.check
+    if is_destructive_run and args.strict and banner_overwrite_pending:
+        print(
+            f"[codegen] --strict: banner_overwrite diagnostic fired — "
+            f"refusing to write (would destroy hand-edited content). "
+            f"Re-run with --no-strict to proceed, or run the audit "
+            f"helper at Scripts/codegen/_audit_banner_candidates.py.",
+            file=sys.stderr,
+        )
+        return 2
+
+    rc = apply_writes(writes, args.dry_run, args.check)
+    if args.check and orphans:
+        print(
+            f"[codegen] --check: {len(orphans)} orphan generated file(s); "
+            f"exiting non-zero.", file=sys.stderr,
+        )
+        return 1
+    if args.strict and fired:
+        print(
+            f"[codegen] --strict: {fired} diagnostic(s) flushed; "
+            f"exiting non-zero.", file=sys.stderr,
+        )
+        return 2
+    return rc
 
 
 if __name__ == "__main__":
diff --git a/Scripts/codegen/regen_all.py b/Scripts/codegen/regen_all.py
index 2d5c9e9..ab3c622 100644
--- a/Scripts/codegen/regen_all.py
+++ b/Scripts/codegen/regen_all.py
@@ -4,12 +4,16 @@
 Runs every codegen step in order:
 
   1. Rebuild snapshots from the installed MuJoCo headers + submodule source:
-     - Scripts/mjspec_snapshot.json   (mjsX struct fields + mjs_setTo* sigs)
-     - Scripts/mjxmacro_snapshot.json (model/data array layouts)
-     - Scripts/mjcf_schema_snapshot.json (MJCF schema, per element)
+     - Scripts/codegen/snapshots/mjxmacro_snapshot.json (model/data array layouts)
+     - Scripts/codegen/snapshots/mjcf_schema_snapshot.json (MJCF schema + per-sensor objtype scrape)
+     - Scripts/codegen/snapshots/introspect_snapshot.json (clang-AST scrape — mjsX struct fields, mjt* enums, mjs_setTo* sigs, EMj* hand enums)
   2. Run the C++ generator (generate_ue_components.py) against the fresh
      snapshots. Emits / updates the per-component .h/.cpp files in Source/.
 
+The clang-AST introspect snapshot now supersedes the old
+``mjspec_snapshot.json`` — the runtime projects introspect into the
+legacy mjspec shape, so build_mjspec_snapshot.py is retired.
+
 Day-to-day this is the script developers run after either:
   - bumping the third_party/MuJoCo submodule pointer + rebuilding the
     install (build_all.ps1 / build_all.sh), OR
@@ -58,13 +62,22 @@ def main(argv: list[str] | None = None) -> int:
     py = sys.executable
 
     steps: list[tuple[str, list[str]]] = [
-        ("mjspec snapshot",
-            [py, os.path.join(HERE, "build_mjspec_snapshot.py")]),
         ("mjxmacro snapshot",
             [py, os.path.join(HERE, "build_mjxmacro_snapshot.py")]),
         ("MJCF schema snapshot",
             [py, os.path.join(HERE, "build_mjcf_schema_snapshot.py")]),
     ]
+    # Introspect snapshot needs libclang and now also supplies the data
+    # build_mjspec_snapshot.py used to scrape via regex (mjsX struct
+    # fields, mjt* enums, mjs_setTo* sigs). Optional only in the sense
+    # that a stale on-disk snapshot still drives the codegen — the
+    # --require-introspect default in generate_ue_components.py makes
+    # the gate fatal at next run if the snapshot really is missing.
+    introspect_step = (
+        "introspect snapshot (clang AST — supersedes mjspec snapshot)",
+        [py, os.path.join(HERE, "build_introspect_snapshot.py")],
+    )
+    steps.append(introspect_step)
     if not args.skip_codegen:
         steps.append(("UE component codegen",
             [py, os.path.join(HERE, "generate_ue_components.py")]))
@@ -72,6 +85,16 @@ def main(argv: list[str] | None = None) -> int:
     for label, cmd in steps:
         rc = _run(label, cmd)
         if rc != 0:
+            # Introspect is optional — warn but continue when it's the
+            # one that failed (most commonly: libclang not on PATH).
+            if label.startswith("introspect snapshot"):
+                print(
+                    "    (introspect is optional; codegen will use the "
+                    "existing snapshot — type-shape drift may be stale "
+                    "until you install libclang.)",
+                    flush=True,
+                )
+                continue
             return rc
 
     print("\nAll regen steps completed.", flush=True)
diff --git a/Scripts/codegen/snapshots/introspect_snapshot.json b/Scripts/codegen/snapshots/introspect_snapshot.json
new file mode 100644
index 0000000..4ff937b
--- /dev/null
+++ b/Scripts/codegen/snapshots/introspect_snapshot.json
@@ -0,0 +1,26735 @@
+{
+  "_meta": {
+    "snapshot_version": 1,
+    "header_hash": "e4e41be1f2a1615ec30345189a81a6dc7c58c113a262d2478167f67a4d117311"
+  },
+  "functions": {
+    "mj_defaultVFS": {
+      "name": "mj_defaultVFS",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "vfs",
+          "c_type": "mjVFS *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 79
+    },
+    "mj_mountVFS": {
+      "name": "mj_mountVFS",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "vfs",
+          "c_type": "mjVFS *",
+          "array_dim": null
+        },
+        {
+          "name": "filepath",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "provider",
+          "c_type": "const mjpResourceProvider *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 83
+    },
+    "mj_unmountVFS": {
+      "name": "mj_unmountVFS",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "vfs",
+          "c_type": "mjVFS *",
+          "array_dim": null
+        },
+        {
+          "name": "filename",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 86
+    },
+    "mj_addFileVFS": {
+      "name": "mj_addFileVFS",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "vfs",
+          "c_type": "mjVFS *",
+          "array_dim": null
+        },
+        {
+          "name": "directory",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "filename",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 89
+    },
+    "mj_addBufferVFS": {
+      "name": "mj_addBufferVFS",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "vfs",
+          "c_type": "mjVFS *",
+          "array_dim": null
+        },
+        {
+          "name": "name",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "buffer",
+          "c_type": "const void *",
+          "array_dim": null
+        },
+        {
+          "name": "nbuffer",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 92
+    },
+    "mj_deleteFileVFS": {
+      "name": "mj_deleteFileVFS",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "vfs",
+          "c_type": "mjVFS *",
+          "array_dim": null
+        },
+        {
+          "name": "filename",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 95
+    },
+    "mj_containsBufferVFS": {
+      "name": "mj_containsBufferVFS",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "vfs",
+          "c_type": "mjVFS *",
+          "array_dim": null
+        },
+        {
+          "name": "name",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 98
+    },
+    "mj_containsFileVFS": {
+      "name": "mj_containsFileVFS",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "vfs",
+          "c_type": "mjVFS *",
+          "array_dim": null
+        },
+        {
+          "name": "directory",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "filename",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 101
+    },
+    "mj_deleteVFS": {
+      "name": "mj_deleteVFS",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "vfs",
+          "c_type": "mjVFS *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 104
+    },
+    "mj_getCacheSize": {
+      "name": "mj_getCacheSize",
+      "return_type": "size_t",
+      "params": [
+        {
+          "name": "cache",
+          "c_type": "const mjCache *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 109
+    },
+    "mj_getCacheCapacity": {
+      "name": "mj_getCacheCapacity",
+      "return_type": "size_t",
+      "params": [
+        {
+          "name": "cache",
+          "c_type": "const mjCache *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 112
+    },
+    "mj_setCacheCapacity": {
+      "name": "mj_setCacheCapacity",
+      "return_type": "size_t",
+      "params": [
+        {
+          "name": "cache",
+          "c_type": "mjCache *",
+          "array_dim": null
+        },
+        {
+          "name": "size",
+          "c_type": "size_t",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 115
+    },
+    "mj_getCache": {
+      "name": "mj_getCache",
+      "return_type": "mjCache *",
+      "params": [],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 118
+    },
+    "mj_clearCache": {
+      "name": "mj_clearCache",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "cache",
+          "c_type": "mjCache *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 121
+    },
+    "mj_loadXML": {
+      "name": "mj_loadXML",
+      "return_type": "mjModel *",
+      "params": [
+        {
+          "name": "filename",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "vfs",
+          "c_type": "const mjVFS *",
+          "array_dim": null
+        },
+        {
+          "name": "error",
+          "c_type": "char *",
+          "array_dim": null
+        },
+        {
+          "name": "error_sz",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 129
+    },
+    "mj_parseXML": {
+      "name": "mj_parseXML",
+      "return_type": "mjSpec *",
+      "params": [
+        {
+          "name": "filename",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "vfs",
+          "c_type": "const mjVFS *",
+          "array_dim": null
+        },
+        {
+          "name": "error",
+          "c_type": "char *",
+          "array_dim": null
+        },
+        {
+          "name": "error_sz",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 133
+    },
+    "mj_parseXMLString": {
+      "name": "mj_parseXMLString",
+      "return_type": "mjSpec *",
+      "params": [
+        {
+          "name": "xml",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "vfs",
+          "c_type": "const mjVFS *",
+          "array_dim": null
+        },
+        {
+          "name": "error",
+          "c_type": "char *",
+          "array_dim": null
+        },
+        {
+          "name": "error_sz",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 137
+    },
+    "mj_parse": {
+      "name": "mj_parse",
+      "return_type": "mjSpec *",
+      "params": [
+        {
+          "name": "filename",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "content_type",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "vfs",
+          "c_type": "const mjVFS *",
+          "array_dim": null
+        },
+        {
+          "name": "error",
+          "c_type": "char *",
+          "array_dim": null
+        },
+        {
+          "name": "error_sz",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 141
+    },
+    "mj_encode": {
+      "name": "mj_encode",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "const mjSpec *",
+          "array_dim": null
+        },
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "filename",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "content_type",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "vfs",
+          "c_type": "const mjVFS *",
+          "array_dim": null
+        },
+        {
+          "name": "error",
+          "c_type": "char *",
+          "array_dim": null
+        },
+        {
+          "name": "error_sz",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 147
+    },
+    "mj_compile": {
+      "name": "mj_compile",
+      "return_type": "mjModel *",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "mjSpec *",
+          "array_dim": null
+        },
+        {
+          "name": "vfs",
+          "c_type": "const mjVFS *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 153
+    },
+    "mj_copyBack": {
+      "name": "mj_copyBack",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "mjSpec *",
+          "array_dim": null
+        },
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 156
+    },
+    "mj_recompile": {
+      "name": "mj_recompile",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "mjSpec *",
+          "array_dim": null
+        },
+        {
+          "name": "vfs",
+          "c_type": "const mjVFS *",
+          "array_dim": null
+        },
+        {
+          "name": "m",
+          "c_type": "mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 160
+    },
+    "mj_saveLastXML": {
+      "name": "mj_saveLastXML",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "filename",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "error",
+          "c_type": "char *",
+          "array_dim": null
+        },
+        {
+          "name": "error_sz",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 165
+    },
+    "mj_freeLastXML": {
+      "name": "mj_freeLastXML",
+      "return_type": "void",
+      "params": [],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 168
+    },
+    "mj_saveXMLString": {
+      "name": "mj_saveXMLString",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "const mjSpec *",
+          "array_dim": null
+        },
+        {
+          "name": "xml",
+          "c_type": "char *",
+          "array_dim": null
+        },
+        {
+          "name": "xml_sz",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "error",
+          "c_type": "char *",
+          "array_dim": null
+        },
+        {
+          "name": "error_sz",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 173
+    },
+    "mj_saveXML": {
+      "name": "mj_saveXML",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "const mjSpec *",
+          "array_dim": null
+        },
+        {
+          "name": "filename",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "error",
+          "c_type": "char *",
+          "array_dim": null
+        },
+        {
+          "name": "error_sz",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 177
+    },
+    "mju_getXMLDependencies": {
+      "name": "mju_getXMLDependencies",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "filename",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "dependencies",
+          "c_type": "mjStringVec *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 181
+    },
+    "mj_step": {
+      "name": "mj_step",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 186
+    },
+    "mj_step1": {
+      "name": "mj_step1",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 189
+    },
+    "mj_step2": {
+      "name": "mj_step2",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 192
+    },
+    "mj_forward": {
+      "name": "mj_forward",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 195
+    },
+    "mj_inverse": {
+      "name": "mj_inverse",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 198
+    },
+    "mj_forwardSkip": {
+      "name": "mj_forwardSkip",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "skipstage",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "skipsensor",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 201
+    },
+    "mj_inverseSkip": {
+      "name": "mj_inverseSkip",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "skipstage",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "skipsensor",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 204
+    },
+    "mj_defaultLROpt": {
+      "name": "mj_defaultLROpt",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "opt",
+          "c_type": "mjLROpt *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 210
+    },
+    "mj_defaultSolRefImp": {
+      "name": "mj_defaultSolRefImp",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "solref",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "solimp",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 214
+    },
+    "mj_defaultOption": {
+      "name": "mj_defaultOption",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "opt",
+          "c_type": "mjOption *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 217
+    },
+    "mj_defaultVisual": {
+      "name": "mj_defaultVisual",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "vis",
+          "c_type": "mjVisual *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 220
+    },
+    "mj_copyModel": {
+      "name": "mj_copyModel",
+      "return_type": "mjModel *",
+      "params": [
+        {
+          "name": "dest",
+          "c_type": "mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "src",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 224
+    },
+    "mj_saveModel": {
+      "name": "mj_saveModel",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "filename",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "buffer",
+          "c_type": "void *",
+          "array_dim": null
+        },
+        {
+          "name": "buffer_sz",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 228
+    },
+    "mj_loadModel": {
+      "name": "mj_loadModel",
+      "return_type": "mjModel *",
+      "params": [
+        {
+          "name": "filename",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "vfs",
+          "c_type": "const mjVFS *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 233
+    },
+    "mj_loadModelBuffer": {
+      "name": "mj_loadModelBuffer",
+      "return_type": "mjModel *",
+      "params": [
+        {
+          "name": "buffer",
+          "c_type": "const void *",
+          "array_dim": null
+        },
+        {
+          "name": "buffer_sz",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 236
+    },
+    "mj_deleteModel": {
+      "name": "mj_deleteModel",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "mjModel *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 239
+    },
+    "mj_sizeModel": {
+      "name": "mj_sizeModel",
+      "return_type": "mjtSize",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 242
+    },
+    "mj_makeData": {
+      "name": "mj_makeData",
+      "return_type": "mjData *",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 246
+    },
+    "mj_copyData": {
+      "name": "mj_copyData",
+      "return_type": "mjData *",
+      "params": [
+        {
+          "name": "dest",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "src",
+          "c_type": "const mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 250
+    },
+    "mjv_copyData": {
+      "name": "mjv_copyData",
+      "return_type": "mjData *",
+      "params": [
+        {
+          "name": "dest",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "src",
+          "c_type": "const mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 253
+    },
+    "mj_resetData": {
+      "name": "mj_resetData",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 256
+    },
+    "mj_resetDataDebug": {
+      "name": "mj_resetDataDebug",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "debug_value",
+          "c_type": "unsigned char",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 259
+    },
+    "mj_resetDataKeyframe": {
+      "name": "mj_resetDataKeyframe",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "key",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 262
+    },
+    "mj_markStack": {
+      "name": "mj_markStack",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 267
+    },
+    "mj_freeStack": {
+      "name": "mj_freeStack",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 271
+    },
+    "mj_stackAllocByte": {
+      "name": "mj_stackAllocByte",
+      "return_type": "void *",
+      "params": [
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "bytes",
+          "c_type": "size_t",
+          "array_dim": null
+        },
+        {
+          "name": "alignment",
+          "c_type": "size_t",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 277
+    },
+    "mj_stackAllocNum": {
+      "name": "mj_stackAllocNum",
+      "return_type": "mjtNum *",
+      "params": [
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "size",
+          "c_type": "size_t",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 280
+    },
+    "mj_stackAllocInt": {
+      "name": "mj_stackAllocInt",
+      "return_type": "int *",
+      "params": [
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "size",
+          "c_type": "size_t",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 283
+    },
+    "mj_deleteData": {
+      "name": "mj_deleteData",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 286
+    },
+    "mj_resetCallbacks": {
+      "name": "mj_resetCallbacks",
+      "return_type": "void",
+      "params": [],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 289
+    },
+    "mj_setConst": {
+      "name": "mj_setConst",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 292
+    },
+    "mj_setLengthRange": {
+      "name": "mj_setLengthRange",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "index",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "opt",
+          "c_type": "const mjLROpt *",
+          "array_dim": null
+        },
+        {
+          "name": "error",
+          "c_type": "char *",
+          "array_dim": null
+        },
+        {
+          "name": "error_sz",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 296
+    },
+    "mj_makeSpec": {
+      "name": "mj_makeSpec",
+      "return_type": "mjSpec *",
+      "params": [],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 300
+    },
+    "mj_copySpec": {
+      "name": "mj_copySpec",
+      "return_type": "mjSpec *",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "const mjSpec *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 303
+    },
+    "mj_deleteSpec": {
+      "name": "mj_deleteSpec",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "mjSpec *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 306
+    },
+    "mjs_activatePlugin": {
+      "name": "mjs_activatePlugin",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "mjSpec *",
+          "array_dim": null
+        },
+        {
+          "name": "name",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 309
+    },
+    "mjs_setDeepCopy": {
+      "name": "mjs_setDeepCopy",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "mjSpec *",
+          "array_dim": null
+        },
+        {
+          "name": "deepcopy",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 312
+    },
+    "mj_printFormattedModel": {
+      "name": "mj_printFormattedModel",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "filename",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "float_format",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 319
+    },
+    "mj_printModel": {
+      "name": "mj_printModel",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "filename",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 322
+    },
+    "mj_printFormattedData": {
+      "name": "mj_printFormattedData",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "const mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "filename",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "float_format",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 326
+    },
+    "mj_printData": {
+      "name": "mj_printData",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "const mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "filename",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 330
+    },
+    "mju_printMat": {
+      "name": "mju_printMat",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "mat",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "nr",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "nc",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 333
+    },
+    "mju_printMatSparse": {
+      "name": "mju_printMatSparse",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "mat",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "nr",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "rownnz",
+          "c_type": "const int *",
+          "array_dim": null
+        },
+        {
+          "name": "rowadr",
+          "c_type": "const int *",
+          "array_dim": null
+        },
+        {
+          "name": "colind",
+          "c_type": "const int *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 336
+    },
+    "mj_printSchema": {
+      "name": "mj_printSchema",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "filename",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "buffer",
+          "c_type": "char *",
+          "array_dim": null
+        },
+        {
+          "name": "buffer_sz",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "flg_html",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "flg_pad",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 340
+    },
+    "mj_printScene": {
+      "name": "mj_printScene",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "const mjvScene *",
+          "array_dim": null
+        },
+        {
+          "name": "filename",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 344
+    },
+    "mj_printFormattedScene": {
+      "name": "mj_printFormattedScene",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "const mjvScene *",
+          "array_dim": null
+        },
+        {
+          "name": "filename",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "float_format",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 348
+    },
+    "mj_fwdKinematics": {
+      "name": "mj_fwdKinematics",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 355
+    },
+    "mj_fwdPosition": {
+      "name": "mj_fwdPosition",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 358
+    },
+    "mj_fwdVelocity": {
+      "name": "mj_fwdVelocity",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 361
+    },
+    "mj_fwdActuation": {
+      "name": "mj_fwdActuation",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 364
+    },
+    "mj_fwdAcceleration": {
+      "name": "mj_fwdAcceleration",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 367
+    },
+    "mj_fwdConstraint": {
+      "name": "mj_fwdConstraint",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 370
+    },
+    "mj_Euler": {
+      "name": "mj_Euler",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 373
+    },
+    "mj_RungeKutta": {
+      "name": "mj_RungeKutta",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "N",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 376
+    },
+    "mj_implicit": {
+      "name": "mj_implicit",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 379
+    },
+    "mj_invPosition": {
+      "name": "mj_invPosition",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 382
+    },
+    "mj_invVelocity": {
+      "name": "mj_invVelocity",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 385
+    },
+    "mj_invConstraint": {
+      "name": "mj_invConstraint",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 388
+    },
+    "mj_compareFwdInv": {
+      "name": "mj_compareFwdInv",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 391
+    },
+    "mj_sensorPos": {
+      "name": "mj_sensorPos",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 397
+    },
+    "mj_sensorVel": {
+      "name": "mj_sensorVel",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 400
+    },
+    "mj_sensorAcc": {
+      "name": "mj_sensorAcc",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 403
+    },
+    "mj_energyPos": {
+      "name": "mj_energyPos",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 406
+    },
+    "mj_energyVel": {
+      "name": "mj_energyVel",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 409
+    },
+    "mj_checkPos": {
+      "name": "mj_checkPos",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 412
+    },
+    "mj_checkVel": {
+      "name": "mj_checkVel",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 415
+    },
+    "mj_checkAcc": {
+      "name": "mj_checkAcc",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 418
+    },
+    "mj_kinematics": {
+      "name": "mj_kinematics",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 421
+    },
+    "mj_comPos": {
+      "name": "mj_comPos",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 424
+    },
+    "mj_camlight": {
+      "name": "mj_camlight",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 427
+    },
+    "mj_flex": {
+      "name": "mj_flex",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 430
+    },
+    "mj_tendon": {
+      "name": "mj_tendon",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 433
+    },
+    "mj_transmission": {
+      "name": "mj_transmission",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 436
+    },
+    "mj_crb": {
+      "name": "mj_crb",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 439
+    },
+    "mj_makeM": {
+      "name": "mj_makeM",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 442
+    },
+    "mj_factorM": {
+      "name": "mj_factorM",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 445
+    },
+    "mj_solveM": {
+      "name": "mj_solveM",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "x",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "y",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "n",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 448
+    },
+    "mj_solveM2": {
+      "name": "mj_solveM2",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "x",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "y",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "sqrtInvD",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "n",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 451
+    },
+    "mj_comVel": {
+      "name": "mj_comVel",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 455
+    },
+    "mj_passive": {
+      "name": "mj_passive",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 458
+    },
+    "mj_subtreeVel": {
+      "name": "mj_subtreeVel",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 461
+    },
+    "mj_rne": {
+      "name": "mj_rne",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "flg_acc",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "result",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 464
+    },
+    "mj_rnePostConstraint": {
+      "name": "mj_rnePostConstraint",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 467
+    },
+    "mj_maxContact": {
+      "name": "mj_maxContact",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "g1",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "g2",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "has_margin",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 472
+    },
+    "mj_collision": {
+      "name": "mj_collision",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 475
+    },
+    "mj_makeConstraint": {
+      "name": "mj_makeConstraint",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 478
+    },
+    "mj_island": {
+      "name": "mj_island",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 481
+    },
+    "mj_projectConstraint": {
+      "name": "mj_projectConstraint",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 484
+    },
+    "mj_referenceConstraint": {
+      "name": "mj_referenceConstraint",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 487
+    },
+    "mj_constraintUpdate": {
+      "name": "mj_constraintUpdate",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "jar",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "cost",
+          "c_type": "mjtNum [1]",
+          "array_dim": 1
+        },
+        {
+          "name": "flg_coneHessian",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 492
+    },
+    "mj_stateSize": {
+      "name": "mj_stateSize",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "sig",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 499
+    },
+    "mj_getState": {
+      "name": "mj_getState",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "const mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "state",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "sig",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 502
+    },
+    "mj_extractState": {
+      "name": "mj_extractState",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "src",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "srcsig",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "dst",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "dstsig",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 505
+    },
+    "mj_setState": {
+      "name": "mj_setState",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "state",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "sig",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 509
+    },
+    "mj_copyState": {
+      "name": "mj_copyState",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "src",
+          "c_type": "const mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "dst",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "sig",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 512
+    },
+    "mj_readCtrl": {
+      "name": "mj_readCtrl",
+      "return_type": "mjtNum",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "const mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "id",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "time",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "interp",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 517
+    },
+    "mj_readSensor": {
+      "name": "mj_readSensor",
+      "return_type": "const mjtNum *",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "const mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "id",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "time",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "result",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "interp",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 523
+    },
+    "mj_initCtrlHistory": {
+      "name": "mj_initCtrlHistory",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "id",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "times",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "values",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 528
+    },
+    "mj_initSensorHistory": {
+      "name": "mj_initSensorHistory",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "id",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "times",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "values",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "phase",
+          "c_type": "mjtNum",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 534
+    },
+    "mj_setKeyframe": {
+      "name": "mj_setKeyframe",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "const mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "k",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 538
+    },
+    "mj_addContact": {
+      "name": "mj_addContact",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "con",
+          "c_type": "const mjContact *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 541
+    },
+    "mj_isPyramidal": {
+      "name": "mj_isPyramidal",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 544
+    },
+    "mj_isSparse": {
+      "name": "mj_isSparse",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 547
+    },
+    "mj_isDual": {
+      "name": "mj_isDual",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 550
+    },
+    "mj_mulJacVec": {
+      "name": "mj_mulJacVec",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "const mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "res",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "vec",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 553
+    },
+    "mj_mulJacTVec": {
+      "name": "mj_mulJacTVec",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "const mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "res",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "vec",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 556
+    },
+    "mj_jac": {
+      "name": "mj_jac",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "const mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "jacp",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "jacr",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "point",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "body",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 560
+    },
+    "mj_jacBody": {
+      "name": "mj_jacBody",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "const mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "jacp",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "jacr",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "body",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 565
+    },
+    "mj_jacBodyCom": {
+      "name": "mj_jacBodyCom",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "const mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "jacp",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "jacr",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "body",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 569
+    },
+    "mj_jacSubtreeCom": {
+      "name": "mj_jacSubtreeCom",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "jacp",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "body",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 572
+    },
+    "mj_jacGeom": {
+      "name": "mj_jacGeom",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "const mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "jacp",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "jacr",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "geom",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 576
+    },
+    "mj_jacSite": {
+      "name": "mj_jacSite",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "const mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "jacp",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "jacr",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "site",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 580
+    },
+    "mj_jacPointAxis": {
+      "name": "mj_jacPointAxis",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "jacPoint",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "jacAxis",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "point",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "axis",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "body",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 584
+    },
+    "mj_jacDot": {
+      "name": "mj_jacDot",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "const mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "jacp",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "jacr",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "point",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "body",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 589
+    },
+    "mj_angmomMat": {
+      "name": "mj_angmomMat",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "mat",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "body",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 593
+    },
+    "mj_name2id": {
+      "name": "mj_name2id",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "type",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "name",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 596
+    },
+    "mj_id2name": {
+      "name": "mj_id2name",
+      "return_type": "const char *",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "type",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "id",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 599
+    },
+    "mj_fullM": {
+      "name": "mj_fullM",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "dst",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "M",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 602
+    },
+    "mj_mulM": {
+      "name": "mj_mulM",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "const mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "res",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "vec",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 605
+    },
+    "mj_mulM2": {
+      "name": "mj_mulM2",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "const mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "res",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "vec",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 608
+    },
+    "mj_addM": {
+      "name": "mj_addM",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "dst",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "rownnz",
+          "c_type": "int *",
+          "array_dim": null
+        },
+        {
+          "name": "rowadr",
+          "c_type": "int *",
+          "array_dim": null
+        },
+        {
+          "name": "colind",
+          "c_type": "int *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 613
+    },
+    "mj_applyFT": {
+      "name": "mj_applyFT",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "force",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "torque",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "point",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "body",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "qfrc_target",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 617
+    },
+    "mj_objectVelocity": {
+      "name": "mj_objectVelocity",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "const mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "objtype",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "objid",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "res",
+          "c_type": "mjtNum [6]",
+          "array_dim": 6
+        },
+        {
+          "name": "flg_local",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 621
+    },
+    "mj_objectAcceleration": {
+      "name": "mj_objectAcceleration",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "const mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "objtype",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "objid",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "res",
+          "c_type": "mjtNum [6]",
+          "array_dim": 6
+        },
+        {
+          "name": "flg_local",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 625
+    },
+    "mj_geomDistance": {
+      "name": "mj_geomDistance",
+      "return_type": "mjtNum",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "geom1",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "geom2",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "distmax",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "fromto",
+          "c_type": "mjtNum [6]",
+          "array_dim": 6
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 630
+    },
+    "mj_contactForce": {
+      "name": "mj_contactForce",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "const mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "id",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "result",
+          "c_type": "mjtNum [6]",
+          "array_dim": 6
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 634
+    },
+    "mj_differentiatePos": {
+      "name": "mj_differentiatePos",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "qvel",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "dt",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "qpos1",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "qpos2",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 637
+    },
+    "mj_integratePos": {
+      "name": "mj_integratePos",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "qpos",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "qvel",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "dt",
+          "c_type": "mjtNum",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 641
+    },
+    "mj_normalizeQuat": {
+      "name": "mj_normalizeQuat",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "qpos",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 644
+    },
+    "mj_local2Global": {
+      "name": "mj_local2Global",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "xpos",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "xmat",
+          "c_type": "mjtNum [9]",
+          "array_dim": 9
+        },
+        {
+          "name": "pos",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "quat",
+          "c_type": "const mjtNum [4]",
+          "array_dim": 4
+        },
+        {
+          "name": "body",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "sameframe",
+          "c_type": "mjtByte",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 647
+    },
+    "mj_getTotalmass": {
+      "name": "mj_getTotalmass",
+      "return_type": "mjtNum",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 651
+    },
+    "mj_setTotalmass": {
+      "name": "mj_setTotalmass",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "newmass",
+          "c_type": "mjtNum",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 654
+    },
+    "mj_getPluginConfig": {
+      "name": "mj_getPluginConfig",
+      "return_type": "const char *",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "plugin_id",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "attrib",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 658
+    },
+    "mj_loadPluginLibrary": {
+      "name": "mj_loadPluginLibrary",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "path",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 661
+    },
+    "mj_loadAllPluginLibraries": {
+      "name": "mj_loadAllPluginLibraries",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "directory",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "callback",
+          "c_type": "mjfPluginLibraryLoadCallback",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 666
+    },
+    "mj_version": {
+      "name": "mj_version",
+      "return_type": "int",
+      "params": [],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 669
+    },
+    "mj_versionString": {
+      "name": "mj_versionString",
+      "return_type": "const char *",
+      "params": [],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 672
+    },
+    "mj_ray": {
+      "name": "mj_ray",
+      "return_type": "mjtNum",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "const mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "pnt",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "vec",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "geomgroup",
+          "c_type": "const mjtByte *",
+          "array_dim": null
+        },
+        {
+          "name": "flg_static",
+          "c_type": "mjtByte",
+          "array_dim": null
+        },
+        {
+          "name": "bodyexclude",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "geomid",
+          "c_type": "int [1]",
+          "array_dim": 1
+        },
+        {
+          "name": "normal",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 681
+    },
+    "mj_multiRay": {
+      "name": "mj_multiRay",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "pnt",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "vec",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "geomgroup",
+          "c_type": "const mjtByte *",
+          "array_dim": null
+        },
+        {
+          "name": "flg_static",
+          "c_type": "mjtByte",
+          "array_dim": null
+        },
+        {
+          "name": "bodyexclude",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "geomid",
+          "c_type": "int *",
+          "array_dim": null
+        },
+        {
+          "name": "dist",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "normal",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "nray",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "cutoff",
+          "c_type": "mjtNum",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 689
+    },
+    "mj_rayHfield": {
+      "name": "mj_rayHfield",
+      "return_type": "mjtNum",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "const mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "geomid",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "pnt",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "vec",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "normal",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 695
+    },
+    "mj_rayMesh": {
+      "name": "mj_rayMesh",
+      "return_type": "mjtNum",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "const mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "geomid",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "pnt",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "vec",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "normal",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 700
+    },
+    "mju_rayGeom": {
+      "name": "mju_rayGeom",
+      "return_type": "mjtNum",
+      "params": [
+        {
+          "name": "pos",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "mat",
+          "c_type": "const mjtNum [9]",
+          "array_dim": 9
+        },
+        {
+          "name": "size",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "pnt",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "vec",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "geomtype",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "normal",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 705
+    },
+    "mj_rayFlex": {
+      "name": "mj_rayFlex",
+      "return_type": "mjtNum",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "const mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "flex_layer",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "flg_vert",
+          "c_type": "mjtByte",
+          "array_dim": null
+        },
+        {
+          "name": "flg_edge",
+          "c_type": "mjtByte",
+          "array_dim": null
+        },
+        {
+          "name": "flg_face",
+          "c_type": "mjtByte",
+          "array_dim": null
+        },
+        {
+          "name": "flg_skin",
+          "c_type": "mjtByte",
+          "array_dim": null
+        },
+        {
+          "name": "flexid",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "pnt",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "vec",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "vertid",
+          "c_type": "int [1]",
+          "array_dim": 1
+        },
+        {
+          "name": "normal",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 712
+    },
+    "mju_raySkin": {
+      "name": "mju_raySkin",
+      "return_type": "mjtNum",
+      "params": [
+        {
+          "name": "nface",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "nvert",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "face",
+          "c_type": "const int *",
+          "array_dim": null
+        },
+        {
+          "name": "vert",
+          "c_type": "const float *",
+          "array_dim": null
+        },
+        {
+          "name": "pnt",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "vec",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "vertid",
+          "c_type": "int [1]",
+          "array_dim": 1
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 720
+    },
+    "mjv_defaultCamera": {
+      "name": "mjv_defaultCamera",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "cam",
+          "c_type": "mjvCamera *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 726
+    },
+    "mjv_defaultFreeCamera": {
+      "name": "mjv_defaultFreeCamera",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "cam",
+          "c_type": "mjvCamera *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 729
+    },
+    "mjv_defaultPerturb": {
+      "name": "mjv_defaultPerturb",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "pert",
+          "c_type": "mjvPerturb *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 732
+    },
+    "mjv_room2model": {
+      "name": "mjv_room2model",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "modelpos",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "modelquat",
+          "c_type": "mjtNum [4]",
+          "array_dim": 4
+        },
+        {
+          "name": "roompos",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "roomquat",
+          "c_type": "const mjtNum [4]",
+          "array_dim": 4
+        },
+        {
+          "name": "scn",
+          "c_type": "const mjvScene *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 735
+    },
+    "mjv_model2room": {
+      "name": "mjv_model2room",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "roompos",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "roomquat",
+          "c_type": "mjtNum [4]",
+          "array_dim": 4
+        },
+        {
+          "name": "modelpos",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "modelquat",
+          "c_type": "const mjtNum [4]",
+          "array_dim": 4
+        },
+        {
+          "name": "scn",
+          "c_type": "const mjvScene *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 739
+    },
+    "mjv_cameraInModel": {
+      "name": "mjv_cameraInModel",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "headpos",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "forward",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "up",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "scn",
+          "c_type": "const mjvScene *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 743
+    },
+    "mjv_cameraInRoom": {
+      "name": "mjv_cameraInRoom",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "headpos",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "forward",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "up",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "scn",
+          "c_type": "const mjvScene *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 747
+    },
+    "mjv_frustumHeight": {
+      "name": "mjv_frustumHeight",
+      "return_type": "mjtNum",
+      "params": [
+        {
+          "name": "scn",
+          "c_type": "const mjvScene *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 751
+    },
+    "mjv_alignToCamera": {
+      "name": "mjv_alignToCamera",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "vec",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "forward",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 754
+    },
+    "mjv_moveCamera": {
+      "name": "mjv_moveCamera",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "action",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "reldx",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "reldy",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "scn",
+          "c_type": "const mjvScene *",
+          "array_dim": null
+        },
+        {
+          "name": "cam",
+          "c_type": "mjvCamera *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 757
+    },
+    "mjv_movePerturb": {
+      "name": "mjv_movePerturb",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "const mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "action",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "reldx",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "reldy",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "scn",
+          "c_type": "const mjvScene *",
+          "array_dim": null
+        },
+        {
+          "name": "pert",
+          "c_type": "mjvPerturb *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 761
+    },
+    "mjv_moveModel": {
+      "name": "mjv_moveModel",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "action",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "reldx",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "reldy",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "roomup",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "scn",
+          "c_type": "mjvScene *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 765
+    },
+    "mjv_initPerturb": {
+      "name": "mjv_initPerturb",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "scn",
+          "c_type": "const mjvScene *",
+          "array_dim": null
+        },
+        {
+          "name": "pert",
+          "c_type": "mjvPerturb *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 769
+    },
+    "mjv_applyPerturbPose": {
+      "name": "mjv_applyPerturbPose",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "pert",
+          "c_type": "const mjvPerturb *",
+          "array_dim": null
+        },
+        {
+          "name": "flg_paused",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 773
+    },
+    "mjv_applyPerturbForce": {
+      "name": "mjv_applyPerturbForce",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "pert",
+          "c_type": "const mjvPerturb *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 777
+    },
+    "mjv_averageCamera": {
+      "name": "mjv_averageCamera",
+      "return_type": "mjvGLCamera",
+      "params": [
+        {
+          "name": "cam1",
+          "c_type": "const mjvGLCamera *",
+          "array_dim": null
+        },
+        {
+          "name": "cam2",
+          "c_type": "const mjvGLCamera *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 780
+    },
+    "mjv_select": {
+      "name": "mjv_select",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "const mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "vopt",
+          "c_type": "const mjvOption *",
+          "array_dim": null
+        },
+        {
+          "name": "aspectratio",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "relx",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "rely",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "scn",
+          "c_type": "const mjvScene *",
+          "array_dim": null
+        },
+        {
+          "name": "selpnt",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "geomid",
+          "c_type": "int [1]",
+          "array_dim": 1
+        },
+        {
+          "name": "flexid",
+          "c_type": "int [1]",
+          "array_dim": 1
+        },
+        {
+          "name": "skinid",
+          "c_type": "int [1]",
+          "array_dim": 1
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 784
+    },
+    "mjv_defaultOption": {
+      "name": "mjv_defaultOption",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "opt",
+          "c_type": "mjvOption *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 793
+    },
+    "mjv_defaultFigure": {
+      "name": "mjv_defaultFigure",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "fig",
+          "c_type": "mjvFigure *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 796
+    },
+    "mjv_initGeom": {
+      "name": "mjv_initGeom",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "geom",
+          "c_type": "mjvGeom *",
+          "array_dim": null
+        },
+        {
+          "name": "type",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "size",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "pos",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "mat",
+          "c_type": "const mjtNum [9]",
+          "array_dim": 9
+        },
+        {
+          "name": "rgba",
+          "c_type": "const float [4]",
+          "array_dim": 4
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 800
+    },
+    "mjv_connector": {
+      "name": "mjv_connector",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "geom",
+          "c_type": "mjvGeom *",
+          "array_dim": null
+        },
+        {
+          "name": "type",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "width",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "from",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "to",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 806
+    },
+    "mjv_defaultScene": {
+      "name": "mjv_defaultScene",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "scn",
+          "c_type": "mjvScene *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 810
+    },
+    "mjv_makeScene": {
+      "name": "mjv_makeScene",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "scn",
+          "c_type": "mjvScene *",
+          "array_dim": null
+        },
+        {
+          "name": "maxgeom",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 813
+    },
+    "mjv_freeScene": {
+      "name": "mjv_freeScene",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "scn",
+          "c_type": "mjvScene *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 816
+    },
+    "mjv_updateScene": {
+      "name": "mjv_updateScene",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "opt",
+          "c_type": "const mjvOption *",
+          "array_dim": null
+        },
+        {
+          "name": "pert",
+          "c_type": "const mjvPerturb *",
+          "array_dim": null
+        },
+        {
+          "name": "cam",
+          "c_type": "mjvCamera *",
+          "array_dim": null
+        },
+        {
+          "name": "catmask",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "scn",
+          "c_type": "mjvScene *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 819
+    },
+    "mjv_copyModel": {
+      "name": "mjv_copyModel",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "dest",
+          "c_type": "mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "src",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 824
+    },
+    "mjv_addGeoms": {
+      "name": "mjv_addGeoms",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "opt",
+          "c_type": "const mjvOption *",
+          "array_dim": null
+        },
+        {
+          "name": "pert",
+          "c_type": "const mjvPerturb *",
+          "array_dim": null
+        },
+        {
+          "name": "catmask",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "scn",
+          "c_type": "mjvScene *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 827
+    },
+    "mjv_makeLights": {
+      "name": "mjv_makeLights",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "const mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "scn",
+          "c_type": "mjvScene *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 831
+    },
+    "mjv_updateCamera": {
+      "name": "mjv_updateCamera",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "const mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "cam",
+          "c_type": "mjvCamera *",
+          "array_dim": null
+        },
+        {
+          "name": "scn",
+          "c_type": "mjvScene *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 834
+    },
+    "mjv_updateSkin": {
+      "name": "mjv_updateSkin",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "const mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "scn",
+          "c_type": "mjvScene *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 837
+    },
+    "mjv_cameraFrame": {
+      "name": "mjv_cameraFrame",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "headpos",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "forward",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "up",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "right",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "d",
+          "c_type": "const mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "cam",
+          "c_type": "const mjvCamera *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 841
+    },
+    "mjv_cameraFrustum": {
+      "name": "mjv_cameraFrustum",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "zver",
+          "c_type": "float [2]",
+          "array_dim": 2
+        },
+        {
+          "name": "zhor",
+          "c_type": "float [2]",
+          "array_dim": 2
+        },
+        {
+          "name": "zclip",
+          "c_type": "float [2]",
+          "array_dim": 2
+        },
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "cam",
+          "c_type": "const mjvCamera *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 846
+    },
+    "mjr_defaultContext": {
+      "name": "mjr_defaultContext",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "con",
+          "c_type": "mjrContext *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 853
+    },
+    "mjr_makeContext": {
+      "name": "mjr_makeContext",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "con",
+          "c_type": "mjrContext *",
+          "array_dim": null
+        },
+        {
+          "name": "fontscale",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 856
+    },
+    "mjr_changeFont": {
+      "name": "mjr_changeFont",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "fontscale",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "con",
+          "c_type": "mjrContext *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 859
+    },
+    "mjr_addAux": {
+      "name": "mjr_addAux",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "index",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "width",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "height",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "samples",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "con",
+          "c_type": "mjrContext *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 862
+    },
+    "mjr_freeContext": {
+      "name": "mjr_freeContext",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "con",
+          "c_type": "mjrContext *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 865
+    },
+    "mjr_resizeOffscreen": {
+      "name": "mjr_resizeOffscreen",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "width",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "height",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "con",
+          "c_type": "mjrContext *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 868
+    },
+    "mjr_uploadTexture": {
+      "name": "mjr_uploadTexture",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "con",
+          "c_type": "const mjrContext *",
+          "array_dim": null
+        },
+        {
+          "name": "texid",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 871
+    },
+    "mjr_uploadMesh": {
+      "name": "mjr_uploadMesh",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "con",
+          "c_type": "const mjrContext *",
+          "array_dim": null
+        },
+        {
+          "name": "meshid",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 874
+    },
+    "mjr_uploadHField": {
+      "name": "mjr_uploadHField",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "con",
+          "c_type": "const mjrContext *",
+          "array_dim": null
+        },
+        {
+          "name": "hfieldid",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 877
+    },
+    "mjr_restoreBuffer": {
+      "name": "mjr_restoreBuffer",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "con",
+          "c_type": "const mjrContext *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 880
+    },
+    "mjr_setBuffer": {
+      "name": "mjr_setBuffer",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "framebuffer",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "con",
+          "c_type": "mjrContext *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 884
+    },
+    "mjr_readPixels": {
+      "name": "mjr_readPixels",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "rgb",
+          "c_type": "unsigned char *",
+          "array_dim": null
+        },
+        {
+          "name": "depth",
+          "c_type": "float *",
+          "array_dim": null
+        },
+        {
+          "name": "viewport",
+          "c_type": "mjrRect",
+          "array_dim": null
+        },
+        {
+          "name": "con",
+          "c_type": "const mjrContext *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 888
+    },
+    "mjr_drawPixels": {
+      "name": "mjr_drawPixels",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "rgb",
+          "c_type": "const unsigned char *",
+          "array_dim": null
+        },
+        {
+          "name": "depth",
+          "c_type": "const float *",
+          "array_dim": null
+        },
+        {
+          "name": "viewport",
+          "c_type": "mjrRect",
+          "array_dim": null
+        },
+        {
+          "name": "con",
+          "c_type": "const mjrContext *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 893
+    },
+    "mjr_blitBuffer": {
+      "name": "mjr_blitBuffer",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "src",
+          "c_type": "mjrRect",
+          "array_dim": null
+        },
+        {
+          "name": "dst",
+          "c_type": "mjrRect",
+          "array_dim": null
+        },
+        {
+          "name": "flg_color",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "flg_depth",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "con",
+          "c_type": "const mjrContext *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 898
+    },
+    "mjr_setAux": {
+      "name": "mjr_setAux",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "index",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "con",
+          "c_type": "const mjrContext *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 902
+    },
+    "mjr_blitAux": {
+      "name": "mjr_blitAux",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "index",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "src",
+          "c_type": "mjrRect",
+          "array_dim": null
+        },
+        {
+          "name": "left",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "bottom",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "con",
+          "c_type": "const mjrContext *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 905
+    },
+    "mjr_text": {
+      "name": "mjr_text",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "font",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "txt",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "con",
+          "c_type": "const mjrContext *",
+          "array_dim": null
+        },
+        {
+          "name": "x",
+          "c_type": "float",
+          "array_dim": null
+        },
+        {
+          "name": "y",
+          "c_type": "float",
+          "array_dim": null
+        },
+        {
+          "name": "r",
+          "c_type": "float",
+          "array_dim": null
+        },
+        {
+          "name": "g",
+          "c_type": "float",
+          "array_dim": null
+        },
+        {
+          "name": "b",
+          "c_type": "float",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 908
+    },
+    "mjr_overlay": {
+      "name": "mjr_overlay",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "font",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "gridpos",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "viewport",
+          "c_type": "mjrRect",
+          "array_dim": null
+        },
+        {
+          "name": "overlay",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "overlay2",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "con",
+          "c_type": "const mjrContext *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 912
+    },
+    "mjr_maxViewport": {
+      "name": "mjr_maxViewport",
+      "return_type": "mjrRect",
+      "params": [
+        {
+          "name": "con",
+          "c_type": "const mjrContext *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 916
+    },
+    "mjr_rectangle": {
+      "name": "mjr_rectangle",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "viewport",
+          "c_type": "mjrRect",
+          "array_dim": null
+        },
+        {
+          "name": "r",
+          "c_type": "float",
+          "array_dim": null
+        },
+        {
+          "name": "g",
+          "c_type": "float",
+          "array_dim": null
+        },
+        {
+          "name": "b",
+          "c_type": "float",
+          "array_dim": null
+        },
+        {
+          "name": "a",
+          "c_type": "float",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 919
+    },
+    "mjr_label": {
+      "name": "mjr_label",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "viewport",
+          "c_type": "mjrRect",
+          "array_dim": null
+        },
+        {
+          "name": "font",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "txt",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "r",
+          "c_type": "float",
+          "array_dim": null
+        },
+        {
+          "name": "g",
+          "c_type": "float",
+          "array_dim": null
+        },
+        {
+          "name": "b",
+          "c_type": "float",
+          "array_dim": null
+        },
+        {
+          "name": "a",
+          "c_type": "float",
+          "array_dim": null
+        },
+        {
+          "name": "rt",
+          "c_type": "float",
+          "array_dim": null
+        },
+        {
+          "name": "gt",
+          "c_type": "float",
+          "array_dim": null
+        },
+        {
+          "name": "bt",
+          "c_type": "float",
+          "array_dim": null
+        },
+        {
+          "name": "con",
+          "c_type": "const mjrContext *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 922
+    },
+    "mjr_figure": {
+      "name": "mjr_figure",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "viewport",
+          "c_type": "mjrRect",
+          "array_dim": null
+        },
+        {
+          "name": "fig",
+          "c_type": "mjvFigure *",
+          "array_dim": null
+        },
+        {
+          "name": "con",
+          "c_type": "const mjrContext *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 927
+    },
+    "mjr_render": {
+      "name": "mjr_render",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "viewport",
+          "c_type": "mjrRect",
+          "array_dim": null
+        },
+        {
+          "name": "scn",
+          "c_type": "mjvScene *",
+          "array_dim": null
+        },
+        {
+          "name": "con",
+          "c_type": "const mjrContext *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 930
+    },
+    "mjr_finish": {
+      "name": "mjr_finish",
+      "return_type": "void",
+      "params": [],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 933
+    },
+    "mjr_getError": {
+      "name": "mjr_getError",
+      "return_type": "int",
+      "params": [],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 936
+    },
+    "mjr_findRect": {
+      "name": "mjr_findRect",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "x",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "y",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "nrect",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "rect",
+          "c_type": "const mjrRect *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 939
+    },
+    "mjui_themeSpacing": {
+      "name": "mjui_themeSpacing",
+      "return_type": "mjuiThemeSpacing",
+      "params": [
+        {
+          "name": "ind",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 945
+    },
+    "mjui_themeColor": {
+      "name": "mjui_themeColor",
+      "return_type": "mjuiThemeColor",
+      "params": [
+        {
+          "name": "ind",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 948
+    },
+    "mjui_add": {
+      "name": "mjui_add",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "ui",
+          "c_type": "mjUI *",
+          "array_dim": null
+        },
+        {
+          "name": "def",
+          "c_type": "const mjuiDef *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 951
+    },
+    "mjui_addToSection": {
+      "name": "mjui_addToSection",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "ui",
+          "c_type": "mjUI *",
+          "array_dim": null
+        },
+        {
+          "name": "sect",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "def",
+          "c_type": "const mjuiDef *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 954
+    },
+    "mjui_resize": {
+      "name": "mjui_resize",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "ui",
+          "c_type": "mjUI *",
+          "array_dim": null
+        },
+        {
+          "name": "con",
+          "c_type": "const mjrContext *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 957
+    },
+    "mjui_update": {
+      "name": "mjui_update",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "section",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "item",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "ui",
+          "c_type": "const mjUI *",
+          "array_dim": null
+        },
+        {
+          "name": "state",
+          "c_type": "const mjuiState *",
+          "array_dim": null
+        },
+        {
+          "name": "con",
+          "c_type": "const mjrContext *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 960
+    },
+    "mjui_event": {
+      "name": "mjui_event",
+      "return_type": "mjuiItem *",
+      "params": [
+        {
+          "name": "ui",
+          "c_type": "mjUI *",
+          "array_dim": null
+        },
+        {
+          "name": "state",
+          "c_type": "mjuiState *",
+          "array_dim": null
+        },
+        {
+          "name": "con",
+          "c_type": "const mjrContext *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 964
+    },
+    "mjui_render": {
+      "name": "mjui_render",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "ui",
+          "c_type": "mjUI *",
+          "array_dim": null
+        },
+        {
+          "name": "state",
+          "c_type": "const mjuiState *",
+          "array_dim": null
+        },
+        {
+          "name": "con",
+          "c_type": "const mjrContext *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 967
+    },
+    "mju_error": {
+      "name": "mju_error",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "msg",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 973
+    },
+    "mju_error_i": {
+      "name": "mju_error_i",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "msg",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "i",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 976
+    },
+    "mju_error_s": {
+      "name": "mju_error_s",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "msg",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "text",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 979
+    },
+    "mju_warning": {
+      "name": "mju_warning",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "msg",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 982
+    },
+    "mju_warning_i": {
+      "name": "mju_warning_i",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "msg",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "i",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 985
+    },
+    "mju_warning_s": {
+      "name": "mju_warning_s",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "msg",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "text",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 988
+    },
+    "mju_clearHandlers": {
+      "name": "mju_clearHandlers",
+      "return_type": "void",
+      "params": [],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 991
+    },
+    "mju_malloc": {
+      "name": "mju_malloc",
+      "return_type": "void *",
+      "params": [
+        {
+          "name": "size",
+          "c_type": "size_t",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 994
+    },
+    "mju_free": {
+      "name": "mju_free",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "ptr",
+          "c_type": "void *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 997
+    },
+    "mj_warning": {
+      "name": "mj_warning",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "warning",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "info",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1000
+    },
+    "mju_writeLog": {
+      "name": "mju_writeLog",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "type",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "msg",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1003
+    },
+    "mjs_getError": {
+      "name": "mjs_getError",
+      "return_type": "const char *",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "mjSpec *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1006
+    },
+    "mjs_isWarning": {
+      "name": "mjs_isWarning",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "mjSpec *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1009
+    },
+    "mju_zero3": {
+      "name": "mju_zero3",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1053
+    },
+    "mju_copy3": {
+      "name": "mju_copy3",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "data",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1056
+    },
+    "mju_scl3": {
+      "name": "mju_scl3",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "vec",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "scl",
+          "c_type": "mjtNum",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1059
+    },
+    "mju_add3": {
+      "name": "mju_add3",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "vec1",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "vec2",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1062
+    },
+    "mju_sub3": {
+      "name": "mju_sub3",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "vec1",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "vec2",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1065
+    },
+    "mju_addTo3": {
+      "name": "mju_addTo3",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "vec",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1068
+    },
+    "mju_subFrom3": {
+      "name": "mju_subFrom3",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "vec",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1071
+    },
+    "mju_addToScl3": {
+      "name": "mju_addToScl3",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "vec",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "scl",
+          "c_type": "mjtNum",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1074
+    },
+    "mju_addScl3": {
+      "name": "mju_addScl3",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "vec1",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "vec2",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "scl",
+          "c_type": "mjtNum",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1077
+    },
+    "mju_normalize3": {
+      "name": "mju_normalize3",
+      "return_type": "mjtNum",
+      "params": [
+        {
+          "name": "vec",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1080
+    },
+    "mju_norm3": {
+      "name": "mju_norm3",
+      "return_type": "mjtNum",
+      "params": [
+        {
+          "name": "vec",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1083
+    },
+    "mju_dot3": {
+      "name": "mju_dot3",
+      "return_type": "mjtNum",
+      "params": [
+        {
+          "name": "vec1",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "vec2",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1086
+    },
+    "mju_dist3": {
+      "name": "mju_dist3",
+      "return_type": "mjtNum",
+      "params": [
+        {
+          "name": "pos1",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "pos2",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1089
+    },
+    "mju_mulMatVec3": {
+      "name": "mju_mulMatVec3",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "mat",
+          "c_type": "const mjtNum [9]",
+          "array_dim": 9
+        },
+        {
+          "name": "vec",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1092
+    },
+    "mju_mulMatTVec3": {
+      "name": "mju_mulMatTVec3",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "mat",
+          "c_type": "const mjtNum [9]",
+          "array_dim": 9
+        },
+        {
+          "name": "vec",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1095
+    },
+    "mju_cross": {
+      "name": "mju_cross",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "a",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "b",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1098
+    },
+    "mju_zero4": {
+      "name": "mju_zero4",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum [4]",
+          "array_dim": 4
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1101
+    },
+    "mju_unit4": {
+      "name": "mju_unit4",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum [4]",
+          "array_dim": 4
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1104
+    },
+    "mju_copy4": {
+      "name": "mju_copy4",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum [4]",
+          "array_dim": 4
+        },
+        {
+          "name": "data",
+          "c_type": "const mjtNum [4]",
+          "array_dim": 4
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1107
+    },
+    "mju_normalize4": {
+      "name": "mju_normalize4",
+      "return_type": "mjtNum",
+      "params": [
+        {
+          "name": "vec",
+          "c_type": "mjtNum [4]",
+          "array_dim": 4
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1110
+    },
+    "mju_zero": {
+      "name": "mju_zero",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "n",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1113
+    },
+    "mju_fill": {
+      "name": "mju_fill",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "val",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "n",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1116
+    },
+    "mju_copy": {
+      "name": "mju_copy",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "vec",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "n",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1119
+    },
+    "mju_sum": {
+      "name": "mju_sum",
+      "return_type": "mjtNum",
+      "params": [
+        {
+          "name": "vec",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "n",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1122
+    },
+    "mju_L1": {
+      "name": "mju_L1",
+      "return_type": "mjtNum",
+      "params": [
+        {
+          "name": "vec",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "n",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1125
+    },
+    "mju_scl": {
+      "name": "mju_scl",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "vec",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "scl",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "n",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1128
+    },
+    "mju_add": {
+      "name": "mju_add",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "vec1",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "vec2",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "n",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1131
+    },
+    "mju_sub": {
+      "name": "mju_sub",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "vec1",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "vec2",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "n",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1134
+    },
+    "mju_addTo": {
+      "name": "mju_addTo",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "vec",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "n",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1137
+    },
+    "mju_subFrom": {
+      "name": "mju_subFrom",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "vec",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "n",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1140
+    },
+    "mju_addToScl": {
+      "name": "mju_addToScl",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "vec",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "scl",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "n",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1143
+    },
+    "mju_addScl": {
+      "name": "mju_addScl",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "vec1",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "vec2",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "scl",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "n",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1146
+    },
+    "mju_normalize": {
+      "name": "mju_normalize",
+      "return_type": "mjtNum",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "n",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1149
+    },
+    "mju_norm": {
+      "name": "mju_norm",
+      "return_type": "mjtNum",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "n",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1152
+    },
+    "mju_dot": {
+      "name": "mju_dot",
+      "return_type": "mjtNum",
+      "params": [
+        {
+          "name": "vec1",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "vec2",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "n",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1155
+    },
+    "mju_mulMatVec": {
+      "name": "mju_mulMatVec",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "mat",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "vec",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "nr",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "nc",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1158
+    },
+    "mju_mulMatTVec": {
+      "name": "mju_mulMatTVec",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "mat",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "vec",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "nr",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "nc",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1161
+    },
+    "mju_mulVecMatVec": {
+      "name": "mju_mulVecMatVec",
+      "return_type": "mjtNum",
+      "params": [
+        {
+          "name": "vec1",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "mat",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "vec2",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "n",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1164
+    },
+    "mju_transpose": {
+      "name": "mju_transpose",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "mat",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "nr",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "nc",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1167
+    },
+    "mju_symmetrize": {
+      "name": "mju_symmetrize",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "mat",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "n",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1170
+    },
+    "mju_eye": {
+      "name": "mju_eye",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "mat",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "n",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1173
+    },
+    "mju_mulMatMat": {
+      "name": "mju_mulMatMat",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "mat1",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "mat2",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "r1",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "c1",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "c2",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1176
+    },
+    "mju_mulMatMatT": {
+      "name": "mju_mulMatMatT",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "mat1",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "mat2",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "r1",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "c1",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "r2",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1180
+    },
+    "mju_mulMatTMat": {
+      "name": "mju_mulMatTMat",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "mat1",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "mat2",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "r1",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "c1",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "c2",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1184
+    },
+    "mju_sqrMatTD": {
+      "name": "mju_sqrMatTD",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "mat",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "diag",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "nr",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "nc",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1188
+    },
+    "mju_transformSpatial": {
+      "name": "mju_transformSpatial",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum [6]",
+          "array_dim": 6
+        },
+        {
+          "name": "vec",
+          "c_type": "const mjtNum [6]",
+          "array_dim": 6
+        },
+        {
+          "name": "flg_force",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "newpos",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "oldpos",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "rotnew2old",
+          "c_type": "const mjtNum [9]",
+          "array_dim": 9
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1193
+    },
+    "mju_dense2sparse": {
+      "name": "mju_dense2sparse",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "mat",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "nr",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "nc",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "rownnz",
+          "c_type": "int *",
+          "array_dim": null
+        },
+        {
+          "name": "rowadr",
+          "c_type": "int *",
+          "array_dim": null
+        },
+        {
+          "name": "colind",
+          "c_type": "int *",
+          "array_dim": null
+        },
+        {
+          "name": "nnz",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1202
+    },
+    "mju_sparse2dense": {
+      "name": "mju_sparse2dense",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "mat",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "nr",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "nc",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "rownnz",
+          "c_type": "const int *",
+          "array_dim": null
+        },
+        {
+          "name": "rowadr",
+          "c_type": "const int *",
+          "array_dim": null
+        },
+        {
+          "name": "colind",
+          "c_type": "const int *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1206
+    },
+    "mju_sym2dense": {
+      "name": "mju_sym2dense",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "mat",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "n",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "rownnz",
+          "c_type": "const int *",
+          "array_dim": null
+        },
+        {
+          "name": "rowadr",
+          "c_type": "const int *",
+          "array_dim": null
+        },
+        {
+          "name": "colind",
+          "c_type": "const int *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1210
+    },
+    "mju_rotVecQuat": {
+      "name": "mju_rotVecQuat",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "vec",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "quat",
+          "c_type": "const mjtNum [4]",
+          "array_dim": 4
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1217
+    },
+    "mju_negQuat": {
+      "name": "mju_negQuat",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum [4]",
+          "array_dim": 4
+        },
+        {
+          "name": "quat",
+          "c_type": "const mjtNum [4]",
+          "array_dim": 4
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1220
+    },
+    "mju_mulQuat": {
+      "name": "mju_mulQuat",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum [4]",
+          "array_dim": 4
+        },
+        {
+          "name": "quat1",
+          "c_type": "const mjtNum [4]",
+          "array_dim": 4
+        },
+        {
+          "name": "quat2",
+          "c_type": "const mjtNum [4]",
+          "array_dim": 4
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1223
+    },
+    "mju_mulQuatAxis": {
+      "name": "mju_mulQuatAxis",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum [4]",
+          "array_dim": 4
+        },
+        {
+          "name": "quat",
+          "c_type": "const mjtNum [4]",
+          "array_dim": 4
+        },
+        {
+          "name": "axis",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1226
+    },
+    "mju_axisAngle2Quat": {
+      "name": "mju_axisAngle2Quat",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum [4]",
+          "array_dim": 4
+        },
+        {
+          "name": "axis",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "angle",
+          "c_type": "mjtNum",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1229
+    },
+    "mju_quat2Vel": {
+      "name": "mju_quat2Vel",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "quat",
+          "c_type": "const mjtNum [4]",
+          "array_dim": 4
+        },
+        {
+          "name": "dt",
+          "c_type": "mjtNum",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1232
+    },
+    "mju_subQuat": {
+      "name": "mju_subQuat",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "qa",
+          "c_type": "const mjtNum [4]",
+          "array_dim": 4
+        },
+        {
+          "name": "qb",
+          "c_type": "const mjtNum [4]",
+          "array_dim": 4
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1235
+    },
+    "mju_quat2Mat": {
+      "name": "mju_quat2Mat",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum [9]",
+          "array_dim": 9
+        },
+        {
+          "name": "quat",
+          "c_type": "const mjtNum [4]",
+          "array_dim": 4
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1238
+    },
+    "mju_mat2Quat": {
+      "name": "mju_mat2Quat",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "quat",
+          "c_type": "mjtNum [4]",
+          "array_dim": 4
+        },
+        {
+          "name": "mat",
+          "c_type": "const mjtNum [9]",
+          "array_dim": 9
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1241
+    },
+    "mju_derivQuat": {
+      "name": "mju_derivQuat",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum [4]",
+          "array_dim": 4
+        },
+        {
+          "name": "quat",
+          "c_type": "const mjtNum [4]",
+          "array_dim": 4
+        },
+        {
+          "name": "vel",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1244
+    },
+    "mju_quatIntegrate": {
+      "name": "mju_quatIntegrate",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "quat",
+          "c_type": "mjtNum [4]",
+          "array_dim": 4
+        },
+        {
+          "name": "vel",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "scale",
+          "c_type": "mjtNum",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1247
+    },
+    "mju_quatZ2Vec": {
+      "name": "mju_quatZ2Vec",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "quat",
+          "c_type": "mjtNum [4]",
+          "array_dim": 4
+        },
+        {
+          "name": "vec",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1250
+    },
+    "mju_mat2Rot": {
+      "name": "mju_mat2Rot",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "quat",
+          "c_type": "mjtNum [4]",
+          "array_dim": 4
+        },
+        {
+          "name": "mat",
+          "c_type": "const mjtNum [9]",
+          "array_dim": 9
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1254
+    },
+    "mju_euler2Quat": {
+      "name": "mju_euler2Quat",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "quat",
+          "c_type": "mjtNum [4]",
+          "array_dim": 4
+        },
+        {
+          "name": "euler",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "seq",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1258
+    },
+    "mju_mulPose": {
+      "name": "mju_mulPose",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "posres",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "quatres",
+          "c_type": "mjtNum [4]",
+          "array_dim": 4
+        },
+        {
+          "name": "pos1",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "quat1",
+          "c_type": "const mjtNum [4]",
+          "array_dim": 4
+        },
+        {
+          "name": "pos2",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "quat2",
+          "c_type": "const mjtNum [4]",
+          "array_dim": 4
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1264
+    },
+    "mju_negPose": {
+      "name": "mju_negPose",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "posres",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "quatres",
+          "c_type": "mjtNum [4]",
+          "array_dim": 4
+        },
+        {
+          "name": "pos",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "quat",
+          "c_type": "const mjtNum [4]",
+          "array_dim": 4
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1269
+    },
+    "mju_trnVecPose": {
+      "name": "mju_trnVecPose",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "pos",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "quat",
+          "c_type": "const mjtNum [4]",
+          "array_dim": 4
+        },
+        {
+          "name": "vec",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1273
+    },
+    "mju_cholFactor": {
+      "name": "mju_cholFactor",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "mat",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "n",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "mindiag",
+          "c_type": "mjtNum",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1280
+    },
+    "mju_cholSolve": {
+      "name": "mju_cholSolve",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "mat",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "vec",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "n",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1283
+    },
+    "mju_cholUpdate": {
+      "name": "mju_cholUpdate",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "mat",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "x",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "n",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "flg_plus",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1286
+    },
+    "mju_cholFactorBand": {
+      "name": "mju_cholFactorBand",
+      "return_type": "mjtNum",
+      "params": [
+        {
+          "name": "mat",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "ntotal",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "nband",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "ndense",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "diagadd",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "diagmul",
+          "c_type": "mjtNum",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1294
+    },
+    "mju_cholSolveBand": {
+      "name": "mju_cholSolveBand",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "mat",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "vec",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "ntotal",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "nband",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "ndense",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1298
+    },
+    "mju_band2Dense": {
+      "name": "mju_band2Dense",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "mat",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "ntotal",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "nband",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "ndense",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "flg_sym",
+          "c_type": "mjtByte",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1302
+    },
+    "mju_dense2Band": {
+      "name": "mju_dense2Band",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "mat",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "ntotal",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "nband",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "ndense",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1306
+    },
+    "mju_bandMulMatVec": {
+      "name": "mju_bandMulMatVec",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "mat",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "vec",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "ntotal",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "nband",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "ndense",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "nvec",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "flg_sym",
+          "c_type": "mjtByte",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1309
+    },
+    "mju_bandDiag": {
+      "name": "mju_bandDiag",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "i",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "ntotal",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "nband",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "ndense",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1313
+    },
+    "mju_eig3": {
+      "name": "mju_eig3",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "eigval",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "eigvec",
+          "c_type": "mjtNum [9]",
+          "array_dim": 9
+        },
+        {
+          "name": "quat",
+          "c_type": "mjtNum [4]",
+          "array_dim": 4
+        },
+        {
+          "name": "mat",
+          "c_type": "const mjtNum [9]",
+          "array_dim": 9
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1316
+    },
+    "mju_boxQP": {
+      "name": "mju_boxQP",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "R",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "index",
+          "c_type": "int *",
+          "array_dim": null
+        },
+        {
+          "name": "H",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "g",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "n",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "lower",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "upper",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1340
+    },
+    "mju_boxQPmalloc": {
+      "name": "mju_boxQPmalloc",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum **",
+          "array_dim": null
+        },
+        {
+          "name": "R",
+          "c_type": "mjtNum **",
+          "array_dim": null
+        },
+        {
+          "name": "index",
+          "c_type": "int **",
+          "array_dim": null
+        },
+        {
+          "name": "H",
+          "c_type": "mjtNum **",
+          "array_dim": null
+        },
+        {
+          "name": "g",
+          "c_type": "mjtNum **",
+          "array_dim": null
+        },
+        {
+          "name": "n",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "lower",
+          "c_type": "mjtNum **",
+          "array_dim": null
+        },
+        {
+          "name": "upper",
+          "c_type": "mjtNum **",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1346
+    },
+    "mju_muscleGain": {
+      "name": "mju_muscleGain",
+      "return_type": "mjtNum",
+      "params": [
+        {
+          "name": "len",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "vel",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "lengthrange",
+          "c_type": "const mjtNum [2]",
+          "array_dim": 2
+        },
+        {
+          "name": "acc0",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "prm",
+          "c_type": "const mjtNum [9]",
+          "array_dim": 9
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1353
+    },
+    "mju_muscleBias": {
+      "name": "mju_muscleBias",
+      "return_type": "mjtNum",
+      "params": [
+        {
+          "name": "len",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "lengthrange",
+          "c_type": "const mjtNum [2]",
+          "array_dim": 2
+        },
+        {
+          "name": "acc0",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "prm",
+          "c_type": "const mjtNum [9]",
+          "array_dim": 9
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1357
+    },
+    "mju_muscleDynamics": {
+      "name": "mju_muscleDynamics",
+      "return_type": "mjtNum",
+      "params": [
+        {
+          "name": "ctrl",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "act",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "prm",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1361
+    },
+    "mju_encodePyramid": {
+      "name": "mju_encodePyramid",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "pyramid",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "force",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "mu",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "dim",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1364
+    },
+    "mju_decodePyramid": {
+      "name": "mju_decodePyramid",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "force",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "pyramid",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "mu",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "dim",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1367
+    },
+    "mju_springDamper": {
+      "name": "mju_springDamper",
+      "return_type": "mjtNum",
+      "params": [
+        {
+          "name": "pos0",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "vel0",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "Kp",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "Kv",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "dt",
+          "c_type": "mjtNum",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1370
+    },
+    "mju_min": {
+      "name": "mju_min",
+      "return_type": "mjtNum",
+      "params": [
+        {
+          "name": "a",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "b",
+          "c_type": "mjtNum",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1373
+    },
+    "mju_max": {
+      "name": "mju_max",
+      "return_type": "mjtNum",
+      "params": [
+        {
+          "name": "a",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "b",
+          "c_type": "mjtNum",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1376
+    },
+    "mju_clip": {
+      "name": "mju_clip",
+      "return_type": "mjtNum",
+      "params": [
+        {
+          "name": "x",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "min",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "max",
+          "c_type": "mjtNum",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1379
+    },
+    "mju_sign": {
+      "name": "mju_sign",
+      "return_type": "mjtNum",
+      "params": [
+        {
+          "name": "x",
+          "c_type": "mjtNum",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1382
+    },
+    "mju_round": {
+      "name": "mju_round",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "x",
+          "c_type": "mjtNum",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1385
+    },
+    "mju_type2Str": {
+      "name": "mju_type2Str",
+      "return_type": "const char *",
+      "params": [
+        {
+          "name": "type",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1388
+    },
+    "mju_str2Type": {
+      "name": "mju_str2Type",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "str",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1391
+    },
+    "mju_writeNumBytes": {
+      "name": "mju_writeNumBytes",
+      "return_type": "const char *",
+      "params": [
+        {
+          "name": "nbytes",
+          "c_type": "size_t",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1394
+    },
+    "mju_warningText": {
+      "name": "mju_warningText",
+      "return_type": "const char *",
+      "params": [
+        {
+          "name": "warning",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "info",
+          "c_type": "size_t",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1397
+    },
+    "mju_isBad": {
+      "name": "mju_isBad",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "x",
+          "c_type": "mjtNum",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1400
+    },
+    "mju_isZero": {
+      "name": "mju_isZero",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "vec",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "n",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1403
+    },
+    "mju_standardNormal": {
+      "name": "mju_standardNormal",
+      "return_type": "mjtNum",
+      "params": [
+        {
+          "name": "num2",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1406
+    },
+    "mju_f2n": {
+      "name": "mju_f2n",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "vec",
+          "c_type": "const float *",
+          "array_dim": null
+        },
+        {
+          "name": "n",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1409
+    },
+    "mju_n2f": {
+      "name": "mju_n2f",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "float *",
+          "array_dim": null
+        },
+        {
+          "name": "vec",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "n",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1412
+    },
+    "mju_d2n": {
+      "name": "mju_d2n",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "vec",
+          "c_type": "const double *",
+          "array_dim": null
+        },
+        {
+          "name": "n",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1415
+    },
+    "mju_n2d": {
+      "name": "mju_n2d",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "res",
+          "c_type": "double *",
+          "array_dim": null
+        },
+        {
+          "name": "vec",
+          "c_type": "const mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "n",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1418
+    },
+    "mju_insertionSort": {
+      "name": "mju_insertionSort",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "list",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "n",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1421
+    },
+    "mju_insertionSortInt": {
+      "name": "mju_insertionSortInt",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "list",
+          "c_type": "int *",
+          "array_dim": null
+        },
+        {
+          "name": "n",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1424
+    },
+    "mju_Halton": {
+      "name": "mju_Halton",
+      "return_type": "mjtNum",
+      "params": [
+        {
+          "name": "index",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "base",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1427
+    },
+    "mju_strncpy": {
+      "name": "mju_strncpy",
+      "return_type": "char *",
+      "params": [
+        {
+          "name": "dst",
+          "c_type": "char *",
+          "array_dim": null
+        },
+        {
+          "name": "src",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "n",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1430
+    },
+    "mju_sigmoid": {
+      "name": "mju_sigmoid",
+      "return_type": "mjtNum",
+      "params": [
+        {
+          "name": "x",
+          "c_type": "mjtNum",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1433
+    },
+    "mjc_getSDF": {
+      "name": "mjc_getSDF",
+      "return_type": "const mjpPlugin *",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "id",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1439
+    },
+    "mjc_distance": {
+      "name": "mjc_distance",
+      "return_type": "mjtNum",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "const mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "s",
+          "c_type": "const mjSDF *",
+          "array_dim": null
+        },
+        {
+          "name": "x",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1442
+    },
+    "mjc_gradient": {
+      "name": "mjc_gradient",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "const mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "s",
+          "c_type": "const mjSDF *",
+          "array_dim": null
+        },
+        {
+          "name": "gradient",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "x",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1445
+    },
+    "mjd_transitionFD": {
+      "name": "mjd_transitionFD",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "eps",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "flg_centered",
+          "c_type": "mjtByte",
+          "array_dim": null
+        },
+        {
+          "name": "A",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "B",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "C",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "D",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1460
+    },
+    "mjd_inverseFD": {
+      "name": "mjd_inverseFD",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "m",
+          "c_type": "const mjModel *",
+          "array_dim": null
+        },
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "eps",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "flg_actuation",
+          "c_type": "mjtByte",
+          "array_dim": null
+        },
+        {
+          "name": "DfDq",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "DfDv",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "DfDa",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "DsDq",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "DsDv",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "DsDa",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        },
+        {
+          "name": "DmDq",
+          "c_type": "mjtNum *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1479
+    },
+    "mjd_subQuat": {
+      "name": "mjd_subQuat",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "qa",
+          "c_type": "const mjtNum [4]",
+          "array_dim": 4
+        },
+        {
+          "name": "qb",
+          "c_type": "const mjtNum [4]",
+          "array_dim": 4
+        },
+        {
+          "name": "Da",
+          "c_type": "mjtNum [9]",
+          "array_dim": 9
+        },
+        {
+          "name": "Db",
+          "c_type": "mjtNum [9]",
+          "array_dim": 9
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1486
+    },
+    "mjd_quatIntegrate": {
+      "name": "mjd_quatIntegrate",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "vel",
+          "c_type": "const mjtNum [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "scale",
+          "c_type": "mjtNum",
+          "array_dim": null
+        },
+        {
+          "name": "Dquat",
+          "c_type": "mjtNum [9]",
+          "array_dim": 9
+        },
+        {
+          "name": "Dvel",
+          "c_type": "mjtNum [9]",
+          "array_dim": 9
+        },
+        {
+          "name": "Dscale",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1490
+    },
+    "mjp_defaultPlugin": {
+      "name": "mjp_defaultPlugin",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "plugin",
+          "c_type": "mjpPlugin *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1497
+    },
+    "mjp_registerPlugin": {
+      "name": "mjp_registerPlugin",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "plugin",
+          "c_type": "const mjpPlugin *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1505
+    },
+    "mjp_pluginCount": {
+      "name": "mjp_pluginCount",
+      "return_type": "int",
+      "params": [],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1508
+    },
+    "mjp_getPlugin": {
+      "name": "mjp_getPlugin",
+      "return_type": "const mjpPlugin *",
+      "params": [
+        {
+          "name": "name",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "slot",
+          "c_type": "int *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1511
+    },
+    "mjp_getPluginAtSlot": {
+      "name": "mjp_getPluginAtSlot",
+      "return_type": "const mjpPlugin *",
+      "params": [
+        {
+          "name": "slot",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1514
+    },
+    "mjp_defaultResourceProvider": {
+      "name": "mjp_defaultResourceProvider",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "provider",
+          "c_type": "mjpResourceProvider *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1517
+    },
+    "mjp_registerResourceProvider": {
+      "name": "mjp_registerResourceProvider",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "provider",
+          "c_type": "const mjpResourceProvider *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1522
+    },
+    "mjp_resourceProviderCount": {
+      "name": "mjp_resourceProviderCount",
+      "return_type": "int",
+      "params": [],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1525
+    },
+    "mjp_getResourceProvider": {
+      "name": "mjp_getResourceProvider",
+      "return_type": "const mjpResourceProvider *",
+      "params": [
+        {
+          "name": "resource_name",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1529
+    },
+    "mjp_getResourceProviderAtSlot": {
+      "name": "mjp_getResourceProviderAtSlot",
+      "return_type": "const mjpResourceProvider *",
+      "params": [
+        {
+          "name": "slot",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1533
+    },
+    "mjp_registerDecoder": {
+      "name": "mjp_registerDecoder",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "decoder",
+          "c_type": "const mjpDecoder *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1538
+    },
+    "mjp_defaultDecoder": {
+      "name": "mjp_defaultDecoder",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "decoder",
+          "c_type": "mjpDecoder *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1541
+    },
+    "mjp_findDecoder": {
+      "name": "mjp_findDecoder",
+      "return_type": "const mjpDecoder *",
+      "params": [
+        {
+          "name": "resource",
+          "c_type": "const mjResource *",
+          "array_dim": null
+        },
+        {
+          "name": "content_type",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1545
+    },
+    "mjp_registerEncoder": {
+      "name": "mjp_registerEncoder",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "encoder",
+          "c_type": "const mjpEncoder *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1550
+    },
+    "mjp_defaultEncoder": {
+      "name": "mjp_defaultEncoder",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "encoder",
+          "c_type": "mjpEncoder *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1553
+    },
+    "mjp_findEncoder": {
+      "name": "mjp_findEncoder",
+      "return_type": "const mjpEncoder *",
+      "params": [
+        {
+          "name": "filename",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "content_type",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1557
+    },
+    "mju_openResource": {
+      "name": "mju_openResource",
+      "return_type": "mjResource *",
+      "params": [
+        {
+          "name": "dir",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "name",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "vfs",
+          "c_type": "const mjVFS *",
+          "array_dim": null
+        },
+        {
+          "name": "error",
+          "c_type": "char *",
+          "array_dim": null
+        },
+        {
+          "name": "nerror",
+          "c_type": "size_t",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1566
+    },
+    "mju_closeResource": {
+      "name": "mju_closeResource",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "resource",
+          "c_type": "mjResource *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1570
+    },
+    "mju_readResource": {
+      "name": "mju_readResource",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "resource",
+          "c_type": "mjResource *",
+          "array_dim": null
+        },
+        {
+          "name": "buffer",
+          "c_type": "const void **",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1574
+    },
+    "mju_getResourceDir": {
+      "name": "mju_getResourceDir",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "resource",
+          "c_type": "mjResource *",
+          "array_dim": null
+        },
+        {
+          "name": "dir",
+          "c_type": "const char **",
+          "array_dim": null
+        },
+        {
+          "name": "ndir",
+          "c_type": "int *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1577
+    },
+    "mju_isModifiedResource": {
+      "name": "mju_isModifiedResource",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "resource",
+          "c_type": "const mjResource *",
+          "array_dim": null
+        },
+        {
+          "name": "timestamp",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1581
+    },
+    "mju_decodeResource": {
+      "name": "mju_decodeResource",
+      "return_type": "mjSpec *",
+      "params": [
+        {
+          "name": "resource",
+          "c_type": "mjResource *",
+          "array_dim": null
+        },
+        {
+          "name": "content_type",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "vfs",
+          "c_type": "const mjVFS *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1586
+    },
+    "mju_threadPoolCreate": {
+      "name": "mju_threadPoolCreate",
+      "return_type": "mjThreadPool *",
+      "params": [
+        {
+          "name": "number_of_threads",
+          "c_type": "size_t",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1593
+    },
+    "mju_bindThreadPool": {
+      "name": "mju_bindThreadPool",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "d",
+          "c_type": "mjData *",
+          "array_dim": null
+        },
+        {
+          "name": "thread_pool",
+          "c_type": "void *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1596
+    },
+    "mju_threadPoolEnqueue": {
+      "name": "mju_threadPoolEnqueue",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "thread_pool",
+          "c_type": "mjThreadPool *",
+          "array_dim": null
+        },
+        {
+          "name": "task",
+          "c_type": "mjTask *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1599
+    },
+    "mju_threadPoolDestroy": {
+      "name": "mju_threadPoolDestroy",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "thread_pool",
+          "c_type": "mjThreadPool *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1602
+    },
+    "mju_defaultTask": {
+      "name": "mju_defaultTask",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "task",
+          "c_type": "mjTask *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1605
+    },
+    "mju_taskJoin": {
+      "name": "mju_taskJoin",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "task",
+          "c_type": "mjTask *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1608
+    },
+    "mjs_attach": {
+      "name": "mjs_attach",
+      "return_type": "mjsElement *",
+      "params": [
+        {
+          "name": "parent",
+          "c_type": "mjsElement *",
+          "array_dim": null
+        },
+        {
+          "name": "child",
+          "c_type": "const mjsElement *",
+          "array_dim": null
+        },
+        {
+          "name": "prefix",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "suffix",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1614
+    },
+    "mjs_addBody": {
+      "name": "mjs_addBody",
+      "return_type": "mjsBody *",
+      "params": [
+        {
+          "name": "body",
+          "c_type": "mjsBody *",
+          "array_dim": null
+        },
+        {
+          "name": "def",
+          "c_type": "const mjsDefault *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1622
+    },
+    "mjs_addSite": {
+      "name": "mjs_addSite",
+      "return_type": "mjsSite *",
+      "params": [
+        {
+          "name": "body",
+          "c_type": "mjsBody *",
+          "array_dim": null
+        },
+        {
+          "name": "def",
+          "c_type": "const mjsDefault *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1626
+    },
+    "mjs_addJoint": {
+      "name": "mjs_addJoint",
+      "return_type": "mjsJoint *",
+      "params": [
+        {
+          "name": "body",
+          "c_type": "mjsBody *",
+          "array_dim": null
+        },
+        {
+          "name": "def",
+          "c_type": "const mjsDefault *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1630
+    },
+    "mjs_addFreeJoint": {
+      "name": "mjs_addFreeJoint",
+      "return_type": "mjsJoint *",
+      "params": [
+        {
+          "name": "body",
+          "c_type": "mjsBody *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1633
+    },
+    "mjs_addGeom": {
+      "name": "mjs_addGeom",
+      "return_type": "mjsGeom *",
+      "params": [
+        {
+          "name": "body",
+          "c_type": "mjsBody *",
+          "array_dim": null
+        },
+        {
+          "name": "def",
+          "c_type": "const mjsDefault *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1637
+    },
+    "mjs_addCamera": {
+      "name": "mjs_addCamera",
+      "return_type": "mjsCamera *",
+      "params": [
+        {
+          "name": "body",
+          "c_type": "mjsBody *",
+          "array_dim": null
+        },
+        {
+          "name": "def",
+          "c_type": "const mjsDefault *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1641
+    },
+    "mjs_addLight": {
+      "name": "mjs_addLight",
+      "return_type": "mjsLight *",
+      "params": [
+        {
+          "name": "body",
+          "c_type": "mjsBody *",
+          "array_dim": null
+        },
+        {
+          "name": "def",
+          "c_type": "const mjsDefault *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1645
+    },
+    "mjs_addFrame": {
+      "name": "mjs_addFrame",
+      "return_type": "mjsFrame *",
+      "params": [
+        {
+          "name": "body",
+          "c_type": "mjsBody *",
+          "array_dim": null
+        },
+        {
+          "name": "parentframe",
+          "c_type": "mjsFrame *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1648
+    },
+    "mjs_delete": {
+      "name": "mjs_delete",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "spec",
+          "c_type": "mjSpec *",
+          "array_dim": null
+        },
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1651
+    },
+    "mjs_addActuator": {
+      "name": "mjs_addActuator",
+      "return_type": "mjsActuator *",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "mjSpec *",
+          "array_dim": null
+        },
+        {
+          "name": "def",
+          "c_type": "const mjsDefault *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1658
+    },
+    "mjs_addSensor": {
+      "name": "mjs_addSensor",
+      "return_type": "mjsSensor *",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "mjSpec *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1661
+    },
+    "mjs_addFlex": {
+      "name": "mjs_addFlex",
+      "return_type": "mjsFlex *",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "mjSpec *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1664
+    },
+    "mjs_addPair": {
+      "name": "mjs_addPair",
+      "return_type": "mjsPair *",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "mjSpec *",
+          "array_dim": null
+        },
+        {
+          "name": "def",
+          "c_type": "const mjsDefault *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1668
+    },
+    "mjs_addExclude": {
+      "name": "mjs_addExclude",
+      "return_type": "mjsExclude *",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "mjSpec *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1671
+    },
+    "mjs_addEquality": {
+      "name": "mjs_addEquality",
+      "return_type": "mjsEquality *",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "mjSpec *",
+          "array_dim": null
+        },
+        {
+          "name": "def",
+          "c_type": "const mjsDefault *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1675
+    },
+    "mjs_addTendon": {
+      "name": "mjs_addTendon",
+      "return_type": "mjsTendon *",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "mjSpec *",
+          "array_dim": null
+        },
+        {
+          "name": "def",
+          "c_type": "const mjsDefault *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1679
+    },
+    "mjs_wrapSite": {
+      "name": "mjs_wrapSite",
+      "return_type": "mjsWrap *",
+      "params": [
+        {
+          "name": "tendon",
+          "c_type": "mjsTendon *",
+          "array_dim": null
+        },
+        {
+          "name": "name",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1682
+    },
+    "mjs_wrapGeom": {
+      "name": "mjs_wrapGeom",
+      "return_type": "mjsWrap *",
+      "params": [
+        {
+          "name": "tendon",
+          "c_type": "mjsTendon *",
+          "array_dim": null
+        },
+        {
+          "name": "name",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "sidesite",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1685
+    },
+    "mjs_wrapJoint": {
+      "name": "mjs_wrapJoint",
+      "return_type": "mjsWrap *",
+      "params": [
+        {
+          "name": "tendon",
+          "c_type": "mjsTendon *",
+          "array_dim": null
+        },
+        {
+          "name": "name",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "coef",
+          "c_type": "double",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1688
+    },
+    "mjs_wrapPulley": {
+      "name": "mjs_wrapPulley",
+      "return_type": "mjsWrap *",
+      "params": [
+        {
+          "name": "tendon",
+          "c_type": "mjsTendon *",
+          "array_dim": null
+        },
+        {
+          "name": "divisor",
+          "c_type": "double",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1691
+    },
+    "mjs_addNumeric": {
+      "name": "mjs_addNumeric",
+      "return_type": "mjsNumeric *",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "mjSpec *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1694
+    },
+    "mjs_addText": {
+      "name": "mjs_addText",
+      "return_type": "mjsText *",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "mjSpec *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1697
+    },
+    "mjs_addTuple": {
+      "name": "mjs_addTuple",
+      "return_type": "mjsTuple *",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "mjSpec *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1700
+    },
+    "mjs_addKey": {
+      "name": "mjs_addKey",
+      "return_type": "mjsKey *",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "mjSpec *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1703
+    },
+    "mjs_addPlugin": {
+      "name": "mjs_addPlugin",
+      "return_type": "mjsPlugin *",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "mjSpec *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1706
+    },
+    "mjs_addDefault": {
+      "name": "mjs_addDefault",
+      "return_type": "mjsDefault *",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "mjSpec *",
+          "array_dim": null
+        },
+        {
+          "name": "classname",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "parent",
+          "c_type": "const mjsDefault *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1710
+    },
+    "mjs_setToMotor": {
+      "name": "mjs_setToMotor",
+      "return_type": "const char *",
+      "params": [
+        {
+          "name": "actuator",
+          "c_type": "mjsActuator *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1716
+    },
+    "mjs_setToPosition": {
+      "name": "mjs_setToPosition",
+      "return_type": "const char *",
+      "params": [
+        {
+          "name": "actuator",
+          "c_type": "mjsActuator *",
+          "array_dim": null
+        },
+        {
+          "name": "kp",
+          "c_type": "double",
+          "array_dim": null
+        },
+        {
+          "name": "kv",
+          "c_type": "double [1]",
+          "array_dim": 1
+        },
+        {
+          "name": "dampratio",
+          "c_type": "double [1]",
+          "array_dim": 1
+        },
+        {
+          "name": "timeconst",
+          "c_type": "double [1]",
+          "array_dim": 1
+        },
+        {
+          "name": "inheritrange",
+          "c_type": "double",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1719
+    },
+    "mjs_setToIntVelocity": {
+      "name": "mjs_setToIntVelocity",
+      "return_type": "const char *",
+      "params": [
+        {
+          "name": "actuator",
+          "c_type": "mjsActuator *",
+          "array_dim": null
+        },
+        {
+          "name": "kp",
+          "c_type": "double",
+          "array_dim": null
+        },
+        {
+          "name": "kv",
+          "c_type": "double [1]",
+          "array_dim": 1
+        },
+        {
+          "name": "dampratio",
+          "c_type": "double [1]",
+          "array_dim": 1
+        },
+        {
+          "name": "timeconst",
+          "c_type": "double [1]",
+          "array_dim": 1
+        },
+        {
+          "name": "inheritrange",
+          "c_type": "double",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1723
+    },
+    "mjs_setToVelocity": {
+      "name": "mjs_setToVelocity",
+      "return_type": "const char *",
+      "params": [
+        {
+          "name": "actuator",
+          "c_type": "mjsActuator *",
+          "array_dim": null
+        },
+        {
+          "name": "kv",
+          "c_type": "double",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1727
+    },
+    "mjs_setToDamper": {
+      "name": "mjs_setToDamper",
+      "return_type": "const char *",
+      "params": [
+        {
+          "name": "actuator",
+          "c_type": "mjsActuator *",
+          "array_dim": null
+        },
+        {
+          "name": "kv",
+          "c_type": "double",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1730
+    },
+    "mjs_setToCylinder": {
+      "name": "mjs_setToCylinder",
+      "return_type": "const char *",
+      "params": [
+        {
+          "name": "actuator",
+          "c_type": "mjsActuator *",
+          "array_dim": null
+        },
+        {
+          "name": "timeconst",
+          "c_type": "double",
+          "array_dim": null
+        },
+        {
+          "name": "bias",
+          "c_type": "double",
+          "array_dim": null
+        },
+        {
+          "name": "area",
+          "c_type": "double",
+          "array_dim": null
+        },
+        {
+          "name": "diameter",
+          "c_type": "double",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1733
+    },
+    "mjs_setToMuscle": {
+      "name": "mjs_setToMuscle",
+      "return_type": "const char *",
+      "params": [
+        {
+          "name": "actuator",
+          "c_type": "mjsActuator *",
+          "array_dim": null
+        },
+        {
+          "name": "timeconst",
+          "c_type": "double [2]",
+          "array_dim": 2
+        },
+        {
+          "name": "tausmooth",
+          "c_type": "double",
+          "array_dim": null
+        },
+        {
+          "name": "range",
+          "c_type": "double [2]",
+          "array_dim": 2
+        },
+        {
+          "name": "force",
+          "c_type": "double",
+          "array_dim": null
+        },
+        {
+          "name": "scale",
+          "c_type": "double",
+          "array_dim": null
+        },
+        {
+          "name": "lmin",
+          "c_type": "double",
+          "array_dim": null
+        },
+        {
+          "name": "lmax",
+          "c_type": "double",
+          "array_dim": null
+        },
+        {
+          "name": "vmax",
+          "c_type": "double",
+          "array_dim": null
+        },
+        {
+          "name": "fpmax",
+          "c_type": "double",
+          "array_dim": null
+        },
+        {
+          "name": "fvmax",
+          "c_type": "double",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1737
+    },
+    "mjs_setToAdhesion": {
+      "name": "mjs_setToAdhesion",
+      "return_type": "const char *",
+      "params": [
+        {
+          "name": "actuator",
+          "c_type": "mjsActuator *",
+          "array_dim": null
+        },
+        {
+          "name": "gain",
+          "c_type": "double",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1742
+    },
+    "mjs_setToDCMotor": {
+      "name": "mjs_setToDCMotor",
+      "return_type": "const char *",
+      "params": [
+        {
+          "name": "actuator",
+          "c_type": "mjsActuator *",
+          "array_dim": null
+        },
+        {
+          "name": "motorconst",
+          "c_type": "double [2]",
+          "array_dim": 2
+        },
+        {
+          "name": "resistance",
+          "c_type": "double",
+          "array_dim": null
+        },
+        {
+          "name": "nominal",
+          "c_type": "double [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "saturation",
+          "c_type": "double [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "inductance",
+          "c_type": "double [2]",
+          "array_dim": 2
+        },
+        {
+          "name": "cogging",
+          "c_type": "double [3]",
+          "array_dim": 3
+        },
+        {
+          "name": "controller",
+          "c_type": "double [6]",
+          "array_dim": 6
+        },
+        {
+          "name": "thermal",
+          "c_type": "double [6]",
+          "array_dim": 6
+        },
+        {
+          "name": "lugre",
+          "c_type": "double [5]",
+          "array_dim": 5
+        },
+        {
+          "name": "input_mode",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1746
+    },
+    "mjs_addMesh": {
+      "name": "mjs_addMesh",
+      "return_type": "mjsMesh *",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "mjSpec *",
+          "array_dim": null
+        },
+        {
+          "name": "def",
+          "c_type": "const mjsDefault *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1756
+    },
+    "mjs_addHField": {
+      "name": "mjs_addHField",
+      "return_type": "mjsHField *",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "mjSpec *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1759
+    },
+    "mjs_addSkin": {
+      "name": "mjs_addSkin",
+      "return_type": "mjsSkin *",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "mjSpec *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1762
+    },
+    "mjs_addTexture": {
+      "name": "mjs_addTexture",
+      "return_type": "mjsTexture *",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "mjSpec *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1765
+    },
+    "mjs_addMaterial": {
+      "name": "mjs_addMaterial",
+      "return_type": "mjsMaterial *",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "mjSpec *",
+          "array_dim": null
+        },
+        {
+          "name": "def",
+          "c_type": "const mjsDefault *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1769
+    },
+    "mjs_makeMesh": {
+      "name": "mjs_makeMesh",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "mesh",
+          "c_type": "mjsMesh *",
+          "array_dim": null
+        },
+        {
+          "name": "builtin",
+          "c_type": "mjtMeshBuiltin",
+          "array_dim": null
+        },
+        {
+          "name": "params",
+          "c_type": "double *",
+          "array_dim": null
+        },
+        {
+          "name": "nparams",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1772
+    },
+    "mjs_getSpec": {
+      "name": "mjs_getSpec",
+      "return_type": "mjSpec *",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "const mjsElement *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1777
+    },
+    "mjs_getOriginSpec": {
+      "name": "mjs_getOriginSpec",
+      "return_type": "mjSpec *",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "const mjsElement *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1781
+    },
+    "mjs_getCompiler": {
+      "name": "mjs_getCompiler",
+      "return_type": "mjsCompiler *",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "const mjsElement *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1784
+    },
+    "mjs_findSpec": {
+      "name": "mjs_findSpec",
+      "return_type": "mjSpec *",
+      "params": [
+        {
+          "name": "spec",
+          "c_type": "const mjSpec *",
+          "array_dim": null
+        },
+        {
+          "name": "name",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1787
+    },
+    "mjs_findBody": {
+      "name": "mjs_findBody",
+      "return_type": "mjsBody *",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "const mjSpec *",
+          "array_dim": null
+        },
+        {
+          "name": "name",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1790
+    },
+    "mjs_findElement": {
+      "name": "mjs_findElement",
+      "return_type": "mjsElement *",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "const mjSpec *",
+          "array_dim": null
+        },
+        {
+          "name": "type",
+          "c_type": "mjtObj",
+          "array_dim": null
+        },
+        {
+          "name": "name",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1793
+    },
+    "mjs_findChild": {
+      "name": "mjs_findChild",
+      "return_type": "mjsBody *",
+      "params": [
+        {
+          "name": "body",
+          "c_type": "const mjsBody *",
+          "array_dim": null
+        },
+        {
+          "name": "name",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1796
+    },
+    "mjs_getParent": {
+      "name": "mjs_getParent",
+      "return_type": "mjsBody *",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "const mjsElement *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1799
+    },
+    "mjs_getFrame": {
+      "name": "mjs_getFrame",
+      "return_type": "mjsFrame *",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "const mjsElement *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1802
+    },
+    "mjs_findFrame": {
+      "name": "mjs_findFrame",
+      "return_type": "mjsFrame *",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "const mjSpec *",
+          "array_dim": null
+        },
+        {
+          "name": "name",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1805
+    },
+    "mjs_getDefault": {
+      "name": "mjs_getDefault",
+      "return_type": "mjsDefault *",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "const mjsElement *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1808
+    },
+    "mjs_findDefault": {
+      "name": "mjs_findDefault",
+      "return_type": "mjsDefault *",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "const mjSpec *",
+          "array_dim": null
+        },
+        {
+          "name": "classname",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1811
+    },
+    "mjs_getSpecDefault": {
+      "name": "mjs_getSpecDefault",
+      "return_type": "mjsDefault *",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "const mjSpec *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1814
+    },
+    "mjs_getId": {
+      "name": "mjs_getId",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "const mjsElement *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1817
+    },
+    "mjs_firstChild": {
+      "name": "mjs_firstChild",
+      "return_type": "mjsElement *",
+      "params": [
+        {
+          "name": "body",
+          "c_type": "const mjsBody *",
+          "array_dim": null
+        },
+        {
+          "name": "type",
+          "c_type": "mjtObj",
+          "array_dim": null
+        },
+        {
+          "name": "recurse",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1820
+    },
+    "mjs_nextChild": {
+      "name": "mjs_nextChild",
+      "return_type": "mjsElement *",
+      "params": [
+        {
+          "name": "body",
+          "c_type": "const mjsBody *",
+          "array_dim": null
+        },
+        {
+          "name": "child",
+          "c_type": "const mjsElement *",
+          "array_dim": null
+        },
+        {
+          "name": "recurse",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1824
+    },
+    "mjs_firstElement": {
+      "name": "mjs_firstElement",
+      "return_type": "mjsElement *",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "const mjSpec *",
+          "array_dim": null
+        },
+        {
+          "name": "type",
+          "c_type": "mjtObj",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1827
+    },
+    "mjs_nextElement": {
+      "name": "mjs_nextElement",
+      "return_type": "mjsElement *",
+      "params": [
+        {
+          "name": "s",
+          "c_type": "const mjSpec *",
+          "array_dim": null
+        },
+        {
+          "name": "element",
+          "c_type": "const mjsElement *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1830
+    },
+    "mjs_getWrapTarget": {
+      "name": "mjs_getWrapTarget",
+      "return_type": "mjsElement *",
+      "params": [
+        {
+          "name": "wrap",
+          "c_type": "const mjsWrap *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1833
+    },
+    "mjs_getWrapSideSite": {
+      "name": "mjs_getWrapSideSite",
+      "return_type": "mjsSite *",
+      "params": [
+        {
+          "name": "wrap",
+          "c_type": "const mjsWrap *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1836
+    },
+    "mjs_getWrapDivisor": {
+      "name": "mjs_getWrapDivisor",
+      "return_type": "double",
+      "params": [
+        {
+          "name": "wrap",
+          "c_type": "const mjsWrap *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1839
+    },
+    "mjs_getWrapCoef": {
+      "name": "mjs_getWrapCoef",
+      "return_type": "double",
+      "params": [
+        {
+          "name": "wrap",
+          "c_type": "const mjsWrap *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1842
+    },
+    "mjs_setName": {
+      "name": "mjs_setName",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null
+        },
+        {
+          "name": "name",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1847
+    },
+    "mjs_setBuffer": {
+      "name": "mjs_setBuffer",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "dest",
+          "c_type": "mjByteVec *",
+          "array_dim": null
+        },
+        {
+          "name": "array",
+          "c_type": "const void *",
+          "array_dim": null
+        },
+        {
+          "name": "size",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1850
+    },
+    "mjs_setString": {
+      "name": "mjs_setString",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "dest",
+          "c_type": "mjString *",
+          "array_dim": null
+        },
+        {
+          "name": "text",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1853
+    },
+    "mjs_setStringVec": {
+      "name": "mjs_setStringVec",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "dest",
+          "c_type": "mjStringVec *",
+          "array_dim": null
+        },
+        {
+          "name": "text",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1856
+    },
+    "mjs_setInStringVec": {
+      "name": "mjs_setInStringVec",
+      "return_type": "mjtByte",
+      "params": [
+        {
+          "name": "dest",
+          "c_type": "mjStringVec *",
+          "array_dim": null
+        },
+        {
+          "name": "i",
+          "c_type": "int",
+          "array_dim": null
+        },
+        {
+          "name": "text",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1859
+    },
+    "mjs_appendString": {
+      "name": "mjs_appendString",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "dest",
+          "c_type": "mjStringVec *",
+          "array_dim": null
+        },
+        {
+          "name": "text",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1862
+    },
+    "mjs_setInt": {
+      "name": "mjs_setInt",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "dest",
+          "c_type": "mjIntVec *",
+          "array_dim": null
+        },
+        {
+          "name": "array",
+          "c_type": "const int *",
+          "array_dim": null
+        },
+        {
+          "name": "size",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1865
+    },
+    "mjs_appendIntVec": {
+      "name": "mjs_appendIntVec",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "dest",
+          "c_type": "mjIntVecVec *",
+          "array_dim": null
+        },
+        {
+          "name": "array",
+          "c_type": "const int *",
+          "array_dim": null
+        },
+        {
+          "name": "size",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1868
+    },
+    "mjs_setFloat": {
+      "name": "mjs_setFloat",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "dest",
+          "c_type": "mjFloatVec *",
+          "array_dim": null
+        },
+        {
+          "name": "array",
+          "c_type": "const float *",
+          "array_dim": null
+        },
+        {
+          "name": "size",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1871
+    },
+    "mjs_appendFloatVec": {
+      "name": "mjs_appendFloatVec",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "dest",
+          "c_type": "mjFloatVecVec *",
+          "array_dim": null
+        },
+        {
+          "name": "array",
+          "c_type": "const float *",
+          "array_dim": null
+        },
+        {
+          "name": "size",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1874
+    },
+    "mjs_setDouble": {
+      "name": "mjs_setDouble",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "dest",
+          "c_type": "mjDoubleVec *",
+          "array_dim": null
+        },
+        {
+          "name": "array",
+          "c_type": "const double *",
+          "array_dim": null
+        },
+        {
+          "name": "size",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1877
+    },
+    "mjs_setPluginAttributes": {
+      "name": "mjs_setPluginAttributes",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "plugin",
+          "c_type": "mjsPlugin *",
+          "array_dim": null
+        },
+        {
+          "name": "attributes",
+          "c_type": "void *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1880
+    },
+    "mjs_getName": {
+      "name": "mjs_getName",
+      "return_type": "mjString *",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1886
+    },
+    "mjs_getString": {
+      "name": "mjs_getString",
+      "return_type": "const char *",
+      "params": [
+        {
+          "name": "source",
+          "c_type": "const mjString *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1889
+    },
+    "mjs_getDouble": {
+      "name": "mjs_getDouble",
+      "return_type": "const double *",
+      "params": [
+        {
+          "name": "source",
+          "c_type": "const mjDoubleVec *",
+          "array_dim": null
+        },
+        {
+          "name": "size",
+          "c_type": "int *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1893
+    },
+    "mjs_getWrapNum": {
+      "name": "mjs_getWrapNum",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "tendonspec",
+          "c_type": "const mjsTendon *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1896
+    },
+    "mjs_getWrap": {
+      "name": "mjs_getWrap",
+      "return_type": "mjsWrap *",
+      "params": [
+        {
+          "name": "tendonspec",
+          "c_type": "const mjsTendon *",
+          "array_dim": null
+        },
+        {
+          "name": "i",
+          "c_type": "int",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1899
+    },
+    "mjs_getPluginAttributes": {
+      "name": "mjs_getPluginAttributes",
+      "return_type": "const void *",
+      "params": [
+        {
+          "name": "plugin",
+          "c_type": "const mjsPlugin *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1902
+    },
+    "mjs_setDefault": {
+      "name": "mjs_setDefault",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null
+        },
+        {
+          "name": "def",
+          "c_type": "const mjsDefault *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1908
+    },
+    "mjs_setFrame": {
+      "name": "mjs_setFrame",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "dest",
+          "c_type": "mjsElement *",
+          "array_dim": null
+        },
+        {
+          "name": "frame",
+          "c_type": "mjsFrame *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1911
+    },
+    "mjs_resolveOrientation": {
+      "name": "mjs_resolveOrientation",
+      "return_type": "const char *",
+      "params": [
+        {
+          "name": "quat",
+          "c_type": "double [4]",
+          "array_dim": 4
+        },
+        {
+          "name": "degree",
+          "c_type": "mjtByte",
+          "array_dim": null
+        },
+        {
+          "name": "sequence",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "orientation",
+          "c_type": "const mjsOrientation *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1914
+    },
+    "mjs_bodyToFrame": {
+      "name": "mjs_bodyToFrame",
+      "return_type": "mjsFrame *",
+      "params": [
+        {
+          "name": "body",
+          "c_type": "mjsBody **",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1918
+    },
+    "mjs_setUserValue": {
+      "name": "mjs_setUserValue",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null
+        },
+        {
+          "name": "key",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "data",
+          "c_type": "const void *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1921
+    },
+    "mjs_setUserValueWithCleanup": {
+      "name": "mjs_setUserValueWithCleanup",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null
+        },
+        {
+          "name": "key",
+          "c_type": "const char *",
+          "array_dim": null
+        },
+        {
+          "name": "data",
+          "c_type": "const void *",
+          "array_dim": null
+        },
+        {
+          "name": "cleanup",
+          "c_type": "void (*)(const void *)",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1926
+    },
+    "mjs_getUserValue": {
+      "name": "mjs_getUserValue",
+      "return_type": "const void *",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null
+        },
+        {
+          "name": "key",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1931
+    },
+    "mjs_deleteUserValue": {
+      "name": "mjs_deleteUserValue",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null
+        },
+        {
+          "name": "key",
+          "c_type": "const char *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1934
+    },
+    "mjs_sensorDim": {
+      "name": "mjs_sensorDim",
+      "return_type": "int",
+      "params": [
+        {
+          "name": "sensor",
+          "c_type": "const mjsSensor *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1937
+    },
+    "mjs_defaultSpec": {
+      "name": "mjs_defaultSpec",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "spec",
+          "c_type": "mjSpec *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1942
+    },
+    "mjs_defaultOrientation": {
+      "name": "mjs_defaultOrientation",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "orient",
+          "c_type": "mjsOrientation *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1945
+    },
+    "mjs_defaultBody": {
+      "name": "mjs_defaultBody",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "body",
+          "c_type": "mjsBody *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1948
+    },
+    "mjs_defaultFrame": {
+      "name": "mjs_defaultFrame",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "frame",
+          "c_type": "mjsFrame *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1951
+    },
+    "mjs_defaultJoint": {
+      "name": "mjs_defaultJoint",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "joint",
+          "c_type": "mjsJoint *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1954
+    },
+    "mjs_defaultGeom": {
+      "name": "mjs_defaultGeom",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "geom",
+          "c_type": "mjsGeom *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1957
+    },
+    "mjs_defaultSite": {
+      "name": "mjs_defaultSite",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "site",
+          "c_type": "mjsSite *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1960
+    },
+    "mjs_defaultCamera": {
+      "name": "mjs_defaultCamera",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "camera",
+          "c_type": "mjsCamera *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1963
+    },
+    "mjs_defaultLight": {
+      "name": "mjs_defaultLight",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "light",
+          "c_type": "mjsLight *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1966
+    },
+    "mjs_defaultFlex": {
+      "name": "mjs_defaultFlex",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "flex",
+          "c_type": "mjsFlex *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1969
+    },
+    "mjs_defaultMesh": {
+      "name": "mjs_defaultMesh",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "mesh",
+          "c_type": "mjsMesh *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1972
+    },
+    "mjs_defaultHField": {
+      "name": "mjs_defaultHField",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "hfield",
+          "c_type": "mjsHField *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1975
+    },
+    "mjs_defaultSkin": {
+      "name": "mjs_defaultSkin",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "skin",
+          "c_type": "mjsSkin *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1978
+    },
+    "mjs_defaultTexture": {
+      "name": "mjs_defaultTexture",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "texture",
+          "c_type": "mjsTexture *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1981
+    },
+    "mjs_defaultMaterial": {
+      "name": "mjs_defaultMaterial",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "material",
+          "c_type": "mjsMaterial *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1984
+    },
+    "mjs_defaultPair": {
+      "name": "mjs_defaultPair",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "pair",
+          "c_type": "mjsPair *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1987
+    },
+    "mjs_defaultEquality": {
+      "name": "mjs_defaultEquality",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "equality",
+          "c_type": "mjsEquality *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1990
+    },
+    "mjs_defaultTendon": {
+      "name": "mjs_defaultTendon",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "tendon",
+          "c_type": "mjsTendon *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1993
+    },
+    "mjs_defaultActuator": {
+      "name": "mjs_defaultActuator",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "actuator",
+          "c_type": "mjsActuator *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1996
+    },
+    "mjs_defaultSensor": {
+      "name": "mjs_defaultSensor",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "sensor",
+          "c_type": "mjsSensor *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1999
+    },
+    "mjs_defaultNumeric": {
+      "name": "mjs_defaultNumeric",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "numeric",
+          "c_type": "mjsNumeric *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 2002
+    },
+    "mjs_defaultText": {
+      "name": "mjs_defaultText",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "text",
+          "c_type": "mjsText *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 2005
+    },
+    "mjs_defaultTuple": {
+      "name": "mjs_defaultTuple",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "tuple",
+          "c_type": "mjsTuple *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 2008
+    },
+    "mjs_defaultKey": {
+      "name": "mjs_defaultKey",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "key",
+          "c_type": "mjsKey *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 2011
+    },
+    "mjs_defaultPlugin": {
+      "name": "mjs_defaultPlugin",
+      "return_type": "void",
+      "params": [
+        {
+          "name": "plugin",
+          "c_type": "mjsPlugin *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 2014
+    },
+    "mjs_asBody": {
+      "name": "mjs_asBody",
+      "return_type": "mjsBody *",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 2020
+    },
+    "mjs_asGeom": {
+      "name": "mjs_asGeom",
+      "return_type": "mjsGeom *",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 2023
+    },
+    "mjs_asJoint": {
+      "name": "mjs_asJoint",
+      "return_type": "mjsJoint *",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 2026
+    },
+    "mjs_asSite": {
+      "name": "mjs_asSite",
+      "return_type": "mjsSite *",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 2029
+    },
+    "mjs_asCamera": {
+      "name": "mjs_asCamera",
+      "return_type": "mjsCamera *",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 2032
+    },
+    "mjs_asLight": {
+      "name": "mjs_asLight",
+      "return_type": "mjsLight *",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 2035
+    },
+    "mjs_asFrame": {
+      "name": "mjs_asFrame",
+      "return_type": "mjsFrame *",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 2038
+    },
+    "mjs_asActuator": {
+      "name": "mjs_asActuator",
+      "return_type": "mjsActuator *",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 2041
+    },
+    "mjs_asSensor": {
+      "name": "mjs_asSensor",
+      "return_type": "mjsSensor *",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 2044
+    },
+    "mjs_asFlex": {
+      "name": "mjs_asFlex",
+      "return_type": "mjsFlex *",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 2047
+    },
+    "mjs_asPair": {
+      "name": "mjs_asPair",
+      "return_type": "mjsPair *",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 2050
+    },
+    "mjs_asEquality": {
+      "name": "mjs_asEquality",
+      "return_type": "mjsEquality *",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 2053
+    },
+    "mjs_asExclude": {
+      "name": "mjs_asExclude",
+      "return_type": "mjsExclude *",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 2056
+    },
+    "mjs_asTendon": {
+      "name": "mjs_asTendon",
+      "return_type": "mjsTendon *",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 2059
+    },
+    "mjs_asNumeric": {
+      "name": "mjs_asNumeric",
+      "return_type": "mjsNumeric *",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 2062
+    },
+    "mjs_asText": {
+      "name": "mjs_asText",
+      "return_type": "mjsText *",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 2065
+    },
+    "mjs_asTuple": {
+      "name": "mjs_asTuple",
+      "return_type": "mjsTuple *",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 2068
+    },
+    "mjs_asKey": {
+      "name": "mjs_asKey",
+      "return_type": "mjsKey *",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 2071
+    },
+    "mjs_asMesh": {
+      "name": "mjs_asMesh",
+      "return_type": "mjsMesh *",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 2074
+    },
+    "mjs_asHField": {
+      "name": "mjs_asHField",
+      "return_type": "mjsHField *",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 2077
+    },
+    "mjs_asSkin": {
+      "name": "mjs_asSkin",
+      "return_type": "mjsSkin *",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 2080
+    },
+    "mjs_asTexture": {
+      "name": "mjs_asTexture",
+      "return_type": "mjsTexture *",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 2083
+    },
+    "mjs_asMaterial": {
+      "name": "mjs_asMaterial",
+      "return_type": "mjsMaterial *",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 2086
+    },
+    "mjs_asPlugin": {
+      "name": "mjs_asPlugin",
+      "return_type": "mjsPlugin *",
+      "params": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 2089
+    }
+  },
+  "enums": {
+    "mjtDisableBit": {
+      "name": "mjtDisableBit",
+      "members": {
+        "mjDSBL_CONSTRAINT": 1,
+        "mjDSBL_EQUALITY": 2,
+        "mjDSBL_FRICTIONLOSS": 4,
+        "mjDSBL_LIMIT": 8,
+        "mjDSBL_CONTACT": 16,
+        "mjDSBL_SPRING": 32,
+        "mjDSBL_DAMPER": 64,
+        "mjDSBL_GRAVITY": 128,
+        "mjDSBL_CLAMPCTRL": 256,
+        "mjDSBL_WARMSTART": 512,
+        "mjDSBL_FILTERPARENT": 1024,
+        "mjDSBL_ACTUATION": 2048,
+        "mjDSBL_REFSAFE": 4096,
+        "mjDSBL_SENSOR": 8192,
+        "mjDSBL_MIDPHASE": 16384,
+        "mjDSBL_EULERDAMP": 32768,
+        "mjDSBL_AUTORESET": 65536,
+        "mjDSBL_NATIVECCD": 131072,
+        "mjDSBL_ISLAND": 262144,
+        "mjDSBL_MULTICCD": 524288,
+        "mjNDISABLE": 20
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 53
+    },
+    "mjtEnableBit": {
+      "name": "mjtEnableBit",
+      "members": {
+        "mjENBL_OVERRIDE": 1,
+        "mjENBL_ENERGY": 2,
+        "mjENBL_FWDINV": 4,
+        "mjENBL_INVDISCRETE": 8,
+        "mjENBL_SLEEP": 16,
+        "mjNENABLE": 5
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 79
+    },
+    "mjtJoint": {
+      "name": "mjtJoint",
+      "members": {
+        "mjJNT_FREE": 0,
+        "mjJNT_BALL": 1,
+        "mjJNT_SLIDE": 2,
+        "mjJNT_HINGE": 3
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 91
+    },
+    "mjtGeom": {
+      "name": "mjtGeom",
+      "members": {
+        "mjGEOM_PLANE": 0,
+        "mjGEOM_HFIELD": 1,
+        "mjGEOM_SPHERE": 2,
+        "mjGEOM_CAPSULE": 3,
+        "mjGEOM_ELLIPSOID": 4,
+        "mjGEOM_CYLINDER": 5,
+        "mjGEOM_BOX": 6,
+        "mjGEOM_MESH": 7,
+        "mjGEOM_SDF": 8,
+        "mjNGEOMTYPES": 9,
+        "mjGEOM_ARROW": 100,
+        "mjGEOM_ARROW1": 101,
+        "mjGEOM_ARROW2": 102,
+        "mjGEOM_LINE": 103,
+        "mjGEOM_LINEBOX": 104,
+        "mjGEOM_FLEX": 105,
+        "mjGEOM_SKIN": 106,
+        "mjGEOM_LABEL": 107,
+        "mjGEOM_TRIANGLE": 108,
+        "mjGEOM_NONE": 1001
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 99
+    },
+    "mjtProjection": {
+      "name": "mjtProjection",
+      "members": {
+        "mjPROJ_PERSPECTIVE": 0,
+        "mjPROJ_ORTHOGRAPHIC": 1
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 128
+    },
+    "mjtCamLight": {
+      "name": "mjtCamLight",
+      "members": {
+        "mjCAMLIGHT_FIXED": 0,
+        "mjCAMLIGHT_TRACK": 1,
+        "mjCAMLIGHT_TRACKCOM": 2,
+        "mjCAMLIGHT_TARGETBODY": 3,
+        "mjCAMLIGHT_TARGETBODYCOM": 4
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 134
+    },
+    "mjtLightType": {
+      "name": "mjtLightType",
+      "members": {
+        "mjLIGHT_SPOT": 0,
+        "mjLIGHT_DIRECTIONAL": 1,
+        "mjLIGHT_POINT": 2,
+        "mjLIGHT_IMAGE": 3
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 143
+    },
+    "mjtTexture": {
+      "name": "mjtTexture",
+      "members": {
+        "mjTEXTURE_2D": 0,
+        "mjTEXTURE_CUBE": 1,
+        "mjTEXTURE_SKYBOX": 2
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 151
+    },
+    "mjtTextureRole": {
+      "name": "mjtTextureRole",
+      "members": {
+        "mjTEXROLE_USER": 0,
+        "mjTEXROLE_RGB": 1,
+        "mjTEXROLE_OCCLUSION": 2,
+        "mjTEXROLE_ROUGHNESS": 3,
+        "mjTEXROLE_METALLIC": 4,
+        "mjTEXROLE_NORMAL": 5,
+        "mjTEXROLE_OPACITY": 6,
+        "mjTEXROLE_EMISSIVE": 7,
+        "mjTEXROLE_RGBA": 8,
+        "mjTEXROLE_ORM": 9,
+        "mjNTEXROLE": 10
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 158
+    },
+    "mjtColorSpace": {
+      "name": "mjtColorSpace",
+      "members": {
+        "mjCOLORSPACE_AUTO": 0,
+        "mjCOLORSPACE_LINEAR": 1,
+        "mjCOLORSPACE_SRGB": 2
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 173
+    },
+    "mjtIntegrator": {
+      "name": "mjtIntegrator",
+      "members": {
+        "mjINT_EULER": 0,
+        "mjINT_RK4": 1,
+        "mjINT_IMPLICIT": 2,
+        "mjINT_IMPLICITFAST": 3
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 180
+    },
+    "mjtCone": {
+      "name": "mjtCone",
+      "members": {
+        "mjCONE_PYRAMIDAL": 0,
+        "mjCONE_ELLIPTIC": 1
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 188
+    },
+    "mjtJacobian": {
+      "name": "mjtJacobian",
+      "members": {
+        "mjJAC_DENSE": 0,
+        "mjJAC_SPARSE": 1,
+        "mjJAC_AUTO": 2
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 194
+    },
+    "mjtSolver": {
+      "name": "mjtSolver",
+      "members": {
+        "mjSOL_PGS": 0,
+        "mjSOL_CG": 1,
+        "mjSOL_NEWTON": 2
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 201
+    },
+    "mjtEq": {
+      "name": "mjtEq",
+      "members": {
+        "mjEQ_CONNECT": 0,
+        "mjEQ_WELD": 1,
+        "mjEQ_JOINT": 2,
+        "mjEQ_TENDON": 3,
+        "mjEQ_FLEX": 4,
+        "mjEQ_FLEXVERT": 5,
+        "mjEQ_FLEXSTRAIN": 6,
+        "mjEQ_DISTANCE": 7
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 208
+    },
+    "mjtWrap": {
+      "name": "mjtWrap",
+      "members": {
+        "mjWRAP_NONE": 0,
+        "mjWRAP_JOINT": 1,
+        "mjWRAP_PULLEY": 2,
+        "mjWRAP_SITE": 3,
+        "mjWRAP_SPHERE": 4,
+        "mjWRAP_CYLINDER": 5
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 220
+    },
+    "mjtTrn": {
+      "name": "mjtTrn",
+      "members": {
+        "mjTRN_JOINT": 0,
+        "mjTRN_JOINTINPARENT": 1,
+        "mjTRN_SLIDERCRANK": 2,
+        "mjTRN_TENDON": 3,
+        "mjTRN_SITE": 4,
+        "mjTRN_BODY": 5,
+        "mjTRN_UNDEFINED": 1000
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 230
+    },
+    "mjtDyn": {
+      "name": "mjtDyn",
+      "members": {
+        "mjDYN_NONE": 0,
+        "mjDYN_INTEGRATOR": 1,
+        "mjDYN_FILTER": 2,
+        "mjDYN_FILTEREXACT": 3,
+        "mjDYN_MUSCLE": 4,
+        "mjDYN_DCMOTOR": 5,
+        "mjDYN_USER": 6
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 242
+    },
+    "mjtGain": {
+      "name": "mjtGain",
+      "members": {
+        "mjGAIN_FIXED": 0,
+        "mjGAIN_AFFINE": 1,
+        "mjGAIN_MUSCLE": 2,
+        "mjGAIN_DCMOTOR": 3,
+        "mjGAIN_USER": 4
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 253
+    },
+    "mjtBias": {
+      "name": "mjtBias",
+      "members": {
+        "mjBIAS_NONE": 0,
+        "mjBIAS_AFFINE": 1,
+        "mjBIAS_MUSCLE": 2,
+        "mjBIAS_DCMOTOR": 3,
+        "mjBIAS_USER": 4
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 262
+    },
+    "mjtObj": {
+      "name": "mjtObj",
+      "members": {
+        "mjOBJ_UNKNOWN": 0,
+        "mjOBJ_BODY": 1,
+        "mjOBJ_XBODY": 2,
+        "mjOBJ_JOINT": 3,
+        "mjOBJ_DOF": 4,
+        "mjOBJ_GEOM": 5,
+        "mjOBJ_SITE": 6,
+        "mjOBJ_CAMERA": 7,
+        "mjOBJ_LIGHT": 8,
+        "mjOBJ_FLEX": 9,
+        "mjOBJ_MESH": 10,
+        "mjOBJ_SKIN": 11,
+        "mjOBJ_HFIELD": 12,
+        "mjOBJ_TEXTURE": 13,
+        "mjOBJ_MATERIAL": 14,
+        "mjOBJ_PAIR": 15,
+        "mjOBJ_EXCLUDE": 16,
+        "mjOBJ_EQUALITY": 17,
+        "mjOBJ_TENDON": 18,
+        "mjOBJ_ACTUATOR": 19,
+        "mjOBJ_SENSOR": 20,
+        "mjOBJ_NUMERIC": 21,
+        "mjOBJ_TEXT": 22,
+        "mjOBJ_TUPLE": 23,
+        "mjOBJ_KEY": 24,
+        "mjOBJ_PLUGIN": 25,
+        "mjNOBJECT": 26,
+        "mjOBJ_FRAME": 100,
+        "mjOBJ_DEFAULT": 101,
+        "mjOBJ_MODEL": 102
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 271
+    },
+    "mjtSensor": {
+      "name": "mjtSensor",
+      "members": {
+        "mjSENS_TOUCH": 0,
+        "mjSENS_ACCELEROMETER": 1,
+        "mjSENS_VELOCIMETER": 2,
+        "mjSENS_GYRO": 3,
+        "mjSENS_FORCE": 4,
+        "mjSENS_TORQUE": 5,
+        "mjSENS_MAGNETOMETER": 6,
+        "mjSENS_RANGEFINDER": 7,
+        "mjSENS_CAMPROJECTION": 8,
+        "mjSENS_JOINTPOS": 9,
+        "mjSENS_JOINTVEL": 10,
+        "mjSENS_TENDONPOS": 11,
+        "mjSENS_TENDONVEL": 12,
+        "mjSENS_ACTUATORPOS": 13,
+        "mjSENS_ACTUATORVEL": 14,
+        "mjSENS_ACTUATORFRC": 15,
+        "mjSENS_JOINTACTFRC": 16,
+        "mjSENS_TENDONACTFRC": 17,
+        "mjSENS_BALLQUAT": 18,
+        "mjSENS_BALLANGVEL": 19,
+        "mjSENS_JOINTLIMITPOS": 20,
+        "mjSENS_JOINTLIMITVEL": 21,
+        "mjSENS_JOINTLIMITFRC": 22,
+        "mjSENS_TENDONLIMITPOS": 23,
+        "mjSENS_TENDONLIMITVEL": 24,
+        "mjSENS_TENDONLIMITFRC": 25,
+        "mjSENS_FRAMEPOS": 26,
+        "mjSENS_FRAMEQUAT": 27,
+        "mjSENS_FRAMEXAXIS": 28,
+        "mjSENS_FRAMEYAXIS": 29,
+        "mjSENS_FRAMEZAXIS": 30,
+        "mjSENS_FRAMELINVEL": 31,
+        "mjSENS_FRAMEANGVEL": 32,
+        "mjSENS_FRAMELINACC": 33,
+        "mjSENS_FRAMEANGACC": 34,
+        "mjSENS_SUBTREECOM": 35,
+        "mjSENS_SUBTREELINVEL": 36,
+        "mjSENS_SUBTREEANGMOM": 37,
+        "mjSENS_INSIDESITE": 38,
+        "mjSENS_GEOMDIST": 39,
+        "mjSENS_GEOMNORMAL": 40,
+        "mjSENS_GEOMFROMTO": 41,
+        "mjSENS_CONTACT": 42,
+        "mjSENS_E_POTENTIAL": 43,
+        "mjSENS_E_KINETIC": 44,
+        "mjSENS_CLOCK": 45,
+        "mjSENS_TACTILE": 46,
+        "mjSENS_PLUGIN": 47,
+        "mjSENS_USER": 48
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 308
+    },
+    "mjtStage": {
+      "name": "mjtStage",
+      "members": {
+        "mjSTAGE_NONE": 0,
+        "mjSTAGE_POS": 1,
+        "mjSTAGE_VEL": 2,
+        "mjSTAGE_ACC": 3
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 384
+    },
+    "mjtDataType": {
+      "name": "mjtDataType",
+      "members": {
+        "mjDATATYPE_REAL": 0,
+        "mjDATATYPE_POSITIVE": 1,
+        "mjDATATYPE_AXIS": 2,
+        "mjDATATYPE_QUATERNION": 3
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 392
+    },
+    "mjtConDataField": {
+      "name": "mjtConDataField",
+      "members": {
+        "mjCONDATA_FOUND": 0,
+        "mjCONDATA_FORCE": 1,
+        "mjCONDATA_TORQUE": 2,
+        "mjCONDATA_DIST": 3,
+        "mjCONDATA_POS": 4,
+        "mjCONDATA_NORMAL": 5,
+        "mjCONDATA_TANGENT": 6,
+        "mjNCONDATA": 7
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 400
+    },
+    "mjtRayDataField": {
+      "name": "mjtRayDataField",
+      "members": {
+        "mjRAYDATA_DIST": 0,
+        "mjRAYDATA_DIR": 1,
+        "mjRAYDATA_ORIGIN": 2,
+        "mjRAYDATA_POINT": 3,
+        "mjRAYDATA_NORMAL": 4,
+        "mjRAYDATA_DEPTH": 5,
+        "mjNRAYDATA": 6
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 413
+    },
+    "mjtCamOutBit": {
+      "name": "mjtCamOutBit",
+      "members": {
+        "mjCAMOUT_RGB": 1,
+        "mjCAMOUT_DEPTH": 2,
+        "mjCAMOUT_DIST": 4,
+        "mjCAMOUT_NORMAL": 8,
+        "mjCAMOUT_SEG": 16,
+        "mjNCAMOUT": 5
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 425
+    },
+    "mjtSameFrame": {
+      "name": "mjtSameFrame",
+      "members": {
+        "mjSAMEFRAME_NONE": 0,
+        "mjSAMEFRAME_BODY": 1,
+        "mjSAMEFRAME_INERTIA": 2,
+        "mjSAMEFRAME_BODYROT": 3,
+        "mjSAMEFRAME_INERTIAROT": 4
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 436
+    },
+    "mjtSleepPolicy": {
+      "name": "mjtSleepPolicy",
+      "members": {
+        "mjSLEEP_AUTO": 0,
+        "mjSLEEP_AUTO_NEVER": 1,
+        "mjSLEEP_AUTO_ALLOWED": 2,
+        "mjSLEEP_NEVER": 3,
+        "mjSLEEP_ALLOWED": 4,
+        "mjSLEEP_INIT": 5
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 445
+    },
+    "mjtLRMode": {
+      "name": "mjtLRMode",
+      "members": {
+        "mjLRMODE_NONE": 0,
+        "mjLRMODE_MUSCLE": 1,
+        "mjLRMODE_MUSCLEUSER": 2,
+        "mjLRMODE_ALL": 3
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 455
+    },
+    "mjtFlexSelf": {
+      "name": "mjtFlexSelf",
+      "members": {
+        "mjFLEXSELF_NONE": 0,
+        "mjFLEXSELF_NARROW": 1,
+        "mjFLEXSELF_BVH": 2,
+        "mjFLEXSELF_SAP": 3,
+        "mjFLEXSELF_AUTO": 4
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 463
+    },
+    "mjtSDFType": {
+      "name": "mjtSDFType",
+      "members": {
+        "mjSDFTYPE_SINGLE": 0,
+        "mjSDFTYPE_INTERSECTION": 1,
+        "mjSDFTYPE_MIDSURFACE": 2,
+        "mjSDFTYPE_COLLISION": 3
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 472
+    },
+    "mjtGeomInertia": {
+      "name": "mjtGeomInertia",
+      "members": {
+        "mjINERTIA_VOLUME": 0,
+        "mjINERTIA_SHELL": 1
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 60
+    },
+    "mjtMeshInertia": {
+      "name": "mjtMeshInertia",
+      "members": {
+        "mjMESH_INERTIA_CONVEX": 0,
+        "mjMESH_INERTIA_EXACT": 1,
+        "mjMESH_INERTIA_LEGACY": 2,
+        "mjMESH_INERTIA_SHELL": 3
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 66
+    },
+    "mjtMeshBuiltin": {
+      "name": "mjtMeshBuiltin",
+      "members": {
+        "mjMESH_BUILTIN_NONE": 0,
+        "mjMESH_BUILTIN_SPHERE": 1,
+        "mjMESH_BUILTIN_HEMISPHERE": 2,
+        "mjMESH_BUILTIN_CONE": 3,
+        "mjMESH_BUILTIN_SUPERSPHERE": 4,
+        "mjMESH_BUILTIN_SUPERTORUS": 5,
+        "mjMESH_BUILTIN_WEDGE": 6,
+        "mjMESH_BUILTIN_PLATE": 7
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 74
+    },
+    "mjtBuiltin": {
+      "name": "mjtBuiltin",
+      "members": {
+        "mjBUILTIN_NONE": 0,
+        "mjBUILTIN_GRADIENT": 1,
+        "mjBUILTIN_CHECKER": 2,
+        "mjBUILTIN_FLAT": 3
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 86
+    },
+    "mjtMark": {
+      "name": "mjtMark",
+      "members": {
+        "mjMARK_NONE": 0,
+        "mjMARK_EDGE": 1,
+        "mjMARK_CROSS": 2,
+        "mjMARK_RANDOM": 3
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 94
+    },
+    "mjtLimited": {
+      "name": "mjtLimited",
+      "members": {
+        "mjLIMITED_FALSE": 0,
+        "mjLIMITED_TRUE": 1,
+        "mjLIMITED_AUTO": 2
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 102
+    },
+    "mjtAlignFree": {
+      "name": "mjtAlignFree",
+      "members": {
+        "mjALIGNFREE_FALSE": 0,
+        "mjALIGNFREE_TRUE": 1,
+        "mjALIGNFREE_AUTO": 2
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 109
+    },
+    "mjtInertiaFromGeom": {
+      "name": "mjtInertiaFromGeom",
+      "members": {
+        "mjINERTIAFROMGEOM_FALSE": 0,
+        "mjINERTIAFROMGEOM_TRUE": 1,
+        "mjINERTIAFROMGEOM_AUTO": 2
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 116
+    },
+    "mjtOrientation": {
+      "name": "mjtOrientation",
+      "members": {
+        "mjORIENTATION_QUAT": 0,
+        "mjORIENTATION_AXISANGLE": 1,
+        "mjORIENTATION_XYAXES": 2,
+        "mjORIENTATION_ZAXIS": 3,
+        "mjORIENTATION_EULER": 4
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 123
+    },
+    "mjtTaskStatus": {
+      "name": "mjtTaskStatus",
+      "members": {
+        "mjTASK_NEW": 0,
+        "mjTASK_QUEUED": 1,
+        "mjTASK_COMPLETED": 2
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjthread.h",
+      "line": 20
+    },
+    "mjtState": {
+      "name": "mjtState",
+      "members": {
+        "mjSTATE_TIME": 1,
+        "mjSTATE_QPOS": 2,
+        "mjSTATE_QVEL": 4,
+        "mjSTATE_ACT": 8,
+        "mjSTATE_HISTORY": 16,
+        "mjSTATE_WARMSTART": 32,
+        "mjSTATE_CTRL": 64,
+        "mjSTATE_QFRC_APPLIED": 128,
+        "mjSTATE_XFRC_APPLIED": 256,
+        "mjSTATE_EQ_ACTIVE": 512,
+        "mjSTATE_MOCAP_POS": 1024,
+        "mjSTATE_MOCAP_QUAT": 2048,
+        "mjSTATE_USERDATA": 4096,
+        "mjSTATE_PLUGIN": 8192,
+        "mjNSTATE": 14,
+        "mjSTATE_PHYSICS": 30,
+        "mjSTATE_FULLPHYSICS": 8223,
+        "mjSTATE_USER": 8128,
+        "mjSTATE_INTEGRATION": 16383
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjdata.h",
+      "line": 27
+    },
+    "mjtConstraint": {
+      "name": "mjtConstraint",
+      "members": {
+        "mjCNSTR_EQUALITY": 0,
+        "mjCNSTR_FRICTION_DOF": 1,
+        "mjCNSTR_FRICTION_TENDON": 2,
+        "mjCNSTR_LIMIT_JOINT": 3,
+        "mjCNSTR_LIMIT_TENDON": 4,
+        "mjCNSTR_CONTACT_FRICTIONLESS": 5,
+        "mjCNSTR_CONTACT_PYRAMIDAL": 6,
+        "mjCNSTR_CONTACT_ELLIPTIC": 7
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjdata.h",
+      "line": 55
+    },
+    "mjtConstraintState": {
+      "name": "mjtConstraintState",
+      "members": {
+        "mjCNSTRSTATE_SATISFIED": 0,
+        "mjCNSTRSTATE_QUADRATIC": 1,
+        "mjCNSTRSTATE_LINEARNEG": 2,
+        "mjCNSTRSTATE_LINEARPOS": 3,
+        "mjCNSTRSTATE_CONE": 4
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjdata.h",
+      "line": 67
+    },
+    "mjtWarning": {
+      "name": "mjtWarning",
+      "members": {
+        "mjWARN_INERTIA": 0,
+        "mjWARN_CONTACTFULL": 1,
+        "mjWARN_CNSTRFULL": 2,
+        "mjWARN_BADQPOS": 3,
+        "mjWARN_BADQVEL": 4,
+        "mjWARN_BADQACC": 5,
+        "mjWARN_BADCTRL": 6,
+        "mjNWARNING": 7
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjdata.h",
+      "line": 76
+    },
+    "mjtTimer": {
+      "name": "mjtTimer",
+      "members": {
+        "mjTIMER_STEP": 0,
+        "mjTIMER_FORWARD": 1,
+        "mjTIMER_INVERSE": 2,
+        "mjTIMER_POSITION": 3,
+        "mjTIMER_VELOCITY": 4,
+        "mjTIMER_ACTUATION": 5,
+        "mjTIMER_CONSTRAINT": 6,
+        "mjTIMER_ADVANCE": 7,
+        "mjTIMER_POS_KINEMATICS": 8,
+        "mjTIMER_POS_INERTIA": 9,
+        "mjTIMER_POS_COLLISION": 10,
+        "mjTIMER_POS_MAKE": 11,
+        "mjTIMER_POS_PROJECT": 12,
+        "mjTIMER_COL_BROAD": 13,
+        "mjTIMER_COL_NARROW": 14,
+        "mjNTIMER": 15
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjdata.h",
+      "line": 89
+    },
+    "mjtSleepState": {
+      "name": "mjtSleepState",
+      "members": {
+        "mjS_STATIC": -1,
+        "mjS_ASLEEP": 0,
+        "mjS_AWAKE": 1
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjdata.h",
+      "line": 117
+    },
+    "mjtCatBit": {
+      "name": "mjtCatBit",
+      "members": {
+        "mjCAT_STATIC": 1,
+        "mjCAT_DYNAMIC": 2,
+        "mjCAT_DECOR": 4,
+        "mjCAT_ALL": 7
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjvisualize.h",
+      "line": 33
+    },
+    "mjtMouse": {
+      "name": "mjtMouse",
+      "members": {
+        "mjMOUSE_NONE": 0,
+        "mjMOUSE_ROTATE_V": 1,
+        "mjMOUSE_ROTATE_H": 2,
+        "mjMOUSE_MOVE_V": 3,
+        "mjMOUSE_MOVE_H": 4,
+        "mjMOUSE_ZOOM": 5,
+        "mjMOUSE_MOVE_V_REL": 6,
+        "mjMOUSE_MOVE_H_REL": 7
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjvisualize.h",
+      "line": 41
+    },
+    "mjtPertBit": {
+      "name": "mjtPertBit",
+      "members": {
+        "mjPERT_TRANSLATE": 1,
+        "mjPERT_ROTATE": 2
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjvisualize.h",
+      "line": 53
+    },
+    "mjtCamera": {
+      "name": "mjtCamera",
+      "members": {
+        "mjCAMERA_FREE": 0,
+        "mjCAMERA_TRACKING": 1,
+        "mjCAMERA_FIXED": 2,
+        "mjCAMERA_USER": 3
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjvisualize.h",
+      "line": 59
+    },
+    "mjtLabel": {
+      "name": "mjtLabel",
+      "members": {
+        "mjLABEL_NONE": 0,
+        "mjLABEL_BODY": 1,
+        "mjLABEL_JOINT": 2,
+        "mjLABEL_GEOM": 3,
+        "mjLABEL_SITE": 4,
+        "mjLABEL_CAMERA": 5,
+        "mjLABEL_LIGHT": 6,
+        "mjLABEL_TENDON": 7,
+        "mjLABEL_ACTUATOR": 8,
+        "mjLABEL_CONSTRAINT": 9,
+        "mjLABEL_FLEX": 10,
+        "mjLABEL_SKIN": 11,
+        "mjLABEL_SELECTION": 12,
+        "mjLABEL_SELPNT": 13,
+        "mjLABEL_CONTACTPOINT": 14,
+        "mjLABEL_CONTACTFORCE": 15,
+        "mjLABEL_ISLAND": 16,
+        "mjNLABEL": 17
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjvisualize.h",
+      "line": 67
+    },
+    "mjtFrame": {
+      "name": "mjtFrame",
+      "members": {
+        "mjFRAME_NONE": 0,
+        "mjFRAME_BODY": 1,
+        "mjFRAME_GEOM": 2,
+        "mjFRAME_SITE": 3,
+        "mjFRAME_CAMERA": 4,
+        "mjFRAME_LIGHT": 5,
+        "mjFRAME_CONTACT": 6,
+        "mjFRAME_WORLD": 7,
+        "mjNFRAME": 8
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjvisualize.h",
+      "line": 90
+    },
+    "mjtVisFlag": {
+      "name": "mjtVisFlag",
+      "members": {
+        "mjVIS_CONVEXHULL": 0,
+        "mjVIS_TEXTURE": 1,
+        "mjVIS_JOINT": 2,
+        "mjVIS_CAMERA": 3,
+        "mjVIS_ACTUATOR": 4,
+        "mjVIS_ACTIVATION": 5,
+        "mjVIS_LIGHT": 6,
+        "mjVIS_TENDON": 7,
+        "mjVIS_RANGEFINDER": 8,
+        "mjVIS_CONSTRAINT": 9,
+        "mjVIS_INERTIA": 10,
+        "mjVIS_SCLINERTIA": 11,
+        "mjVIS_PERTFORCE": 12,
+        "mjVIS_PERTOBJ": 13,
+        "mjVIS_CONTACTPOINT": 14,
+        "mjVIS_ISLAND": 15,
+        "mjVIS_CONTACTFORCE": 16,
+        "mjVIS_CONTACTSPLIT": 17,
+        "mjVIS_TRANSPARENT": 18,
+        "mjVIS_AUTOCONNECT": 19,
+        "mjVIS_COM": 20,
+        "mjVIS_SELECT": 21,
+        "mjVIS_STATIC": 22,
+        "mjVIS_SKIN": 23,
+        "mjVIS_FLEXVERT": 24,
+        "mjVIS_FLEXEDGE": 25,
+        "mjVIS_FLEXFACE": 26,
+        "mjVIS_FLEXSKIN": 27,
+        "mjVIS_BODYBVH": 28,
+        "mjVIS_MESHBVH": 29,
+        "mjVIS_SDFITER": 30,
+        "mjNVISFLAG": 31
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjvisualize.h",
+      "line": 104
+    },
+    "mjtRndFlag": {
+      "name": "mjtRndFlag",
+      "members": {
+        "mjRND_SHADOW": 0,
+        "mjRND_WIREFRAME": 1,
+        "mjRND_REFLECTION": 2,
+        "mjRND_ADDITIVE": 3,
+        "mjRND_SKYBOX": 4,
+        "mjRND_FOG": 5,
+        "mjRND_HAZE": 6,
+        "mjRND_DEPTH": 7,
+        "mjRND_SEGMENT": 8,
+        "mjRND_IDCOLOR": 9,
+        "mjRND_CULL_FACE": 10,
+        "mjNRNDFLAG": 11
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjvisualize.h",
+      "line": 141
+    },
+    "mjtStereo": {
+      "name": "mjtStereo",
+      "members": {
+        "mjSTEREO_NONE": 0,
+        "mjSTEREO_QUADBUFFERED": 1,
+        "mjSTEREO_SIDEBYSIDE": 2
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjvisualize.h",
+      "line": 158
+    },
+    "mjtPluginCapabilityBit": {
+      "name": "mjtPluginCapabilityBit",
+      "members": {
+        "mjPLUGIN_ACTUATOR": 1,
+        "mjPLUGIN_SENSOR": 2,
+        "mjPLUGIN_PASSIVE": 4,
+        "mjPLUGIN_SDF": 8
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjplugin.h",
+      "line": 107
+    },
+    "mjtGridPos": {
+      "name": "mjtGridPos",
+      "members": {
+        "mjGRID_TOPLEFT": 0,
+        "mjGRID_TOPRIGHT": 1,
+        "mjGRID_BOTTOMLEFT": 2,
+        "mjGRID_BOTTOMRIGHT": 3,
+        "mjGRID_TOP": 4,
+        "mjGRID_BOTTOM": 5,
+        "mjGRID_LEFT": 6,
+        "mjGRID_RIGHT": 7
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjrender.h",
+      "line": 30
+    },
+    "mjtFramebuffer": {
+      "name": "mjtFramebuffer",
+      "members": {
+        "mjFB_WINDOW": 0,
+        "mjFB_OFFSCREEN": 1
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjrender.h",
+      "line": 42
+    },
+    "mjtDepthMap": {
+      "name": "mjtDepthMap",
+      "members": {
+        "mjDEPTH_ZERONEAR": 0,
+        "mjDEPTH_ZEROFAR": 1
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjrender.h",
+      "line": 47
+    },
+    "mjtFontScale": {
+      "name": "mjtFontScale",
+      "members": {
+        "mjFONTSCALE_50": 50,
+        "mjFONTSCALE_100": 100,
+        "mjFONTSCALE_150": 150,
+        "mjFONTSCALE_200": 200,
+        "mjFONTSCALE_250": 250,
+        "mjFONTSCALE_300": 300
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjrender.h",
+      "line": 52
+    },
+    "mjtFont": {
+      "name": "mjtFont",
+      "members": {
+        "mjFONT_NORMAL": 0,
+        "mjFONT_SHADOW": 1,
+        "mjFONT_BIG": 2
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjrender.h",
+      "line": 62
+    },
+    "mjtButton": {
+      "name": "mjtButton",
+      "members": {
+        "mjBUTTON_NONE": 0,
+        "mjBUTTON_LEFT": 1,
+        "mjBUTTON_RIGHT": 2,
+        "mjBUTTON_MIDDLE": 3
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 65
+    },
+    "mjtEvent": {
+      "name": "mjtEvent",
+      "members": {
+        "mjEVENT_NONE": 0,
+        "mjEVENT_MOVE": 1,
+        "mjEVENT_PRESS": 2,
+        "mjEVENT_RELEASE": 3,
+        "mjEVENT_SCROLL": 4,
+        "mjEVENT_KEY": 5,
+        "mjEVENT_RESIZE": 6,
+        "mjEVENT_REDRAW": 7,
+        "mjEVENT_FILESDROP": 8
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 73
+    },
+    "mjtItem": {
+      "name": "mjtItem",
+      "members": {
+        "mjITEM_END": -2,
+        "mjITEM_SECTION": -1,
+        "mjITEM_SEPARATOR": 0,
+        "mjITEM_STATIC": 1,
+        "mjITEM_BUTTON": 2,
+        "mjITEM_CHECKINT": 3,
+        "mjITEM_CHECKBYTE": 4,
+        "mjITEM_RADIO": 5,
+        "mjITEM_RADIOLINE": 6,
+        "mjITEM_SELECT": 7,
+        "mjITEM_SLIDERINT": 8,
+        "mjITEM_SLIDERNUM": 9,
+        "mjITEM_EDITINT": 10,
+        "mjITEM_EDITNUM": 11,
+        "mjITEM_EDITFLOAT": 12,
+        "mjITEM_EDITTXT": 13,
+        "mjNITEM": 14
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 86
+    },
+    "mjtSection": {
+      "name": "mjtSection",
+      "members": {
+        "mjSECT_CLOSED": 0,
+        "mjSECT_OPEN": 1,
+        "mjSECT_FIXED": 2
+      },
+      "member_docs": {},
+      "underlying_type": "int",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 110
+    }
+  },
+  "structs": {
+    "mjLROpt": {
+      "name": "mjLROpt",
+      "fields": [
+        {
+          "name": "mode",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "useexisting",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "uselimit",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "accel",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "maxforce",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "timeconst",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "timestep",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "inttotal",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "interval",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "tolrange",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 482
+    },
+    "mjCache": {
+      "name": "mjCache",
+      "fields": [
+        {
+          "name": "impl_",
+          "c_type": "void *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 501
+    },
+    "mjVFS": {
+      "name": "mjVFS",
+      "fields": [
+        {
+          "name": "impl_",
+          "c_type": "void *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 508
+    },
+    "mjOption": {
+      "name": "mjOption",
+      "fields": [
+        {
+          "name": "timestep",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "impratio",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "tolerance",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "ls_tolerance",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "noslip_tolerance",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "ccd_tolerance",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "sleep_tolerance",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "gravity",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "wind",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "magnetic",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "density",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "viscosity",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "o_margin",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "o_solref",
+          "c_type": "mjtNum [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "o_solimp",
+          "c_type": "mjtNum [5]",
+          "array_dim": 5,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "o_friction",
+          "c_type": "mjtNum [5]",
+          "array_dim": 5,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "integrator",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "cone",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "jacobian",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "solver",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "iterations",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "ls_iterations",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "noslip_iterations",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "ccd_iterations",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "disableflags",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "enableflags",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "disableactuator",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "sdf_initpoints",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "sdf_iterations",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 515
+    },
+    "mjVisual": {
+      "name": "mjVisual",
+      "fields": [
+        {
+          "name": "global",
+          "c_type": "struct (anonymous struct at C:\\Users\\jonat\\Documents\\Unreal Projects\\url_proj\\Plugins\\UnrealRoboticsLab\\third_party\\install\\MuJoCo\\include\\mujoco/mjmodel.h:565:3)",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "quality",
+          "c_type": "struct (anonymous struct at C:\\Users\\jonat\\Documents\\Unreal Projects\\url_proj\\Plugins\\UnrealRoboticsLab\\third_party\\install\\MuJoCo\\include\\mujoco/mjmodel.h:581:3)",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "headlight",
+          "c_type": "struct (anonymous struct at C:\\Users\\jonat\\Documents\\Unreal Projects\\url_proj\\Plugins\\UnrealRoboticsLab\\third_party\\install\\MuJoCo\\include\\mujoco/mjmodel.h:589:3)",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "map",
+          "c_type": "struct (anonymous struct at C:\\Users\\jonat\\Documents\\Unreal Projects\\url_proj\\Plugins\\UnrealRoboticsLab\\third_party\\install\\MuJoCo\\include\\mujoco/mjmodel.h:596:3)",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "scale",
+          "c_type": "struct (anonymous struct at C:\\Users\\jonat\\Documents\\Unreal Projects\\url_proj\\Plugins\\UnrealRoboticsLab\\third_party\\install\\MuJoCo\\include\\mujoco/mjmodel.h:612:3)",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "rgba",
+          "c_type": "struct (anonymous struct at C:\\Users\\jonat\\Documents\\Unreal Projects\\url_proj\\Plugins\\UnrealRoboticsLab\\third_party\\install\\MuJoCo\\include\\mujoco/mjmodel.h:632:3)",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 564
+    },
+    "mjStatistic": {
+      "name": "mjStatistic",
+      "fields": [
+        {
+          "name": "meaninertia",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "meanmass",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "meansize",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "extent",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "center",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 665
+    },
+    "mjModel": {
+      "name": "mjModel",
+      "fields": [
+        {
+          "name": "nq",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nv",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nu",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "na",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nbody",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nbvh",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nbvhstatic",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nbvhdynamic",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "noct",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "njnt",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "ntree",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nM",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nB",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nC",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nD",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "ngeom",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nsite",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "ncam",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nlight",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nflex",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nflexnode",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nflexvert",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nflexedge",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nflexelem",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nflexelemdata",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nflexstiffness",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nflexbending",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nflexelemedge",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nflexshelldata",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nflexevpair",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nflextexcoord",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nJfe",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nJfv",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nmesh",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nmeshvert",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nmeshnormal",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nmeshtexcoord",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nmeshface",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nmeshgraph",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nmeshpoly",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nmeshpolyvert",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nmeshpolymap",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nskin",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nskinvert",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nskintexvert",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nskinface",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nskinbone",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nskinbonevert",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nhfield",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nhfielddata",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "ntex",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "ntexdata",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nmat",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "npair",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nexclude",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "neq",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "ntendon",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nJten",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nwrap",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nsensor",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nnumeric",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nnumericdata",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "ntext",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "ntextdata",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "ntuple",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "ntupledata",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nkey",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nmocap",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nplugin",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "npluginattr",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nuser_body",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nuser_jnt",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nuser_geom",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nuser_site",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nuser_cam",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nuser_tendon",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nuser_actuator",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nuser_sensor",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nnames",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "npaths",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nnames_map",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nJmom",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "ngravcomp",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nemax",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "njmax",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nconmax",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nuserdata",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nsensordata",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "npluginstate",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nhistory",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "narena",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nbuffer",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "opt",
+          "c_type": "mjOption",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "vis",
+          "c_type": "mjVisual",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "stat",
+          "c_type": "mjStatistic",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "buffer",
+          "c_type": "void *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "qpos0",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "qpos_spring",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "body_parentid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "body_rootid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "body_weldid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "body_mocapid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "body_jntnum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "body_jntadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "body_dofnum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "body_dofadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "body_treeid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "body_geomnum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "body_geomadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "body_simple",
+          "c_type": "mjtByte *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "body_sameframe",
+          "c_type": "mjtByte *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "body_pos",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "body_quat",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "body_ipos",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "body_iquat",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "body_mass",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "body_subtreemass",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "body_inertia",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "body_invweight0",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "body_gravcomp",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "body_margin",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "body_user",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "body_plugin",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "body_contype",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "body_conaffinity",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "body_bvhadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "body_bvhnum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "bvh_depth",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "bvh_child",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "bvh_nodeid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "bvh_aabb",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "oct_depth",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "oct_child",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "oct_aabb",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "oct_coeff",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "jnt_type",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "jnt_qposadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "jnt_dofadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "jnt_bodyid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "jnt_actuatorid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "jnt_group",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "jnt_limited",
+          "c_type": "mjtByte *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "jnt_actfrclimited",
+          "c_type": "mjtByte *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "jnt_actgravcomp",
+          "c_type": "mjtByte *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "jnt_solref",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "jnt_solimp",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "jnt_pos",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "jnt_axis",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "jnt_stiffness",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "jnt_stiffnesspoly",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "jnt_range",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "jnt_actfrcrange",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "jnt_margin",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "jnt_user",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "dof_bodyid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "dof_jntid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "dof_parentid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "dof_treeid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "dof_Madr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "dof_simplenum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "dof_solref",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "dof_solimp",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "dof_frictionloss",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "dof_armature",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "dof_damping",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "dof_dampingpoly",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "dof_invweight0",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "dof_M0",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "dof_length",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tree_bodyadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tree_bodynum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tree_dofadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tree_dofnum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tree_sleep_policy",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "geom_type",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "geom_contype",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "geom_conaffinity",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "geom_condim",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "geom_bodyid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "geom_dataid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "geom_matid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "geom_group",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "geom_priority",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "geom_plugin",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "geom_sameframe",
+          "c_type": "mjtByte *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "geom_solmix",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "geom_solref",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "geom_solimp",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "geom_size",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "geom_aabb",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "geom_rbound",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "geom_pos",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "geom_quat",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "geom_friction",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "geom_margin",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "geom_gap",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "geom_fluid",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "geom_user",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "geom_rgba",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "site_type",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "site_bodyid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "site_matid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "site_group",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "site_sameframe",
+          "c_type": "mjtByte *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "site_size",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "site_pos",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "site_quat",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "site_user",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "site_rgba",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "cam_mode",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "cam_bodyid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "cam_targetbodyid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "cam_pos",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "cam_quat",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "cam_poscom0",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "cam_pos0",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "cam_mat0",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "cam_projection",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "cam_fovy",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "cam_ipd",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "cam_resolution",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "cam_output",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "cam_sensorsize",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "cam_intrinsic",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "cam_user",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "light_mode",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "light_bodyid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "light_targetbodyid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "light_type",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "light_texid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "light_castshadow",
+          "c_type": "mjtByte *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "light_bulbradius",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "light_intensity",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "light_range",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "light_active",
+          "c_type": "mjtByte *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "light_pos",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "light_dir",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "light_poscom0",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "light_pos0",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "light_dir0",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "light_attenuation",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "light_cutoff",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "light_exponent",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "light_ambient",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "light_diffuse",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "light_specular",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_contype",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_conaffinity",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_condim",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_priority",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_solmix",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_solref",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_solimp",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_friction",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_margin",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_gap",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_internal",
+          "c_type": "mjtByte *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_selfcollide",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_activelayers",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_passive",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_dim",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_matid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_group",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_interp",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_bandwidth",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_cellnum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_nodeadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_nodenum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_vertadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_vertnum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_edgeadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_edgenum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_elemadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_elemnum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_elemdataadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_stiffnessadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_elemedgeadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_bendingadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_shellnum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_shelldataadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_evpairadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_evpairnum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_texcoordadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_nodebodyid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_vertbodyid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_vertedgeadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_vertedgenum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_vertedge",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_edge",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_edgeflap",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_elem",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_elemtexcoord",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_elemedge",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_elemlayer",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_shell",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_evpair",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_vert",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_vert0",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_vertmetric",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_node",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_node0",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flexedge_length0",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flexedge_invweight0",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_radius",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_size",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_stiffness",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_bending",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_damping",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_edgestiffness",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_edgedamping",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_edgeequality",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_rigid",
+          "c_type": "mjtByte *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flexedge_rigid",
+          "c_type": "mjtByte *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_centered",
+          "c_type": "mjtByte *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_flatskin",
+          "c_type": "mjtByte *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_bvhadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_bvhnum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flexedge_J_rownnz",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flexedge_J_rowadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flexedge_J_colind",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flexvert_J_rownnz",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flexvert_J_rowadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flexvert_J_colind",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_rgba",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex_texcoord",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mesh_vertadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mesh_vertnum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mesh_faceadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mesh_facenum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mesh_bvhadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mesh_bvhnum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mesh_octadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mesh_octnum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mesh_normaladr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mesh_normalnum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mesh_texcoordadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mesh_texcoordnum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mesh_graphadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mesh_vert",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mesh_normal",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mesh_texcoord",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mesh_face",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mesh_facenormal",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mesh_facetexcoord",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mesh_graph",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mesh_scale",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mesh_pos",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mesh_quat",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mesh_pathadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mesh_polynum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mesh_polyadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mesh_polynormal",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mesh_polyvertadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mesh_polyvertnum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mesh_polyvert",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mesh_polymapadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mesh_polymapnum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mesh_polymap",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "skin_matid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "skin_group",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "skin_rgba",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "skin_inflate",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "skin_vertadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "skin_vertnum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "skin_texcoordadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "skin_faceadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "skin_facenum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "skin_boneadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "skin_bonenum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "skin_vert",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "skin_texcoord",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "skin_face",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "skin_bonevertadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "skin_bonevertnum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "skin_bonebindpos",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "skin_bonebindquat",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "skin_bonebodyid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "skin_bonevertid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "skin_bonevertweight",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "skin_pathadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "hfield_size",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "hfield_nrow",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "hfield_ncol",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "hfield_adr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "hfield_data",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "hfield_pathadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tex_type",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tex_colorspace",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tex_height",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tex_width",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tex_nchannel",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tex_adr",
+          "c_type": "mjtSize *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tex_data",
+          "c_type": "mjtByte *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tex_pathadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mat_texid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mat_texuniform",
+          "c_type": "mjtByte *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mat_texrepeat",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mat_emission",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mat_specular",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mat_shininess",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mat_reflectance",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mat_metallic",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mat_roughness",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mat_rgba",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "pair_dim",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "pair_geom1",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "pair_geom2",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "pair_signature",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "pair_solref",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "pair_solreffriction",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "pair_solimp",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "pair_margin",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "pair_gap",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "pair_friction",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "exclude_signature",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "eq_type",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "eq_obj1id",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "eq_obj2id",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "eq_objtype",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "eq_active0",
+          "c_type": "mjtByte *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "eq_solref",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "eq_solimp",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "eq_data",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tendon_adr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tendon_num",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tendon_matid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tendon_actuatorid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tendon_group",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tendon_treenum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tendon_treeid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "ten_J_rownnz",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "ten_J_rowadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "ten_J_colind",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tendon_limited",
+          "c_type": "mjtByte *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tendon_actfrclimited",
+          "c_type": "mjtByte *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tendon_width",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tendon_solref_lim",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tendon_solimp_lim",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tendon_solref_fri",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tendon_solimp_fri",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tendon_range",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tendon_actfrcrange",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tendon_margin",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tendon_stiffness",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tendon_stiffnesspoly",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tendon_damping",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tendon_dampingpoly",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tendon_armature",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tendon_frictionloss",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tendon_lengthspring",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tendon_length0",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tendon_invweight0",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tendon_user",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tendon_rgba",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "wrap_type",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "wrap_objid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "wrap_prm",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_trntype",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_dyntype",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_gaintype",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_biastype",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_trnid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_damping",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_dampingpoly",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_armature",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_actadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_actnum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_group",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_history",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_historyadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_delay",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_ctrllimited",
+          "c_type": "mjtByte *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_forcelimited",
+          "c_type": "mjtByte *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_actlimited",
+          "c_type": "mjtByte *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_dynprm",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_gainprm",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_biasprm",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_actearly",
+          "c_type": "mjtByte *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_ctrlrange",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_forcerange",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_actrange",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_gear",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_cranklength",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_acc0",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_length0",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_lengthrange",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_user",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_plugin",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "sensor_type",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "sensor_datatype",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "sensor_needstage",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "sensor_objtype",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "sensor_objid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "sensor_reftype",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "sensor_refid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "sensor_intprm",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "sensor_dim",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "sensor_adr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "sensor_cutoff",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "sensor_noise",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "sensor_history",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "sensor_historyadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "sensor_delay",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "sensor_interval",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "sensor_user",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "sensor_plugin",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "plugin",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "plugin_stateadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "plugin_statenum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "plugin_attr",
+          "c_type": "char *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "plugin_attradr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "numeric_adr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "numeric_size",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "numeric_data",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "text_adr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "text_size",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "text_data",
+          "c_type": "char *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tuple_adr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tuple_size",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tuple_objtype",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tuple_objid",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tuple_objprm",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "key_time",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "key_qpos",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "key_qvel",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "key_act",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "key_mpos",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "key_mquat",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "key_ctrl",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "name_bodyadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "name_jntadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "name_geomadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "name_siteadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "name_camadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "name_lightadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "name_flexadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "name_meshadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "name_skinadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "name_hfieldadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "name_texadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "name_matadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "name_pairadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "name_excludeadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "name_eqadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "name_tendonadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "name_actuatoradr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "name_sensoradr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "name_numericadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "name_textadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "name_tupleadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "name_keyadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "name_pluginadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "names",
+          "c_type": "char *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "names_map",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "paths",
+          "c_type": "char *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "B_rownnz",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "B_rowadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "B_colind",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "M_rownnz",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "M_rowadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "M_colind",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mapM2M",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "D_rownnz",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "D_rowadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "D_diag",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "D_colind",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mapM2D",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mapD2M",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "signature",
+          "c_type": "uint64_t",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 677
+    },
+    "mjsElement": {
+      "name": "mjsElement",
+      "fields": [
+        {
+          "name": "elemtype",
+          "c_type": "mjtObj",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "signature",
+          "c_type": "uint64_t",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 134
+    },
+    "mjsCompiler": {
+      "name": "mjsCompiler",
+      "fields": [
+        {
+          "name": "autolimits",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "boundmass",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "boundinertia",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "settotalmass",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "balanceinertia",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "fitaabb",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "degree",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "eulerseq",
+          "c_type": "char [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "discardvisual",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "usethread",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "fusestatic",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "inertiafromgeom",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "inertiagrouprange",
+          "c_type": "int [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "saveinertial",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "alignfree",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "LRopt",
+          "c_type": "mjLROpt",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "meshdir",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "texturedir",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 140
+    },
+    "mjSpec": {
+      "name": "mjSpec",
+      "fields": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "modelname",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "compiler",
+          "c_type": "mjsCompiler",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "strippath",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "option",
+          "c_type": "mjOption",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "visual",
+          "c_type": "mjVisual",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "stat",
+          "c_type": "mjStatistic",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "memory",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nemax",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nuserdata",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nuser_body",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nuser_jnt",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nuser_geom",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nuser_site",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nuser_cam",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nuser_tendon",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nuser_actuator",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nuser_sensor",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nkey",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "njmax",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nconmax",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nstack",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "comment",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "modelfiledir",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "hasImplicitPluginElem",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 162
+    },
+    "mjsOrientation": {
+      "name": "mjsOrientation",
+      "fields": [
+        {
+          "name": "type",
+          "c_type": "mjtOrientation",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "axisangle",
+          "c_type": "double [4]",
+          "array_dim": 4,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "xyaxes",
+          "c_type": "double [6]",
+          "array_dim": 6,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "zaxis",
+          "c_type": "double [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "euler",
+          "c_type": "double [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 201
+    },
+    "mjsPlugin": {
+      "name": "mjsPlugin",
+      "fields": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "name",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "plugin_name",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "active",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "info",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 210
+    },
+    "mjsBody": {
+      "name": "mjsBody",
+      "fields": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "childclass",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "pos",
+          "c_type": "double [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "quat",
+          "c_type": "double [4]",
+          "array_dim": 4,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "alt",
+          "c_type": "mjsOrientation",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "mass",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "ipos",
+          "c_type": "double [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "iquat",
+          "c_type": "double [4]",
+          "array_dim": 4,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "inertia",
+          "c_type": "double [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "ialt",
+          "c_type": "mjsOrientation",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "fullinertia",
+          "c_type": "double [6]",
+          "array_dim": 6,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "mocap",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "gravcomp",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "sleep",
+          "c_type": "mjtSleepPolicy",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "userdata",
+          "c_type": "mjDoubleVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "explicitinertial",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "plugin",
+          "c_type": "mjsPlugin",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "info",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 219
+    },
+    "mjsFrame": {
+      "name": "mjsFrame",
+      "fields": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "childclass",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "pos",
+          "c_type": "double [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "quat",
+          "c_type": "double [4]",
+          "array_dim": 4,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "alt",
+          "c_type": "mjsOrientation",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "info",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 247
+    },
+    "mjsJoint": {
+      "name": "mjsJoint",
+      "fields": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "type",
+          "c_type": "mjtJoint",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "pos",
+          "c_type": "double [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "axis",
+          "c_type": "double [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "ref",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "align",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "stiffness",
+          "c_type": "double [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "springref",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "springdamper",
+          "c_type": "double [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "limited",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "range",
+          "c_type": "double [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "margin",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "solref_limit",
+          "c_type": "mjtNum [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "solimp_limit",
+          "c_type": "mjtNum [5]",
+          "array_dim": 5,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "actfrclimited",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "actfrcrange",
+          "c_type": "double [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "armature",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "damping",
+          "c_type": "double [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "frictionloss",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "solref_friction",
+          "c_type": "mjtNum [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "solimp_friction",
+          "c_type": "mjtNum [5]",
+          "array_dim": 5,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "group",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "actgravcomp",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "userdata",
+          "c_type": "mjDoubleVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "info",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 257
+    },
+    "mjsGeom": {
+      "name": "mjsGeom",
+      "fields": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "type",
+          "c_type": "mjtGeom",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "pos",
+          "c_type": "double [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "quat",
+          "c_type": "double [4]",
+          "array_dim": 4,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "alt",
+          "c_type": "mjsOrientation",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "fromto",
+          "c_type": "double [6]",
+          "array_dim": 6,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "size",
+          "c_type": "double [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "contype",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "conaffinity",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "condim",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "priority",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "friction",
+          "c_type": "double [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "solmix",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "solref",
+          "c_type": "mjtNum [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "solimp",
+          "c_type": "mjtNum [5]",
+          "array_dim": 5,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "margin",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "gap",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "mass",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "density",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "typeinertia",
+          "c_type": "mjtGeomInertia",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "fluid_ellipsoid",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "fluid_coefs",
+          "c_type": "mjtNum [5]",
+          "array_dim": 5,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "material",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "rgba",
+          "c_type": "float [4]",
+          "array_dim": 4,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "group",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "hfieldname",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "meshname",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "fitscale",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "userdata",
+          "c_type": "mjDoubleVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "plugin",
+          "c_type": "mjsPlugin",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "info",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 296
+    },
+    "mjsSite": {
+      "name": "mjsSite",
+      "fields": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "pos",
+          "c_type": "double [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "quat",
+          "c_type": "double [4]",
+          "array_dim": 4,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "alt",
+          "c_type": "mjsOrientation",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "fromto",
+          "c_type": "double [6]",
+          "array_dim": 6,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "size",
+          "c_type": "double [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "type",
+          "c_type": "mjtGeom",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "material",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "group",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "rgba",
+          "c_type": "float [4]",
+          "array_dim": 4,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "userdata",
+          "c_type": "mjDoubleVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "info",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 343
+    },
+    "mjsCamera": {
+      "name": "mjsCamera",
+      "fields": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "pos",
+          "c_type": "double [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "quat",
+          "c_type": "double [4]",
+          "array_dim": 4,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "alt",
+          "c_type": "mjsOrientation",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "mode",
+          "c_type": "mjtCamLight",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "targetbody",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "proj",
+          "c_type": "mjtProjection",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "resolution",
+          "c_type": "int [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "output",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "fovy",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "ipd",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "intrinsic",
+          "c_type": "float [4]",
+          "array_dim": 4,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "sensor_size",
+          "c_type": "float [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "focal_length",
+          "c_type": "float [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "focal_pixel",
+          "c_type": "float [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "principal_length",
+          "c_type": "float [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "principal_pixel",
+          "c_type": "float [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "userdata",
+          "c_type": "mjDoubleVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "info",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 365
+    },
+    "mjsLight": {
+      "name": "mjsLight",
+      "fields": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "pos",
+          "c_type": "double [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "dir",
+          "c_type": "double [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "mode",
+          "c_type": "mjtCamLight",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "targetbody",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "active",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "type",
+          "c_type": "mjtLightType",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "texture",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "castshadow",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "bulbradius",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "intensity",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "range",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "attenuation",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "cutoff",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "exponent",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "ambient",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "diffuse",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "specular",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "info",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 394
+    },
+    "mjsFlex": {
+      "name": "mjsFlex",
+      "fields": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "contype",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "conaffinity",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "condim",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "priority",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "friction",
+          "c_type": "double [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "solmix",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "solref",
+          "c_type": "mjtNum [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "solimp",
+          "c_type": "mjtNum [5]",
+          "array_dim": 5,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "margin",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "gap",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "dim",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "radius",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "size",
+          "c_type": "double [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "internal",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "flatskin",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "selfcollide",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "passive",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "activelayers",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "group",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "edgestiffness",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "edgedamping",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "rgba",
+          "c_type": "float [4]",
+          "array_dim": 4,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "material",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "young",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "poisson",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "damping",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "thickness",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "elastic2d",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "cellcount",
+          "c_type": "int [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "order",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nodebody",
+          "c_type": "mjStringVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "vertbody",
+          "c_type": "mjStringVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "node",
+          "c_type": "mjDoubleVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "vert",
+          "c_type": "mjDoubleVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "elem",
+          "c_type": "mjIntVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "texcoord",
+          "c_type": "mjFloatVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "elemtexcoord",
+          "c_type": "mjIntVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "info",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 423
+    },
+    "mjsMesh": {
+      "name": "mjsMesh",
+      "fields": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "content_type",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "file",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "refpos",
+          "c_type": "double [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "refquat",
+          "c_type": "double [4]",
+          "array_dim": 4,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "scale",
+          "c_type": "double [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "inertia",
+          "c_type": "mjtMeshInertia",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "smoothnormal",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "needsdf",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "maxhullvert",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "uservert",
+          "c_type": "mjFloatVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "usernormal",
+          "c_type": "mjFloatVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "usertexcoord",
+          "c_type": "mjFloatVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "userface",
+          "c_type": "mjIntVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "userfacenormal",
+          "c_type": "mjIntVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "userfacetexcoord",
+          "c_type": "mjIntVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "plugin",
+          "c_type": "mjsPlugin",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "material",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "octree_maxdepth",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "info",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 474
+    },
+    "mjsHField": {
+      "name": "mjsHField",
+      "fields": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "content_type",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "file",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "size",
+          "c_type": "double [4]",
+          "array_dim": 4,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nrow",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "ncol",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "userdata",
+          "c_type": "mjFloatVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "info",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 498
+    },
+    "mjsSkin": {
+      "name": "mjsSkin",
+      "fields": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "file",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "material",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "rgba",
+          "c_type": "float [4]",
+          "array_dim": 4,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "inflate",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "group",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "vert",
+          "c_type": "mjFloatVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "texcoord",
+          "c_type": "mjFloatVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "face",
+          "c_type": "mjIntVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "bodyname",
+          "c_type": "mjStringVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "bindpos",
+          "c_type": "mjFloatVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "bindquat",
+          "c_type": "mjFloatVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "vertid",
+          "c_type": "mjIntVecVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "vertweight",
+          "c_type": "mjFloatVecVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "info",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 511
+    },
+    "mjsTexture": {
+      "name": "mjsTexture",
+      "fields": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "type",
+          "c_type": "mjtTexture",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "colorspace",
+          "c_type": "mjtColorSpace",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "builtin",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "mark",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "rgb1",
+          "c_type": "double [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "rgb2",
+          "c_type": "double [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "markrgb",
+          "c_type": "double [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "random",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "height",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "width",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nchannel",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "content_type",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "file",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "gridsize",
+          "c_type": "int [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "gridlayout",
+          "c_type": "char [12]",
+          "array_dim": 12,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "cubefiles",
+          "c_type": "mjStringVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "data",
+          "c_type": "mjByteVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "hflip",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "vflip",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "info",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 536
+    },
+    "mjsMaterial": {
+      "name": "mjsMaterial",
+      "fields": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "textures",
+          "c_type": "mjStringVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "texuniform",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "texrepeat",
+          "c_type": "float [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "emission",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "specular",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "shininess",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "reflectance",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "metallic",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "roughness",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "rgba",
+          "c_type": "float [4]",
+          "array_dim": 4,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "info",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 573
+    },
+    "mjsPair": {
+      "name": "mjsPair",
+      "fields": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "geomname1",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "geomname2",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "condim",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "solref",
+          "c_type": "mjtNum [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "solreffriction",
+          "c_type": "mjtNum [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "solimp",
+          "c_type": "mjtNum [5]",
+          "array_dim": 5,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "margin",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "gap",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "friction",
+          "c_type": "double [5]",
+          "array_dim": 5,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "info",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 589
+    },
+    "mjsExclude": {
+      "name": "mjsExclude",
+      "fields": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "bodyname1",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "bodyname2",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "info",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 606
+    },
+    "mjsEquality": {
+      "name": "mjsEquality",
+      "fields": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "type",
+          "c_type": "mjtEq",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "data",
+          "c_type": "double [11]",
+          "array_dim": 11,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "active",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "name1",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "name2",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "objtype",
+          "c_type": "mjtObj",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "solref",
+          "c_type": "mjtNum [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "solimp",
+          "c_type": "mjtNum [5]",
+          "array_dim": 5,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "info",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 614
+    },
+    "mjsTendon": {
+      "name": "mjsTendon",
+      "fields": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "stiffness",
+          "c_type": "double [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "springlength",
+          "c_type": "double [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "damping",
+          "c_type": "double [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "frictionloss",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "solref_friction",
+          "c_type": "mjtNum [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "solimp_friction",
+          "c_type": "mjtNum [5]",
+          "array_dim": 5,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "armature",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "limited",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "actfrclimited",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "range",
+          "c_type": "double [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "actfrcrange",
+          "c_type": "double [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "margin",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "solref_limit",
+          "c_type": "mjtNum [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "solimp_limit",
+          "c_type": "mjtNum [5]",
+          "array_dim": 5,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "material",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "width",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "rgba",
+          "c_type": "float [4]",
+          "array_dim": 4,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "group",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "userdata",
+          "c_type": "mjDoubleVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "info",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 628
+    },
+    "mjsWrap": {
+      "name": "mjsWrap",
+      "fields": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "type",
+          "c_type": "mjtWrap",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "info",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 661
+    },
+    "mjsActuator": {
+      "name": "mjsActuator",
+      "fields": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "gaintype",
+          "c_type": "mjtGain",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "gainprm",
+          "c_type": "double [10]",
+          "array_dim": 10,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "biastype",
+          "c_type": "mjtBias",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "biasprm",
+          "c_type": "double [10]",
+          "array_dim": 10,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "dyntype",
+          "c_type": "mjtDyn",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "dynprm",
+          "c_type": "double [10]",
+          "array_dim": 10,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "actdim",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "actearly",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "trntype",
+          "c_type": "mjtTrn",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "gear",
+          "c_type": "double [6]",
+          "array_dim": 6,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "target",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "refsite",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "slidersite",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "cranklength",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "lengthrange",
+          "c_type": "double [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "inheritrange",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "damping",
+          "c_type": "double [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "armature",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "ctrllimited",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "ctrlrange",
+          "c_type": "double [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "forcelimited",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "forcerange",
+          "c_type": "double [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "actlimited",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "actrange",
+          "c_type": "double [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "group",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nsample",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "interp",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "delay",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "userdata",
+          "c_type": "mjDoubleVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "plugin",
+          "c_type": "mjsPlugin",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "info",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 668
+    },
+    "mjsSensor": {
+      "name": "mjsSensor",
+      "fields": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "type",
+          "c_type": "mjtSensor",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "objtype",
+          "c_type": "mjtObj",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "objname",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "reftype",
+          "c_type": "mjtObj",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "refname",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "intprm",
+          "c_type": "int [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "datatype",
+          "c_type": "mjtDataType",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "needstage",
+          "c_type": "mjtStage",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "dim",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "cutoff",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "noise",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nsample",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "interp",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "delay",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "interval",
+          "c_type": "double [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "userdata",
+          "c_type": "mjDoubleVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "plugin",
+          "c_type": "mjsPlugin",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "info",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 714
+    },
+    "mjsNumeric": {
+      "name": "mjsNumeric",
+      "fields": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "data",
+          "c_type": "mjDoubleVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "size",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "info",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 747
+    },
+    "mjsText": {
+      "name": "mjsText",
+      "fields": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "data",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "info",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 755
+    },
+    "mjsTuple": {
+      "name": "mjsTuple",
+      "fields": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "objtype",
+          "c_type": "mjIntVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "objname",
+          "c_type": "mjStringVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "objprm",
+          "c_type": "mjDoubleVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "info",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 762
+    },
+    "mjsKey": {
+      "name": "mjsKey",
+      "fields": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "time",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "qpos",
+          "c_type": "mjDoubleVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "qvel",
+          "c_type": "mjDoubleVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "act",
+          "c_type": "mjDoubleVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mpos",
+          "c_type": "mjDoubleVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mquat",
+          "c_type": "mjDoubleVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "ctrl",
+          "c_type": "mjDoubleVec *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "info",
+          "c_type": "mjString *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 771
+    },
+    "mjsDefault": {
+      "name": "mjsDefault",
+      "fields": [
+        {
+          "name": "element",
+          "c_type": "mjsElement *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "joint",
+          "c_type": "mjsJoint *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "geom",
+          "c_type": "mjsGeom *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "site",
+          "c_type": "mjsSite *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "camera",
+          "c_type": "mjsCamera *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "light",
+          "c_type": "mjsLight *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flex",
+          "c_type": "mjsFlex *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mesh",
+          "c_type": "mjsMesh *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "material",
+          "c_type": "mjsMaterial *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "pair",
+          "c_type": "mjsPair *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "equality",
+          "c_type": "mjsEquality *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tendon",
+          "c_type": "mjsTendon *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator",
+          "c_type": "mjsActuator *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjspec.h",
+      "line": 784
+    },
+    "mjThreadPool": {
+      "name": "mjThreadPool",
+      "fields": [
+        {
+          "name": "nworker",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjthread.h",
+      "line": 30
+    },
+    "mjTask": {
+      "name": "mjTask",
+      "fields": [
+        {
+          "name": "func",
+          "c_type": "mjfTask",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "args",
+          "c_type": "void *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "status",
+          "c_type": "volatile int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjthread.h",
+      "line": 35
+    },
+    "mjContact": {
+      "name": "mjContact",
+      "fields": [
+        {
+          "name": "dist",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "pos",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "frame",
+          "c_type": "mjtNum [9]",
+          "array_dim": 9,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "includemargin",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "friction",
+          "c_type": "mjtNum [5]",
+          "array_dim": 5,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "solref",
+          "c_type": "mjtNum [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "solreffriction",
+          "c_type": "mjtNum [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "solimp",
+          "c_type": "mjtNum [5]",
+          "array_dim": 5,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "mu",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "H",
+          "c_type": "mjtNum [36]",
+          "array_dim": 36,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "dim",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "geom1",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "geom2",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "geom",
+          "c_type": "int [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "flex",
+          "c_type": "int [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "elem",
+          "c_type": "int [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "vert",
+          "c_type": "int [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "exclude",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "efc_address",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjdata.h",
+      "line": 126
+    },
+    "mjWarningStat": {
+      "name": "mjWarningStat",
+      "fields": [
+        {
+          "name": "lastinfo",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "number",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjdata.h",
+      "line": 163
+    },
+    "mjTimerStat": {
+      "name": "mjTimerStat",
+      "fields": [
+        {
+          "name": "duration",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "number",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjdata.h",
+      "line": 170
+    },
+    "mjSolverStat": {
+      "name": "mjSolverStat",
+      "fields": [
+        {
+          "name": "improvement",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "gradient",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "lineslope",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nactive",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nchange",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "neval",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nupdate",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjdata.h",
+      "line": 177
+    },
+    "mjData": {
+      "name": "mjData",
+      "fields": [
+        {
+          "name": "narena",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nbuffer",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nplugin",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "pstack",
+          "c_type": "size_t",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "pbase",
+          "c_type": "size_t",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "parena",
+          "c_type": "size_t",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "maxuse_stack",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "maxuse_threadstack",
+          "c_type": "mjtSize [128]",
+          "array_dim": 128,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "maxuse_arena",
+          "c_type": "mjtSize",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "maxuse_con",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "maxuse_efc",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "solver",
+          "c_type": "mjSolverStat [4000]",
+          "array_dim": 4000,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "solver_niter",
+          "c_type": "int [20]",
+          "array_dim": 20,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "solver_nnz",
+          "c_type": "int [20]",
+          "array_dim": 20,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "solver_fwdinv",
+          "c_type": "mjtNum [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "warning",
+          "c_type": "mjWarningStat [7]",
+          "array_dim": 7,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "timer",
+          "c_type": "mjTimerStat [15]",
+          "array_dim": 15,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "ncon",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "ne",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nf",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nl",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nefc",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nJ",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nA",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nisland",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nidof",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "ntree_awake",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nbody_awake",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nparent_awake",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nv_awake",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "flg_energypos",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "flg_energyvel",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "flg_subtreevel",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "flg_rnepost",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "time",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "energy",
+          "c_type": "mjtNum [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "buffer",
+          "c_type": "void *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "arena",
+          "c_type": "void *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "qpos",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "qvel",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "act",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "history",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "qacc_warmstart",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "plugin_state",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "ctrl",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "qfrc_applied",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "xfrc_applied",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "eq_active",
+          "c_type": "mjtByte *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mocap_pos",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "mocap_quat",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "qacc",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "act_dot",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "userdata",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "sensordata",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tree_asleep",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "plugin",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "plugin_data",
+          "c_type": "uintptr_t *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "xpos",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "xquat",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "xmat",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "xipos",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "ximat",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "xanchor",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "xaxis",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "geom_xpos",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "geom_xmat",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "site_xpos",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "site_xmat",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "cam_xpos",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "cam_xmat",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "light_xpos",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "light_xdir",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "subtree_com",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "cdof",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "cinert",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flexvert_xpos",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flexelem_aabb",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flexedge_J",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flexedge_length",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flexvert_J",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flexvert_length",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "bvh_aabb_dyn",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "ten_wrapadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "ten_wrapnum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "ten_J",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "ten_length",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "wrap_obj",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "wrap_xpos",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_length",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "moment_rownnz",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "moment_rowadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "moment_colind",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_moment",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "crb",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "qM",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "M",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "qLD",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "qLDiagInv",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "bvh_active",
+          "c_type": "mjtByte *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tree_awake",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "body_awake",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "body_awake_ind",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "parent_awake_ind",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "dof_awake_ind",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flexedge_velocity",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "ten_velocity",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_velocity",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "cvel",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "cdof_dot",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "qfrc_bias",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "qfrc_spring",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "qfrc_damper",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "qfrc_gravcomp",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "qfrc_fluid",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "qfrc_passive",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "subtree_linvel",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "subtree_angmom",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "qH",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "qHDiagInv",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "qDeriv",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "qLU",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_force",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "qfrc_actuator",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "qfrc_smooth",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "qacc_smooth",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "qfrc_constraint",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "qfrc_inverse",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "cacc",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "cfrc_int",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "cfrc_ext",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "contact",
+          "c_type": "mjContact *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "efc_type",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "efc_id",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "efc_J_rownnz",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "efc_J_rowadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "efc_J_rowsuper",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "efc_J_colind",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "efc_J",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "efc_pos",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "efc_margin",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "efc_frictionloss",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "efc_diagApprox",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "efc_KBIP",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "efc_D",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "efc_R",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tendon_efcadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "tree_island",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "island_ntree",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "island_itreeadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "map_itree2tree",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "dof_island",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "island_nv",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "island_idofadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "island_dofadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "map_dof2idof",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "map_idof2dof",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "ifrc_smooth",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "iacc_smooth",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "iM_rownnz",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "iM_rowadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "iM_colind",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "iM",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "iLD",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "iLDiagInv",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "iacc",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "efc_island",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "island_ne",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "island_nf",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "island_nefc",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "island_iefcadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "map_efc2iefc",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "map_iefc2efc",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "iefc_type",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "iefc_id",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "iefc_J_rownnz",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "iefc_J_rowadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "iefc_J_rowsuper",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "iefc_J_colind",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "iefc_J",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "iefc_frictionloss",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "iefc_D",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "iefc_R",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "efc_AR_rownnz",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "efc_AR_rowadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "efc_AR_colind",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "efc_AR",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "efc_vel",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "efc_aref",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "efc_b",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "iefc_aref",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "iefc_state",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "iefc_force",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "efc_state",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "efc_force",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "ifrc_constraint",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "threadpool",
+          "c_type": "uintptr_t",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "signature",
+          "c_type": "uint64_t",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjdata.h",
+      "line": 191
+    },
+    "mjvPerturb": {
+      "name": "mjvPerturb",
+      "fields": [
+        {
+          "name": "select",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "flexselect",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "skinselect",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "active",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "active2",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "refpos",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "refquat",
+          "c_type": "mjtNum [4]",
+          "array_dim": 4,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "refselpos",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "localpos",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "localmass",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "scale",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjvisualize.h",
+      "line": 167
+    },
+    "mjvCamera": {
+      "name": "mjvCamera",
+      "fields": [
+        {
+          "name": "type",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "fixedcamid",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "trackbodyid",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "lookat",
+          "c_type": "mjtNum [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "distance",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "azimuth",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "elevation",
+          "c_type": "mjtNum",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "orthographic",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjvisualize.h",
+      "line": 185
+    },
+    "mjvGLCamera": {
+      "name": "mjvGLCamera",
+      "fields": [
+        {
+          "name": "pos",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "forward",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "up",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "frustum_center",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "frustum_width",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "frustum_bottom",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "frustum_top",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "frustum_near",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "frustum_far",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "orthographic",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjvisualize.h",
+      "line": 205
+    },
+    "mjvGeom": {
+      "name": "mjvGeom",
+      "fields": [
+        {
+          "name": "type",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "dataid",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "objtype",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "objid",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "category",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "matid",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "texcoord",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "segid",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "size",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "pos",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "mat",
+          "c_type": "float [9]",
+          "array_dim": 9,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "rgba",
+          "c_type": "float [4]",
+          "array_dim": 4,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "emission",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "specular",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "shininess",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "reflectance",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "label",
+          "c_type": "char [100]",
+          "array_dim": 100,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "camdist",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "modelrbound",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "transparent",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjvisualize.h",
+      "line": 227
+    },
+    "mjvLight": {
+      "name": "mjvLight",
+      "fields": [
+        {
+          "name": "id",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "pos",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "dir",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "type",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "texid",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "attenuation",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "cutoff",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "exponent",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "ambient",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "diffuse",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "specular",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "headlight",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "castshadow",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "bulbradius",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "intensity",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "range",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjvisualize.h",
+      "line": 262
+    },
+    "mjvOption": {
+      "name": "mjvOption",
+      "fields": [
+        {
+          "name": "label",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "frame",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "geomgroup",
+          "c_type": "mjtByte [6]",
+          "array_dim": 6,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "sitegroup",
+          "c_type": "mjtByte [6]",
+          "array_dim": 6,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "jointgroup",
+          "c_type": "mjtByte [6]",
+          "array_dim": 6,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "tendongroup",
+          "c_type": "mjtByte [6]",
+          "array_dim": 6,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "actuatorgroup",
+          "c_type": "mjtByte [6]",
+          "array_dim": 6,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "flexgroup",
+          "c_type": "mjtByte [6]",
+          "array_dim": 6,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "skingroup",
+          "c_type": "mjtByte [6]",
+          "array_dim": 6,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "flags",
+          "c_type": "mjtByte [31]",
+          "array_dim": 31,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "bvh_depth",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "flex_layer",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjvisualize.h",
+      "line": 285
+    },
+    "mjvScene": {
+      "name": "mjvScene",
+      "fields": [
+        {
+          "name": "maxgeom",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "ngeom",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "geoms",
+          "c_type": "mjvGeom *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "geomorder",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "nflex",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "flexedgeadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flexedgenum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flexvertadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flexvertnum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flexfaceadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flexfacenum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flexfaceused",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flexedge",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flexvert",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flexface",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flexnormal",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flextexcoord",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "flexvertopt",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "flexedgeopt",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "flexfaceopt",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "flexskinopt",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nskin",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "skinfacenum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "skinvertadr",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "skinvertnum",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "skinvert",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "skinnormal",
+          "c_type": "float *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "nlight",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "lights",
+          "c_type": "mjvLight [100]",
+          "array_dim": 100,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "camera",
+          "c_type": "mjvGLCamera [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "enabletransform",
+          "c_type": "mjtByte",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "translate",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "rotate",
+          "c_type": "float [4]",
+          "array_dim": 4,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "scale",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "stereo",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "flags",
+          "c_type": "mjtByte [11]",
+          "array_dim": 11,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "framewidth",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "framergb",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "status",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjvisualize.h",
+      "line": 304
+    },
+    "mjvFigure": {
+      "name": "mjvFigure",
+      "fields": [
+        {
+          "name": "flg_legend",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "flg_ticklabel",
+          "c_type": "int [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "flg_extend",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "flg_barplot",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "flg_selection",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "flg_symmetric",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "linewidth",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "gridwidth",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "gridsize",
+          "c_type": "int [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "gridrgb",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "figurergba",
+          "c_type": "float [4]",
+          "array_dim": 4,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "panergba",
+          "c_type": "float [4]",
+          "array_dim": 4,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "legendrgba",
+          "c_type": "float [4]",
+          "array_dim": 4,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "textrgb",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "linergb",
+          "c_type": "float [100][3]",
+          "array_dim": 100,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "range",
+          "c_type": "float [2][2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "xformat",
+          "c_type": "char [20]",
+          "array_dim": 20,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "yformat",
+          "c_type": "char [20]",
+          "array_dim": 20,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "minwidth",
+          "c_type": "char [20]",
+          "array_dim": 20,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "title",
+          "c_type": "char [1000]",
+          "array_dim": 1000,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "xlabel",
+          "c_type": "char [100]",
+          "array_dim": 100,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "linename",
+          "c_type": "char [100][100]",
+          "array_dim": 100,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "legendoffset",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "subplot",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "highlight",
+          "c_type": "int [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "highlightid",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "selection",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "linepnt",
+          "c_type": "int [100]",
+          "array_dim": 100,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "linedata",
+          "c_type": "float [100][2002]",
+          "array_dim": 100,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "xaxispixel",
+          "c_type": "int [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "yaxispixel",
+          "c_type": "int [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "xaxisdata",
+          "c_type": "float [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "yaxisdata",
+          "c_type": "float [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjvisualize.h",
+      "line": 367
+    },
+    "mjResource": {
+      "name": "mjResource",
+      "fields": [
+        {
+          "name": "name",
+          "c_type": "char *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "data",
+          "c_type": "void *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "vfs",
+          "c_type": "mjVFS *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "timestamp",
+          "c_type": "char [512]",
+          "array_dim": 512,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "provider",
+          "c_type": "const struct mjpResourceProvider *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjplugin.h",
+      "line": 27
+    },
+    "mjpResourceProvider": {
+      "name": "mjpResourceProvider",
+      "fields": [
+        {
+          "name": "prefix",
+          "c_type": "const char *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "open",
+          "c_type": "mjfOpenResource",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "read",
+          "c_type": "mjfReadResource",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "close",
+          "c_type": "mjfCloseResource",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "mount",
+          "c_type": "mjfMountResource",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "unmount",
+          "c_type": "mjfUnmountResource",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "modified",
+          "c_type": "mjfResourceModified",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "data",
+          "c_type": "void *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjplugin.h",
+      "line": 60
+    },
+    "mjpDecoder": {
+      "name": "mjpDecoder",
+      "fields": [
+        {
+          "name": "content_type",
+          "c_type": "const char *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "extension",
+          "c_type": "const char *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "can_decode",
+          "c_type": "mjfCanDecode",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "decode",
+          "c_type": "mjfDecode",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjplugin.h",
+      "line": 81
+    },
+    "mjpEncoder": {
+      "name": "mjpEncoder",
+      "fields": [
+        {
+          "name": "content_type",
+          "c_type": "const char *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "extension",
+          "c_type": "const char *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "encode",
+          "c_type": "mjfEncode",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "close_resource",
+          "c_type": "mjfCloseResource",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjplugin.h",
+      "line": 97
+    },
+    "mjpPlugin": {
+      "name": "mjpPlugin",
+      "fields": [
+        {
+          "name": "name",
+          "c_type": "const char *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "nattribute",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "attributes",
+          "c_type": "const char *const *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "capabilityflags",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "needstage",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nstate",
+          "c_type": "int (*)(const mjModel *, int)",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "nsensordata",
+          "c_type": "int (*)(const mjModel *, int, int)",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "init",
+          "c_type": "int (*)(const mjModel *, mjData *, int)",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "destroy",
+          "c_type": "void (*)(mjData *, int)",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "copy",
+          "c_type": "void (*)(mjData *, const mjModel *, const mjData *, int)",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "reset",
+          "c_type": "void (*)(const mjModel *, mjtNum *, void *, int)",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "compute",
+          "c_type": "void (*)(const mjModel *, mjData *, int, int)",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "advance",
+          "c_type": "void (*)(const mjModel *, mjData *, int)",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "visualize",
+          "c_type": "void (*)(const mjModel *, mjData *, const mjvOption *, mjvScene *, int)",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "actuator_act_dot",
+          "c_type": "void (*)(const mjModel *, mjData *, int)",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "sdf_distance",
+          "c_type": "mjtNum (*)(const mjtNum *, const mjData *, int)",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "sdf_gradient",
+          "c_type": "void (*)(mjtNum *, const mjtNum *, const mjData *, int)",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "sdf_staticdistance",
+          "c_type": "mjtNum (*)(const mjtNum *, const mjtNum *)",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "sdf_attribute",
+          "c_type": "void (*)(mjtNum *, const char **, const char **)",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "sdf_aabb",
+          "c_type": "void (*)(mjtNum *, const mjtNum *)",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjplugin.h",
+      "line": 114
+    },
+    "mjSDF": {
+      "name": "mjSDF",
+      "fields": [
+        {
+          "name": "plugin",
+          "c_type": "const mjpPlugin **",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "id",
+          "c_type": "int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "type",
+          "c_type": "mjtSDFType",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "relpos",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "relmat",
+          "c_type": "mjtNum *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "geomtype",
+          "c_type": "mjtGeom *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjplugin.h",
+      "line": 175
+    },
+    "mjrRect": {
+      "name": "mjrRect",
+      "fields": [
+        {
+          "name": "left",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "bottom",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "width",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "height",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjrender.h",
+      "line": 69
+    },
+    "mjrContext": {
+      "name": "mjrContext",
+      "fields": [
+        {
+          "name": "lineWidth",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "shadowClip",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "shadowScale",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "fogStart",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "fogEnd",
+          "c_type": "float",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "fogRGBA",
+          "c_type": "float [4]",
+          "array_dim": 4,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "shadowSize",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "offWidth",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "offHeight",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "offSamples",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "fontScale",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "auxWidth",
+          "c_type": "int [10]",
+          "array_dim": 10,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "auxHeight",
+          "c_type": "int [10]",
+          "array_dim": 10,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "auxSamples",
+          "c_type": "int [10]",
+          "array_dim": 10,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "offFBO",
+          "c_type": "unsigned int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "offFBO_r",
+          "c_type": "unsigned int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "offColor",
+          "c_type": "unsigned int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "offColor_r",
+          "c_type": "unsigned int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "offDepthStencil",
+          "c_type": "unsigned int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "offDepthStencil_r",
+          "c_type": "unsigned int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "shadowFBO",
+          "c_type": "unsigned int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "shadowTex",
+          "c_type": "unsigned int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "auxFBO",
+          "c_type": "unsigned int [10]",
+          "array_dim": 10,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "auxFBO_r",
+          "c_type": "unsigned int [10]",
+          "array_dim": 10,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "auxColor",
+          "c_type": "unsigned int [10]",
+          "array_dim": 10,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "auxColor_r",
+          "c_type": "unsigned int [10]",
+          "array_dim": 10,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "mat_texid",
+          "c_type": "int [10000]",
+          "array_dim": 10000,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "mat_texuniform",
+          "c_type": "int [1000]",
+          "array_dim": 1000,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "mat_texrepeat",
+          "c_type": "float [2000]",
+          "array_dim": 2000,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "ntexture",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "textureType",
+          "c_type": "int [1000]",
+          "array_dim": 1000,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "texture",
+          "c_type": "unsigned int [1000]",
+          "array_dim": 1000,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "basePlane",
+          "c_type": "unsigned int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "baseMesh",
+          "c_type": "unsigned int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "baseHField",
+          "c_type": "unsigned int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "baseBuiltin",
+          "c_type": "unsigned int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "baseFontNormal",
+          "c_type": "unsigned int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "baseFontShadow",
+          "c_type": "unsigned int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "baseFontBig",
+          "c_type": "unsigned int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "rangePlane",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "rangeMesh",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "rangeHField",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "rangeBuiltin",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "rangeFont",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nskin",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "skinvertVBO",
+          "c_type": "unsigned int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "skinnormalVBO",
+          "c_type": "unsigned int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "skintexcoordVBO",
+          "c_type": "unsigned int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "skinfaceVBO",
+          "c_type": "unsigned int *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "charWidth",
+          "c_type": "int [127]",
+          "array_dim": 127,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "charWidthBig",
+          "c_type": "int [127]",
+          "array_dim": 127,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "charHeight",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "charHeightBig",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "glInitialized",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "windowAvailable",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "windowSamples",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "windowStereo",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "windowDoublebuffer",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "currentBuffer",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "readPixelFormat",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "readDepthMap",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjrender.h",
+      "line": 80
+    },
+    "mjuiState": {
+      "name": "mjuiState",
+      "fields": [
+        {
+          "name": "nrect",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "rect",
+          "c_type": "mjrRect [25]",
+          "array_dim": 25,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "userdata",
+          "c_type": "void *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "type",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "left",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "right",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "middle",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "doubleclick",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "button",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "buttontime",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "x",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "y",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "dx",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "dy",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "sx",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "sy",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "control",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "shift",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "alt",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "key",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "keytime",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "mouserect",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "dragrect",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "dragbutton",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "dropcount",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "droppaths",
+          "c_type": "const char **",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 123
+    },
+    "mjuiThemeSpacing": {
+      "name": "mjuiThemeSpacing",
+      "fields": [
+        {
+          "name": "total",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "scroll",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "label",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "section",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "cornersect",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "cornersep",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "itemside",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "itemmid",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "itemver",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "texthor",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "textver",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "linescroll",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "samples",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 169
+    },
+    "mjuiThemeColor": {
+      "name": "mjuiThemeColor",
+      "fields": [
+        {
+          "name": "master",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "thumb",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "secttitle",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "secttitle2",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "secttitleuncheck",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "secttitleuncheck2",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "secttitlecheck",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "secttitlecheck2",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "sectfont",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "sectsymbol",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "sectpane",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "separator",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "separator2",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "shortcut",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "fontactive",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "fontinactive",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "decorinactive",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "decorinactive2",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "button",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "check",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "radio",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "select",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "select2",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "slider",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "slider2",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "edit",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "edit2",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "cursor",
+          "c_type": "float [3]",
+          "array_dim": 3,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 189
+    },
+    "mjuiItemSingle": {
+      "name": "mjuiItemSingle",
+      "fields": [
+        {
+          "name": "modifier",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "shortcut",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 224
+    },
+    "mjuiItemMulti": {
+      "name": "mjuiItemMulti",
+      "fields": [
+        {
+          "name": "nelem",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "name",
+          "c_type": "char [35][40]",
+          "array_dim": 35,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 230
+    },
+    "mjuiItemSlider": {
+      "name": "mjuiItemSlider",
+      "fields": [
+        {
+          "name": "range",
+          "c_type": "double [2]",
+          "array_dim": 2,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "divisions",
+          "c_type": "double",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 236
+    },
+    "mjuiItemEdit": {
+      "name": "mjuiItemEdit",
+      "fields": [
+        {
+          "name": "nelem",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "range",
+          "c_type": "double [7][2]",
+          "array_dim": 7,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 242
+    },
+    "mjuiItem": {
+      "name": "mjuiItem",
+      "fields": [
+        {
+          "name": "type",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "name",
+          "c_type": "char [40]",
+          "array_dim": 40,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "state",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "pdata",
+          "c_type": "void *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "sectionid",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "itemid",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "userid",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "rect",
+          "c_type": "mjrRect",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "skip",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 248
+    },
+    "mjuiSection": {
+      "name": "mjuiSection",
+      "fields": [
+        {
+          "name": "name",
+          "c_type": "char [40]",
+          "array_dim": 40,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "state",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "modifier",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "shortcut",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "checkbox",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "nitem",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "item",
+          "c_type": "mjuiItem [200]",
+          "array_dim": 200,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "rtitle",
+          "c_type": "mjrRect",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "rcontent",
+          "c_type": "mjrRect",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "lastclick",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 275
+    },
+    "mjUI": {
+      "name": "mjUI",
+      "fields": [
+        {
+          "name": "spacing",
+          "c_type": "mjuiThemeSpacing",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "color",
+          "c_type": "mjuiThemeColor",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "predicate",
+          "c_type": "mjfItemEnable",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "userdata",
+          "c_type": "void *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "rectid",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "auxid",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "radiocol",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "width",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "height",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "maxheight",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "scroll",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "mousesect",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "mouseitem",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "mousehelp",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "mouseclicks",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "mousesectcheck",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "editsect",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "edititem",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "editcursor",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "editscroll",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "edittext",
+          "c_type": "char [300]",
+          "array_dim": 300,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "editchanged",
+          "c_type": "mjuiItem *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "nsect",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "sect",
+          "c_type": "mjuiSection [10]",
+          "array_dim": 10,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 295
+    },
+    "mjuiDef": {
+      "name": "mjuiDef",
+      "fields": [
+        {
+          "name": "type",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "name",
+          "c_type": "char [40]",
+          "array_dim": 40,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "state",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "pdata",
+          "c_type": "void *",
+          "array_dim": null,
+          "is_pointer": true,
+          "doc": ""
+        },
+        {
+          "name": "other",
+          "c_type": "char [300]",
+          "array_dim": 300,
+          "is_pointer": false,
+          "doc": ""
+        },
+        {
+          "name": "otherint",
+          "c_type": "int",
+          "array_dim": null,
+          "is_pointer": false,
+          "doc": ""
+        }
+      ],
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 335
+    }
+  },
+  "defines": {
+    "mjMINVAL": {
+      "value": "1E-15",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjtnum.h",
+      "line": 26
+    },
+    "mjPI": {
+      "value": "3.14159265358979323846",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 25
+    },
+    "mjMAXVAL": {
+      "value": "1E+10",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 26
+    },
+    "mjMINMU": {
+      "value": "1E-5",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 27
+    },
+    "mjMINIMP": {
+      "value": "0.0001",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 28
+    },
+    "mjMAXIMP": {
+      "value": "0.9999",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 29
+    },
+    "mjMAXCONPAIR": {
+      "value": "50",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 30
+    },
+    "mjMAXTREEDEPTH": {
+      "value": "50",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 31
+    },
+    "mjMAXFLEXNODES": {
+      "value": "27",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 32
+    },
+    "mjMINAWAKE": {
+      "value": "10",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 33
+    },
+    "mjNEQDATA": {
+      "value": "11",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 38
+    },
+    "mjNDYN": {
+      "value": "10",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 39
+    },
+    "mjNGAIN": {
+      "value": "10",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 40
+    },
+    "mjNBIAS": {
+      "value": "10",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 41
+    },
+    "mjNFLUID": {
+      "value": "12",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 42
+    },
+    "mjNREF": {
+      "value": "2",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 43
+    },
+    "mjNIMP": {
+      "value": "5",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 44
+    },
+    "mjNPOLY": {
+      "value": "2",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 45
+    },
+    "mjNSENS": {
+      "value": "3",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 46
+    },
+    "mjNSOLVER": {
+      "value": "200",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 47
+    },
+    "mjNISLAND": {
+      "value": "20",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmodel.h",
+      "line": 48
+    },
+    "MJOPTION_FIELDS": {
+      "value": "X ( mjtNum , timestep , 1 ) X ( mjtNum , impratio , 1 ) X ( mjtNum , tolerance , 1 ) X ( mjtNum , ls_tolerance , 1 ) X ( mjtNum , noslip_tolerance , 1 ) X ( mjtNum , ccd_tolerance , 1 ) X ( mjtNum , sleep_tolerance , 1 ) XVEC ( mjtNum , gravity , 3 ) XVEC ( mjtNum , wind , 3 ) XVEC ( mjtNum , magnetic , 3 ) X ( mjtNum , density , 1 ) X ( mjtNum , viscosity , 1 ) X ( mjtNum , o_margin , 1 ) XVEC ( mjtNum , o_solref , mjNREF ) XVEC ( mjtNum , o_solimp , mjNIMP ) XVEC ( mjtNum , o_friction , 5 ) X ( int , integrator , 1 ) X ( int , cone , 1 ) X ( int , jacobian , 1 ) X ( int , solver , 1 ) X ( int , iterations , 1 ) X ( int , ls_iterations , 1 ) X ( int , noslip_iterations , 1 ) X ( int , ccd_iterations , 1 ) X ( int , disableflags , 1 ) X ( int , enableflags , 1 ) X ( int , disableactuator , 1 ) X ( int , sdf_initpoints , 1 ) X ( int , sdf_iterations , 1 )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 23
+    },
+    "MJSTATISTIC_FIELDS": {
+      "value": "X ( meaninertia , 1 ) X ( meanmass , 1 ) X ( meansize , 1 ) X ( extent , 1 ) XVEC ( center , 3 )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 58
+    },
+    "MJVISUAL_GLOBAL_FIELDS": {
+      "value": "X ( int , cameraid ) X ( int , orthographic ) X ( float , fovy ) X ( float , ipd ) X ( float , azimuth ) X ( float , elevation ) X ( float , linewidth ) X ( float , glow ) X ( float , realtime ) X ( int , offwidth ) X ( int , offheight ) X ( int , ellipsoidinertia ) X ( int , bvactive )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 69
+    },
+    "MJVISUAL_QUALITY_FIELDS": {
+      "value": "X ( shadowsize ) X ( offsamples ) X ( numslices ) X ( numstacks ) X ( numquads )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 84
+    },
+    "MJVISUAL_HEADLIGHT_FIELDS": {
+      "value": "XVEC ( float , ambient , 3 ) XVEC ( float , diffuse , 3 ) XVEC ( float , specular , 3 ) X ( int , active , 1 )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 91
+    },
+    "MJVISUAL_MAP_FIELDS": {
+      "value": "X ( stiffness ) X ( stiffnessrot ) X ( force ) X ( torque ) X ( alpha ) X ( fogstart ) X ( fogend ) X ( znear ) X ( zfar ) X ( haze ) X ( shadowclip ) X ( shadowscale ) X ( actuatortendon )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 97
+    },
+    "MJVISUAL_SCALE_FIELDS": {
+      "value": "X ( forcewidth ) X ( contactwidth ) X ( contactheight ) X ( connect ) X ( com ) X ( camera ) X ( light ) X ( selectpoint ) X ( jointlength ) X ( jointwidth ) X ( actuatorlength ) X ( actuatorwidth ) X ( framelength ) X ( framewidth ) X ( constraint ) X ( slidercrank ) X ( frustum )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 112
+    },
+    "MJVISUAL_RGBA_FIELDS": {
+      "value": "X ( fog ) X ( haze ) X ( force ) X ( inertia ) X ( joint ) X ( actuator ) X ( actuatornegative ) X ( actuatorpositive ) X ( com ) X ( camera ) X ( light ) X ( selectpoint ) X ( connect ) X ( contactpoint ) X ( contactforce ) X ( contactfriction ) X ( contacttorque ) X ( contactgap ) X ( rangefinder ) X ( constraint ) X ( slidercrank ) X ( crankbroken ) X ( frustum ) X ( bv ) X ( bvactive )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 131
+    },
+    "MJMODEL_SIZES": {
+      "value": "X ( nq ) X ( nv ) X ( nu ) X ( na ) X ( nbody ) X ( nbvh ) X ( nbvhstatic ) X ( nbvhdynamic ) X ( noct ) X ( njnt ) X ( ntree ) X ( nM ) X ( nB ) X ( nC ) X ( nD ) X ( ngeom ) X ( nsite ) X ( ncam ) X ( nlight ) X ( nflex ) X ( nflexnode ) X ( nflexvert ) X ( nflexedge ) X ( nflexelem ) X ( nflexelemdata ) X ( nflexstiffness ) X ( nflexbending ) X ( nflexelemedge ) X ( nflexshelldata ) X ( nflexevpair ) X ( nflextexcoord ) X ( nJfe ) X ( nJfv ) X ( nmesh ) X ( nmeshvert ) X ( nmeshnormal ) X ( nmeshtexcoord ) X ( nmeshface ) X ( nmeshgraph ) X ( nmeshpoly ) X ( nmeshpolyvert ) X ( nmeshpolymap ) X ( nskin ) X ( nskinvert ) X ( nskintexvert ) X ( nskinface ) X ( nskinbone ) X ( nskinbonevert ) X ( nhfield ) X ( nhfielddata ) X ( ntex ) X ( ntexdata ) X ( nmat ) X ( npair ) X ( nexclude ) X ( neq ) X ( ntendon ) X ( nJten ) X ( nwrap ) X ( nsensor ) X ( nnumeric ) X ( nnumericdata ) X ( ntext ) X ( ntextdata ) X ( ntuple ) X ( ntupledata ) X ( nkey ) X ( nmocap ) X ( nplugin ) X ( npluginattr ) X ( nuser_body ) X ( nuser_jnt ) X ( nuser_geom ) X ( nuser_site ) X ( nuser_cam ) X ( nuser_tendon ) X ( nuser_actuator ) X ( nuser_sensor ) X ( nnames ) X ( npaths ) X ( nnames_map ) X ( nJmom ) X ( ngravcomp ) X ( nemax ) X ( njmax ) X ( nconmax ) X ( nuserdata ) X ( nsensordata ) X ( npluginstate ) X ( nhistory ) X ( narena ) X ( nbuffer )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 162
+    },
+    "MJMODEL_POINTERS_PREAMBLE": {
+      "value": "( m ) int nuser_body = m -> nuser_body ; int nuser_jnt = m -> nuser_jnt ; int nuser_geom = m -> nuser_geom ; int nuser_site = m -> nuser_site ; int nuser_cam = m -> nuser_cam ; int nuser_tendon = m -> nuser_tendon ; int nuser_actuator = m -> nuser_actuator ; int nuser_sensor = m -> nuser_sensor ; int nq = m -> nq ; int nv = m -> nv ; int na = m -> na ; int nu = m -> nu ; int nmocap = m -> nmocap ;",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 260
+    },
+    "MJ_M": {
+      "value": "( n ) n",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 277
+    },
+    "MJMODEL_POINTERS_BODY": {
+      "value": "X ( int , body_parentid , nbody , 1 ) X ( int , body_rootid , nbody , 1 ) X ( int , body_weldid , nbody , 1 ) X ( int , body_mocapid , nbody , 1 ) X ( int , body_jntnum , nbody , 1 ) X ( int , body_jntadr , nbody , 1 ) X ( int , body_dofnum , nbody , 1 ) X ( int , body_dofadr , nbody , 1 ) X ( int , body_treeid , nbody , 1 ) X ( int , body_geomnum , nbody , 1 ) X ( int , body_geomadr , nbody , 1 ) X ( mjtByte , body_simple , nbody , 1 ) X ( mjtByte , body_sameframe , nbody , 1 ) X ( mjtNum , body_pos , nbody , 3 ) X ( mjtNum , body_quat , nbody , 4 ) X ( mjtNum , body_ipos , nbody , 3 ) X ( mjtNum , body_iquat , nbody , 4 ) X ( mjtNum , body_mass , nbody , 1 ) X ( mjtNum , body_subtreemass , nbody , 1 ) X ( mjtNum , body_inertia , nbody , 3 ) X ( mjtNum , body_invweight0 , nbody , 2 ) X ( mjtNum , body_gravcomp , nbody , 1 ) X ( mjtNum , body_margin , nbody , 1 ) X ( mjtNum , body_user , nbody , MJ_M ( nuser_body ) ) X ( int , body_plugin , nbody , 1 ) X ( int , body_contype , nbody , 1 ) X ( int , body_conaffinity , nbody , 1 ) X ( int , body_bvhadr , nbody , 1 ) X ( int , body_bvhnum , nbody , 1 )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 283
+    },
+    "MJMODEL_POINTERS_JOINT": {
+      "value": "X ( int , jnt_type , njnt , 1 ) X ( int , jnt_qposadr , njnt , 1 ) X ( int , jnt_dofadr , njnt , 1 ) X ( int , jnt_bodyid , njnt , 1 ) X ( int , jnt_actuatorid , njnt , 1 ) X ( int , jnt_group , njnt , 1 ) X ( mjtByte , jnt_limited , njnt , 1 ) X ( mjtByte , jnt_actfrclimited , njnt , 1 ) X ( mjtByte , jnt_actgravcomp , njnt , 1 ) X ( mjtNum , jnt_solref , njnt , mjNREF ) X ( mjtNum , jnt_solimp , njnt , mjNIMP ) X ( mjtNum , jnt_pos , njnt , 3 ) X ( mjtNum , jnt_axis , njnt , 3 ) X ( mjtNum , jnt_stiffness , njnt , 1 ) X ( mjtNum , jnt_stiffnesspoly , njnt , mjNPOLY ) X ( mjtNum , jnt_range , njnt , 2 ) X ( mjtNum , jnt_actfrcrange , njnt , 2 ) X ( mjtNum , jnt_margin , njnt , 1 ) X ( mjtNum , jnt_user , njnt , MJ_M ( nuser_jnt ) )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 314
+    },
+    "MJMODEL_POINTERS_DOF": {
+      "value": "X ( int , dof_bodyid , nv , 1 ) X ( int , dof_jntid , nv , 1 ) X ( int , dof_parentid , nv , 1 ) X ( int , dof_treeid , nv , 1 ) X ( int , dof_Madr , nv , 1 ) X ( int , dof_simplenum , nv , 1 ) X ( mjtNum , dof_solref , nv , mjNREF ) X ( mjtNum , dof_solimp , nv , mjNIMP ) X ( mjtNum , dof_frictionloss , nv , 1 ) X ( mjtNum , dof_armature , nv , 1 ) X ( mjtNum , dof_damping , nv , 1 ) X ( mjtNum , dof_dampingpoly , nv , mjNPOLY ) X ( mjtNum , dof_invweight0 , nv , 1 ) X ( mjtNum , dof_M0 , nv , 1 ) X ( mjtNum , dof_length , nv , 1 )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 335
+    },
+    "MJMODEL_POINTERS_TREE": {
+      "value": "X ( int , tree_bodyadr , ntree , 1 ) X ( int , tree_bodynum , ntree , 1 ) X ( int , tree_dofadr , ntree , 1 ) X ( int , tree_dofnum , ntree , 1 ) X ( int , tree_sleep_policy , ntree , 1 )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 352
+    },
+    "MJMODEL_POINTERS_GEOM": {
+      "value": "X ( int , geom_type , ngeom , 1 ) X ( int , geom_contype , ngeom , 1 ) X ( int , geom_conaffinity , ngeom , 1 ) X ( int , geom_condim , ngeom , 1 ) X ( int , geom_bodyid , ngeom , 1 ) X ( int , geom_dataid , ngeom , 1 ) X ( int , geom_matid , ngeom , 1 ) X ( int , geom_group , ngeom , 1 ) X ( int , geom_priority , ngeom , 1 ) X ( int , geom_plugin , ngeom , 1 ) X ( mjtByte , geom_sameframe , ngeom , 1 ) X ( mjtNum , geom_solmix , ngeom , 1 ) X ( mjtNum , geom_solref , ngeom , mjNREF ) X ( mjtNum , geom_solimp , ngeom , mjNIMP ) X ( mjtNum , geom_size , ngeom , 3 ) X ( mjtNum , geom_aabb , ngeom , 6 ) X ( mjtNum , geom_rbound , ngeom , 1 ) X ( mjtNum , geom_pos , ngeom , 3 ) X ( mjtNum , geom_quat , ngeom , 4 ) X ( mjtNum , geom_friction , ngeom , 3 ) X ( mjtNum , geom_margin , ngeom , 1 ) X ( mjtNum , geom_gap , ngeom , 1 ) XNV ( mjtNum , geom_fluid , ngeom , mjNFLUID ) X ( mjtNum , geom_user , ngeom , MJ_M ( nuser_geom ) ) X ( float , geom_rgba , ngeom , 4 )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 359
+    },
+    "MJMODEL_POINTERS_SITE": {
+      "value": "X ( int , site_type , nsite , 1 ) X ( int , site_bodyid , nsite , 1 ) X ( int , site_matid , nsite , 1 ) X ( int , site_group , nsite , 1 ) X ( mjtByte , site_sameframe , nsite , 1 ) X ( mjtNum , site_size , nsite , 3 ) X ( mjtNum , site_pos , nsite , 3 ) X ( mjtNum , site_quat , nsite , 4 ) X ( mjtNum , site_user , nsite , MJ_M ( nuser_site ) ) X ( float , site_rgba , nsite , 4 )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 386
+    },
+    "MJMODEL_POINTERS_CAMERA": {
+      "value": "X ( int , cam_mode , ncam , 1 ) X ( int , cam_bodyid , ncam , 1 ) X ( int , cam_targetbodyid , ncam , 1 ) X ( mjtNum , cam_pos , ncam , 3 ) X ( mjtNum , cam_quat , ncam , 4 ) X ( mjtNum , cam_poscom0 , ncam , 3 ) X ( mjtNum , cam_pos0 , ncam , 3 ) X ( mjtNum , cam_mat0 , ncam , 9 ) X ( int , cam_projection , ncam , 1 ) X ( mjtNum , cam_fovy , ncam , 1 ) X ( mjtNum , cam_ipd , ncam , 1 ) X ( int , cam_resolution , ncam , 2 ) X ( int , cam_output , ncam , 1 ) X ( float , cam_sensorsize , ncam , 2 ) X ( float , cam_intrinsic , ncam , 4 ) X ( mjtNum , cam_user , ncam , MJ_M ( nuser_cam ) )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 398
+    },
+    "MJMODEL_POINTERS_LIGHT": {
+      "value": "X ( int , light_mode , nlight , 1 ) X ( int , light_bodyid , nlight , 1 ) X ( int , light_targetbodyid , nlight , 1 ) X ( int , light_type , nlight , 1 ) X ( int , light_texid , nlight , 1 ) X ( mjtByte , light_castshadow , nlight , 1 ) X ( float , light_bulbradius , nlight , 1 ) X ( float , light_intensity , nlight , 1 ) X ( float , light_range , nlight , 1 ) X ( mjtByte , light_active , nlight , 1 ) X ( mjtNum , light_pos , nlight , 3 ) X ( mjtNum , light_dir , nlight , 3 ) X ( mjtNum , light_poscom0 , nlight , 3 ) X ( mjtNum , light_pos0 , nlight , 3 ) X ( mjtNum , light_dir0 , nlight , 3 ) X ( float , light_attenuation , nlight , 3 ) X ( float , light_cutoff , nlight , 1 ) X ( float , light_exponent , nlight , 1 ) X ( float , light_ambient , nlight , 3 ) X ( float , light_diffuse , nlight , 3 ) X ( float , light_specular , nlight , 3 )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 416
+    },
+    "MJMODEL_POINTERS_FLEX": {
+      "value": "X ( int , flex_contype , nflex , 1 ) X ( int , flex_conaffinity , nflex , 1 ) X ( int , flex_condim , nflex , 1 ) X ( int , flex_priority , nflex , 1 ) X ( mjtNum , flex_solmix , nflex , 1 ) X ( mjtNum , flex_solref , nflex , mjNREF ) X ( mjtNum , flex_solimp , nflex , mjNIMP ) X ( mjtNum , flex_friction , nflex , 3 ) X ( mjtNum , flex_margin , nflex , 1 ) X ( mjtNum , flex_gap , nflex , 1 ) X ( mjtByte , flex_internal , nflex , 1 ) X ( int , flex_selfcollide , nflex , 1 ) X ( int , flex_activelayers , nflex , 1 ) X ( int , flex_passive , nflex , 1 ) X ( int , flex_dim , nflex , 1 ) X ( int , flex_matid , nflex , 1 ) X ( int , flex_group , nflex , 1 ) X ( int , flex_interp , nflex , 1 ) X ( int , flex_bandwidth , nflex , 1 ) X ( int , flex_cellnum , nflex , 3 ) X ( int , flex_nodeadr , nflex , 1 ) X ( int , flex_nodenum , nflex , 1 ) X ( int , flex_vertadr , nflex , 1 ) X ( int , flex_vertnum , nflex , 1 ) X ( int , flex_edgeadr , nflex , 1 ) X ( int , flex_edgenum , nflex , 1 ) X ( int , flex_elemadr , nflex , 1 ) X ( int , flex_elemnum , nflex , 1 ) X ( int , flex_elemdataadr , nflex , 1 ) X ( int , flex_stiffnessadr , nflex , 1 ) X ( int , flex_elemedgeadr , nflex , 1 ) X ( int , flex_bendingadr , nflex , 1 ) X ( int , flex_shellnum , nflex , 1 ) X ( int , flex_shelldataadr , nflex , 1 ) X ( int , flex_evpairadr , nflex , 1 ) X ( int , flex_evpairnum , nflex , 1 ) X ( int , flex_texcoordadr , nflex , 1 ) X ( int , flex_nodebodyid , nflexnode , 1 ) X ( int , flex_vertbodyid , nflexvert , 1 ) X ( int , flex_vertedgeadr , nflexvert , 1 ) X ( int , flex_vertedgenum , nflexvert , 1 ) X ( int , flex_vertedge , nflexedge , 2 ) X ( int , flex_edge , nflexedge , 2 ) X ( int , flex_edgeflap , nflexedge , 2 ) X ( int , flex_elem , nflexelemdata , 1 ) X ( int , flex_elemtexcoord , nflexelemdata , 1 ) X ( int , flex_elemedge , nflexelemedge , 1 ) X ( int , flex_elemlayer , nflexelem , 1 ) X ( int , flex_shell , nflexshelldata , 1 ) X ( int , flex_evpair , nflexevpair , 2 ) X ( mjtNum , flex_vert , nflexvert , 3 ) X ( mjtNum , flex_vert0 , nflexvert , 3 ) X ( mjtNum , flex_vertmetric , nflexvert , 4 ) X ( mjtNum , flex_node , nflexnode , 3 ) X ( mjtNum , flex_node0 , nflexnode , 3 ) X ( mjtNum , flexedge_length0 , nflexedge , 1 ) X ( mjtNum , flexedge_invweight0 , nflexedge , 1 ) X ( mjtNum , flex_radius , nflex , 1 ) X ( mjtNum , flex_size , nflex , 3 ) X ( mjtNum , flex_stiffness , nflexstiffness , 1 ) X ( mjtNum , flex_bending , nflexbending , 1 ) X ( mjtNum , flex_damping , nflex , 1 ) X ( mjtNum , flex_edgestiffness , nflex , 1 ) X ( mjtNum , flex_edgedamping , nflex , 1 ) X ( int , flex_edgeequality , nflex , 1 ) X ( mjtByte , flex_rigid , nflex , 1 ) X ( mjtByte , flexedge_rigid , nflexedge , 1 ) X ( mjtByte , flex_centered , nflex , 1 ) X ( mjtByte , flex_flatskin , nflex , 1 ) X ( int , flex_bvhadr , nflex , 1 ) X ( int , flex_bvhnum , nflex , 1 ) X ( int , flexedge_J_rownnz , nflexedge , 1 ) X ( int , flexedge_J_rowadr , nflexedge , 1 ) X ( int , flexedge_J_colind , nJfe , 1 ) X ( int , flexvert_J_rownnz , nflexvert , 2 ) X ( int , flexvert_J_rowadr , nflexvert , 2 ) X ( int , flexvert_J_colind , nJfv , 2 ) X ( float , flex_rgba , nflex , 4 ) X ( float , flex_texcoord , nflextexcoord , 2 )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 439
+    },
+    "MJMODEL_POINTERS_MESH": {
+      "value": "X ( int , mesh_vertadr , nmesh , 1 ) X ( int , mesh_vertnum , nmesh , 1 ) X ( int , mesh_faceadr , nmesh , 1 ) X ( int , mesh_facenum , nmesh , 1 ) X ( int , mesh_bvhadr , nmesh , 1 ) X ( int , mesh_bvhnum , nmesh , 1 ) X ( int , mesh_octadr , nmesh , 1 ) X ( int , mesh_octnum , nmesh , 1 ) X ( int , mesh_normaladr , nmesh , 1 ) X ( int , mesh_normalnum , nmesh , 1 ) X ( int , mesh_texcoordadr , nmesh , 1 ) X ( int , mesh_texcoordnum , nmesh , 1 ) X ( int , mesh_graphadr , nmesh , 1 ) XNV ( float , mesh_vert , nmeshvert , 3 ) XNV ( float , mesh_normal , nmeshnormal , 3 ) XNV ( float , mesh_texcoord , nmeshtexcoord , 2 ) XNV ( int , mesh_face , nmeshface , 3 ) XNV ( int , mesh_facenormal , nmeshface , 3 ) XNV ( int , mesh_facetexcoord , nmeshface , 3 ) XNV ( int , mesh_graph , nmeshgraph , 1 ) X ( mjtNum , mesh_scale , nmesh , 3 ) X ( mjtNum , mesh_pos , nmesh , 3 ) X ( mjtNum , mesh_quat , nmesh , 4 ) X ( int , mesh_pathadr , nmesh , 1 ) XNV ( int , mesh_polynum , nmesh , 1 ) XNV ( int , mesh_polyadr , nmesh , 1 ) XNV ( mjtNum , mesh_polynormal , nmeshpoly , 3 ) XNV ( int , mesh_polyvertadr , nmeshpoly , 1 ) XNV ( int , mesh_polyvertnum , nmeshpoly , 1 ) XNV ( int , mesh_polyvert , nmeshpolyvert , 1 ) XNV ( int , mesh_polymapadr , nmeshvert , 1 ) XNV ( int , mesh_polymapnum , nmeshvert , 1 ) XNV ( int , mesh_polymap , nmeshpolymap , 1 )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 520
+    },
+    "MJMODEL_POINTERS_SKIN": {
+      "value": "X ( int , skin_matid , nskin , 1 ) X ( int , skin_group , nskin , 1 ) X ( float , skin_rgba , nskin , 4 ) X ( float , skin_inflate , nskin , 1 ) X ( int , skin_vertadr , nskin , 1 ) X ( int , skin_vertnum , nskin , 1 ) X ( int , skin_texcoordadr , nskin , 1 ) X ( int , skin_faceadr , nskin , 1 ) X ( int , skin_facenum , nskin , 1 ) X ( int , skin_boneadr , nskin , 1 ) X ( int , skin_bonenum , nskin , 1 ) X ( float , skin_vert , nskinvert , 3 ) X ( float , skin_texcoord , nskintexvert , 2 ) X ( int , skin_face , nskinface , 3 ) X ( int , skin_bonevertadr , nskinbone , 1 ) X ( int , skin_bonevertnum , nskinbone , 1 ) X ( float , skin_bonebindpos , nskinbone , 3 ) X ( float , skin_bonebindquat , nskinbone , 4 ) X ( int , skin_bonebodyid , nskinbone , 1 ) X ( int , skin_bonevertid , nskinbonevert , 1 ) X ( float , skin_bonevertweight , nskinbonevert , 1 ) X ( int , skin_pathadr , nskin , 1 )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 555
+    },
+    "MJMODEL_POINTERS_HFIELD": {
+      "value": "X ( mjtNum , hfield_size , nhfield , 4 ) X ( int , hfield_nrow , nhfield , 1 ) X ( int , hfield_ncol , nhfield , 1 ) X ( int , hfield_adr , nhfield , 1 ) XNV ( float , hfield_data , nhfielddata , 1 ) X ( int , hfield_pathadr , nhfield , 1 )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 579
+    },
+    "MJMODEL_POINTERS_TEXTURE": {
+      "value": "X ( int , tex_type , ntex , 1 ) X ( int , tex_colorspace , ntex , 1 ) X ( int , tex_height , ntex , 1 ) X ( int , tex_width , ntex , 1 ) X ( int , tex_nchannel , ntex , 1 ) X ( mjtSize , tex_adr , ntex , 1 ) XNV ( mjtByte , tex_data , ntexdata , 1 ) X ( int , tex_pathadr , ntex , 1 )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 587
+    },
+    "MJMODEL_POINTERS_MATERIAL": {
+      "value": "X ( int , mat_texid , nmat , mjNTEXROLE ) X ( mjtByte , mat_texuniform , nmat , 1 ) X ( float , mat_texrepeat , nmat , 2 ) X ( float , mat_emission , nmat , 1 ) X ( float , mat_specular , nmat , 1 ) X ( float , mat_shininess , nmat , 1 ) X ( float , mat_reflectance , nmat , 1 ) X ( float , mat_metallic , nmat , 1 ) X ( float , mat_roughness , nmat , 1 ) X ( float , mat_rgba , nmat , 4 )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 597
+    },
+    "MJMODEL_POINTERS_PAIR": {
+      "value": "X ( int , pair_dim , npair , 1 ) X ( int , pair_geom1 , npair , 1 ) X ( int , pair_geom2 , npair , 1 ) X ( int , pair_signature , npair , 1 ) X ( mjtNum , pair_solref , npair , mjNREF ) X ( mjtNum , pair_solreffriction , npair , mjNREF ) X ( mjtNum , pair_solimp , npair , mjNIMP ) X ( mjtNum , pair_margin , npair , 1 ) X ( mjtNum , pair_gap , npair , 1 ) X ( mjtNum , pair_friction , npair , 5 )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 609
+    },
+    "MJMODEL_POINTERS_EXCLUDE": {
+      "value": "X ( int , exclude_signature , nexclude , 1 )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 621
+    },
+    "MJMODEL_POINTERS_EQUALITY": {
+      "value": "X ( int , eq_type , neq , 1 ) X ( int , eq_obj1id , neq , 1 ) X ( int , eq_obj2id , neq , 1 ) X ( int , eq_objtype , neq , 1 ) X ( mjtByte , eq_active0 , neq , 1 ) X ( mjtNum , eq_solref , neq , mjNREF ) X ( mjtNum , eq_solimp , neq , mjNIMP ) X ( mjtNum , eq_data , neq , mjNEQDATA )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 624
+    },
+    "MJMODEL_POINTERS_TENDON": {
+      "value": "X ( int , tendon_adr , ntendon , 1 ) X ( int , tendon_num , ntendon , 1 ) X ( int , tendon_matid , ntendon , 1 ) X ( int , tendon_actuatorid , ntendon , 1 ) X ( int , tendon_group , ntendon , 1 ) X ( int , tendon_treenum , ntendon , 1 ) X ( int , tendon_treeid , ntendon , 2 ) X ( int , ten_J_rownnz , ntendon , 1 ) X ( int , ten_J_rowadr , ntendon , 1 ) X ( int , ten_J_colind , nJten , 1 ) X ( mjtByte , tendon_limited , ntendon , 1 ) X ( mjtByte , tendon_actfrclimited , ntendon , 1 ) X ( mjtNum , tendon_width , ntendon , 1 ) X ( mjtNum , tendon_solref_lim , ntendon , mjNREF ) X ( mjtNum , tendon_solimp_lim , ntendon , mjNIMP ) X ( mjtNum , tendon_solref_fri , ntendon , mjNREF ) X ( mjtNum , tendon_solimp_fri , ntendon , mjNIMP ) X ( mjtNum , tendon_range , ntendon , 2 ) X ( mjtNum , tendon_actfrcrange , ntendon , 2 ) X ( mjtNum , tendon_margin , ntendon , 1 ) X ( mjtNum , tendon_stiffness , ntendon , 1 ) X ( mjtNum , tendon_stiffnesspoly , ntendon , mjNPOLY ) X ( mjtNum , tendon_damping , ntendon , 1 ) X ( mjtNum , tendon_dampingpoly , ntendon , mjNPOLY ) X ( mjtNum , tendon_armature , ntendon , 1 ) X ( mjtNum , tendon_frictionloss , ntendon , 1 ) X ( mjtNum , tendon_lengthspring , ntendon , 2 ) X ( mjtNum , tendon_length0 , ntendon , 1 ) X ( mjtNum , tendon_invweight0 , ntendon , 1 ) X ( mjtNum , tendon_user , ntendon , MJ_M ( nuser_tendon ) ) X ( float , tendon_rgba , ntendon , 4 )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 634
+    },
+    "MJMODEL_POINTERS_ACTUATOR": {
+      "value": "X ( int , actuator_trntype , nu , 1 ) X ( int , actuator_dyntype , nu , 1 ) X ( int , actuator_gaintype , nu , 1 ) X ( int , actuator_biastype , nu , 1 ) X ( int , actuator_trnid , nu , 2 ) X ( mjtNum , actuator_damping , nu , 1 ) X ( mjtNum , actuator_dampingpoly , nu , mjNPOLY ) X ( mjtNum , actuator_armature , nu , 1 ) X ( int , actuator_actadr , nu , 1 ) X ( int , actuator_actnum , nu , 1 ) X ( int , actuator_group , nu , 1 ) X ( int , actuator_history , nu , 2 ) X ( int , actuator_historyadr , nu , 1 ) X ( mjtNum , actuator_delay , nu , 1 ) X ( mjtByte , actuator_ctrllimited , nu , 1 ) X ( mjtByte , actuator_forcelimited , nu , 1 ) X ( mjtByte , actuator_actlimited , nu , 1 ) X ( mjtNum , actuator_dynprm , nu , mjNDYN ) X ( mjtNum , actuator_gainprm , nu , mjNGAIN ) X ( mjtNum , actuator_biasprm , nu , mjNBIAS ) X ( mjtByte , actuator_actearly , nu , 1 ) X ( mjtNum , actuator_ctrlrange , nu , 2 ) X ( mjtNum , actuator_forcerange , nu , 2 ) X ( mjtNum , actuator_actrange , nu , 2 ) X ( mjtNum , actuator_gear , nu , 6 ) X ( mjtNum , actuator_cranklength , nu , 1 ) X ( mjtNum , actuator_acc0 , nu , 1 ) X ( mjtNum , actuator_length0 , nu , 1 ) X ( mjtNum , actuator_lengthrange , nu , 2 ) X ( mjtNum , actuator_user , nu , MJ_M ( nuser_actuator ) ) X ( int , actuator_plugin , nu , 1 )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 667
+    },
+    "MJMODEL_POINTERS_SENSOR": {
+      "value": "X ( int , sensor_type , nsensor , 1 ) X ( int , sensor_datatype , nsensor , 1 ) X ( int , sensor_needstage , nsensor , 1 ) X ( int , sensor_objtype , nsensor , 1 ) X ( int , sensor_objid , nsensor , 1 ) X ( int , sensor_reftype , nsensor , 1 ) X ( int , sensor_refid , nsensor , 1 ) X ( int , sensor_intprm , nsensor , mjNSENS ) X ( int , sensor_dim , nsensor , 1 ) X ( int , sensor_adr , nsensor , 1 ) X ( mjtNum , sensor_cutoff , nsensor , 1 ) X ( mjtNum , sensor_noise , nsensor , 1 ) X ( int , sensor_history , nsensor , 2 ) X ( int , sensor_historyadr , nsensor , 1 ) X ( mjtNum , sensor_delay , nsensor , 1 ) X ( mjtNum , sensor_interval , nsensor , 2 ) X ( mjtNum , sensor_user , nsensor , MJ_M ( nuser_sensor ) ) X ( int , sensor_plugin , nsensor , 1 )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 700
+    },
+    "MJMODEL_POINTERS": {
+      "value": "X ( mjtNum , qpos0 , nq , 1 ) X ( mjtNum , qpos_spring , nq , 1 ) MJMODEL_POINTERS_BODY X ( int , bvh_depth , nbvh , 1 ) X ( int , bvh_child , nbvh , 2 ) X ( int , bvh_nodeid , nbvh , 1 ) X ( mjtNum , bvh_aabb , nbvhstatic , 6 ) X ( int , oct_depth , noct , 1 ) X ( int , oct_child , noct , 8 ) X ( mjtNum , oct_aabb , noct , 6 ) X ( mjtNum , oct_coeff , noct , 8 ) MJMODEL_POINTERS_JOINT MJMODEL_POINTERS_DOF MJMODEL_POINTERS_TREE MJMODEL_POINTERS_GEOM MJMODEL_POINTERS_SITE MJMODEL_POINTERS_CAMERA MJMODEL_POINTERS_LIGHT MJMODEL_POINTERS_FLEX MJMODEL_POINTERS_MESH MJMODEL_POINTERS_SKIN MJMODEL_POINTERS_HFIELD MJMODEL_POINTERS_TEXTURE MJMODEL_POINTERS_MATERIAL MJMODEL_POINTERS_PAIR MJMODEL_POINTERS_EXCLUDE MJMODEL_POINTERS_EQUALITY MJMODEL_POINTERS_TENDON X ( int , wrap_type , nwrap , 1 ) X ( int , wrap_objid , nwrap , 1 ) X ( mjtNum , wrap_prm , nwrap , 1 ) MJMODEL_POINTERS_ACTUATOR MJMODEL_POINTERS_SENSOR X ( int , plugin , nplugin , 1 ) X ( int , plugin_stateadr , nplugin , 1 ) X ( int , plugin_statenum , nplugin , 1 ) X ( char , plugin_attr , npluginattr , 1 ) X ( int , plugin_attradr , nplugin , 1 ) X ( int , numeric_adr , nnumeric , 1 ) X ( int , numeric_size , nnumeric , 1 ) X ( mjtNum , numeric_data , nnumericdata , 1 ) X ( int , text_adr , ntext , 1 ) X ( int , text_size , ntext , 1 ) X ( char , text_data , ntextdata , 1 ) X ( int , tuple_adr , ntuple , 1 ) X ( int , tuple_size , ntuple , 1 ) X ( int , tuple_objtype , ntupledata , 1 ) X ( int , tuple_objid , ntupledata , 1 ) X ( mjtNum , tuple_objprm , ntupledata , 1 ) X ( mjtNum , key_time , nkey , 1 ) X ( mjtNum , key_qpos , nkey , MJ_M ( nq ) ) X ( mjtNum , key_qvel , nkey , MJ_M ( nv ) ) X ( mjtNum , key_act , nkey , MJ_M ( na ) ) X ( mjtNum , key_mpos , nkey , MJ_M ( nmocap ) * 3 ) X ( mjtNum , key_mquat , nkey , MJ_M ( nmocap ) * 4 ) X ( mjtNum , key_ctrl , nkey , MJ_M ( nu ) ) X ( int , name_bodyadr , nbody , 1 ) X ( int , name_jntadr , njnt , 1 ) X ( int , name_geomadr , ngeom , 1 ) X ( int , name_siteadr , nsite , 1 ) X ( int , name_camadr , ncam , 1 ) X ( int , name_lightadr , nlight , 1 ) X ( int , name_flexadr , nflex , 1 ) X ( int , name_meshadr , nmesh , 1 ) X ( int , name_skinadr , nskin , 1 ) X ( int , name_hfieldadr , nhfield , 1 ) X ( int , name_texadr , ntex , 1 ) X ( int , name_matadr , nmat , 1 ) X ( int , name_pairadr , npair , 1 ) X ( int , name_excludeadr , nexclude , 1 ) X ( int , name_eqadr , neq , 1 ) X ( int , name_tendonadr , ntendon , 1 ) X ( int , name_actuatoradr , nu , 1 ) X ( int , name_sensoradr , nsensor , 1 ) X ( int , name_numericadr , nnumeric , 1 ) X ( int , name_textadr , ntext , 1 ) X ( int , name_tupleadr , ntuple , 1 ) X ( int , name_keyadr , nkey , 1 ) X ( int , name_pluginadr , nplugin , 1 ) X ( char , names , nnames , 1 ) X ( int , names_map , nnames_map , 1 ) X ( char , paths , npaths , 1 ) X ( int , B_rownnz , nbody , 1 ) X ( int , B_rowadr , nbody , 1 ) X ( int , B_colind , nB , 1 ) X ( int , M_rownnz , nv , 1 ) X ( int , M_rowadr , nv , 1 ) X ( int , M_colind , nC , 1 ) X ( int , mapM2M , nC , 1 ) X ( int , D_rownnz , nv , 1 ) X ( int , D_rowadr , nv , 1 ) X ( int , D_diag , nv , 1 ) X ( int , D_colind , nD , 1 ) X ( int , mapM2D , nD , 1 ) X ( int , mapD2M , nC , 1 )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 720
+    },
+    "MJDATA_POINTERS": {
+      "value": "X ( mjtNum , qpos , nq , 1 ) X ( mjtNum , qvel , nv , 1 ) X ( mjtNum , act , na , 1 ) X ( mjtNum , history , nhistory , 1 ) X ( mjtNum , qacc_warmstart , nv , 1 ) X ( mjtNum , plugin_state , npluginstate , 1 ) X ( mjtNum , ctrl , nu , 1 ) X ( mjtNum , qfrc_applied , nv , 1 ) X ( mjtNum , xfrc_applied , nbody , 6 ) X ( mjtByte , eq_active , neq , 1 ) X ( mjtNum , mocap_pos , nmocap , 3 ) X ( mjtNum , mocap_quat , nmocap , 4 ) X ( mjtNum , qacc , nv , 1 ) X ( mjtNum , act_dot , na , 1 ) X ( mjtNum , userdata , nuserdata , 1 ) X ( mjtNum , sensordata , nsensordata , 1 ) X ( int , tree_asleep , ntree , 1 ) X ( int , plugin , nplugin , 1 ) X ( uintptr_t , plugin_data , nplugin , 1 ) X ( mjtNum , xpos , nbody , 3 ) X ( mjtNum , xquat , nbody , 4 ) X ( mjtNum , xmat , nbody , 9 ) X ( mjtNum , xipos , nbody , 3 ) X ( mjtNum , ximat , nbody , 9 ) X ( mjtNum , xanchor , njnt , 3 ) X ( mjtNum , xaxis , njnt , 3 ) X ( mjtNum , geom_xpos , ngeom , 3 ) X ( mjtNum , geom_xmat , ngeom , 9 ) X ( mjtNum , site_xpos , nsite , 3 ) X ( mjtNum , site_xmat , nsite , 9 ) X ( mjtNum , cam_xpos , ncam , 3 ) X ( mjtNum , cam_xmat , ncam , 9 ) X ( mjtNum , light_xpos , nlight , 3 ) X ( mjtNum , light_xdir , nlight , 3 ) X ( mjtNum , subtree_com , nbody , 3 ) X ( mjtNum , cdof , nv , 6 ) X ( mjtNum , cinert , nbody , 10 ) X ( mjtNum , flexvert_xpos , nflexvert , 3 ) X ( mjtNum , flexelem_aabb , nflexelem , 6 ) X ( mjtNum , flexedge_J , nJfe , 1 ) X ( mjtNum , flexedge_length , nflexedge , 1 ) X ( mjtNum , flexvert_J , nJfv , 2 ) X ( mjtNum , flexvert_length , nflexvert , 2 ) X ( mjtNum , bvh_aabb_dyn , nbvhdynamic , 6 ) X ( int , ten_wrapadr , ntendon , 1 ) X ( int , ten_wrapnum , ntendon , 1 ) X ( mjtNum , ten_J , nJten , 1 ) X ( mjtNum , ten_length , ntendon , 1 ) X ( int , wrap_obj , nwrap , 2 ) X ( mjtNum , wrap_xpos , nwrap , 6 ) X ( mjtNum , actuator_length , nu , 1 ) X ( int , moment_rownnz , nu , 1 ) X ( int , moment_rowadr , nu , 1 ) X ( int , moment_colind , nJmom , 1 ) X ( mjtNum , actuator_moment , nJmom , 1 ) XNV ( mjtNum , crb , nbody , 10 ) XNV ( mjtNum , qM , nM , 1 ) XNV ( mjtNum , M , nC , 1 ) XNV ( mjtNum , qLD , nC , 1 ) X ( mjtNum , qLDiagInv , nv , 1 ) X ( mjtByte , bvh_active , nbvh , 1 ) X ( int , tree_awake , ntree , 1 ) X ( int , body_awake , nbody , 1 ) X ( int , body_awake_ind , nbody , 1 ) X ( int , parent_awake_ind , nbody , 1 ) X ( int , dof_awake_ind , nv , 1 ) X ( mjtNum , flexedge_velocity , nflexedge , 1 ) X ( mjtNum , ten_velocity , ntendon , 1 ) X ( mjtNum , actuator_velocity , nu , 1 ) X ( mjtNum , cvel , nbody , 6 ) X ( mjtNum , cdof_dot , nv , 6 ) X ( mjtNum , qfrc_bias , nv , 1 ) X ( mjtNum , qfrc_spring , nv , 1 ) X ( mjtNum , qfrc_damper , nv , 1 ) X ( mjtNum , qfrc_gravcomp , nv , 1 ) X ( mjtNum , qfrc_fluid , nv , 1 ) X ( mjtNum , qfrc_passive , nv , 1 ) X ( mjtNum , subtree_linvel , nbody , 3 ) X ( mjtNum , subtree_angmom , nbody , 3 ) XNV ( mjtNum , qH , nC , 1 ) X ( mjtNum , qHDiagInv , nv , 1 ) XNV ( mjtNum , qDeriv , nD , 1 ) XNV ( mjtNum , qLU , nD , 1 ) X ( mjtNum , actuator_force , nu , 1 ) X ( mjtNum , qfrc_actuator , nv , 1 ) X ( mjtNum , qfrc_smooth , nv , 1 ) X ( mjtNum , qacc_smooth , nv , 1 ) X ( mjtNum , qfrc_constraint , nv , 1 ) X ( mjtNum , qfrc_inverse , nv , 1 ) X ( mjtNum , cacc , nbody , 6 ) X ( mjtNum , cfrc_int , nbody , 6 ) X ( mjtNum , cfrc_ext , nbody , 6 )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 822
+    },
+    "MJ_D": {
+      "value": "( n ) n",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 919
+    },
+    "MJDATA_ARENA_POINTERS_CONTACT": {
+      "value": "X ( mjContact , contact , MJ_D ( ncon ) , 1 )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 922
+    },
+    "MJDATA_ARENA_POINTERS_SOLVER": {
+      "value": "X ( int , efc_type , MJ_D ( nefc ) , 1 ) X ( int , efc_id , MJ_D ( nefc ) , 1 ) XNV ( int , efc_J_rownnz , MJ_D ( nefc ) , 1 ) XNV ( int , efc_J_rowadr , MJ_D ( nefc ) , 1 ) XNV ( int , efc_J_rowsuper , MJ_D ( nefc ) , 1 ) XNV ( int , efc_J_colind , MJ_D ( nJ ) , 1 ) XNV ( mjtNum , efc_J , MJ_D ( nJ ) , 1 ) X ( mjtNum , efc_pos , MJ_D ( nefc ) , 1 ) X ( mjtNum , efc_margin , MJ_D ( nefc ) , 1 ) X ( mjtNum , efc_frictionloss , MJ_D ( nefc ) , 1 ) X ( mjtNum , efc_diagApprox , MJ_D ( nefc ) , 1 ) X ( mjtNum , efc_KBIP , MJ_D ( nefc ) , 4 ) X ( mjtNum , efc_D , MJ_D ( nefc ) , 1 ) X ( mjtNum , efc_R , MJ_D ( nefc ) , 1 ) X ( int , tendon_efcadr , MJ_M ( ntendon ) , 1 ) X ( mjtNum , efc_vel , MJ_D ( nefc ) , 1 ) X ( mjtNum , efc_aref , MJ_D ( nefc ) , 1 ) X ( mjtNum , efc_b , MJ_D ( nefc ) , 1 ) X ( int , efc_state , MJ_D ( nefc ) , 1 ) X ( mjtNum , efc_force , MJ_D ( nefc ) , 1 )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 926
+    },
+    "MJDATA_ARENA_POINTERS_DUAL": {
+      "value": "XNV ( int , efc_AR_rownnz , MJ_D ( nefc ) , 1 ) XNV ( int , efc_AR_rowadr , MJ_D ( nefc ) , 1 ) XNV ( int , efc_AR_colind , MJ_D ( nA ) , 1 ) XNV ( mjtNum , efc_AR , MJ_D ( nA ) , 1 )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 949
+    },
+    "MJDATA_ARENA_POINTERS_ISLAND": {
+      "value": "X ( int , tree_island , MJ_M ( ntree ) , 1 ) X ( int , island_ntree , MJ_D ( nisland ) , 1 ) X ( int , island_itreeadr , MJ_D ( nisland ) , 1 ) X ( int , map_itree2tree , MJ_M ( ntree ) , 1 ) X ( int , dof_island , MJ_M ( nv ) , 1 ) X ( int , island_nv , MJ_D ( nisland ) , 1 ) X ( int , island_idofadr , MJ_D ( nisland ) , 1 ) X ( int , island_dofadr , MJ_D ( nisland ) , 1 ) X ( int , map_dof2idof , MJ_M ( nv ) , 1 ) X ( int , map_idof2dof , MJ_M ( nv ) , 1 ) X ( mjtNum , ifrc_smooth , MJ_D ( nidof ) , 1 ) X ( mjtNum , iacc_smooth , MJ_D ( nidof ) , 1 ) XNV ( int , iM_rownnz , MJ_D ( nidof ) , 1 ) XNV ( int , iM_rowadr , MJ_D ( nidof ) , 1 ) XNV ( int , iM_colind , MJ_M ( nC ) , 1 ) XNV ( mjtNum , iM , MJ_M ( nC ) , 1 ) XNV ( mjtNum , iLD , MJ_M ( nC ) , 1 ) X ( mjtNum , iLDiagInv , MJ_D ( nidof ) , 1 ) X ( mjtNum , iacc , MJ_D ( nidof ) , 1 ) X ( int , efc_island , MJ_D ( nefc ) , 1 ) X ( int , island_ne , MJ_D ( nisland ) , 1 ) X ( int , island_nf , MJ_D ( nisland ) , 1 ) X ( int , island_nefc , MJ_D ( nisland ) , 1 ) X ( int , island_iefcadr , MJ_D ( nisland ) , 1 ) X ( int , map_efc2iefc , MJ_D ( nefc ) , 1 ) X ( int , map_iefc2efc , MJ_D ( nefc ) , 1 ) X ( int , iefc_type , MJ_D ( nefc ) , 1 ) X ( int , iefc_id , MJ_D ( nefc ) , 1 ) XNV ( int , iefc_J_rownnz , MJ_D ( nefc ) , 1 ) XNV ( int , iefc_J_rowadr , MJ_D ( nefc ) , 1 ) XNV ( int , iefc_J_rowsuper , MJ_D ( nefc ) , 1 ) XNV ( int , iefc_J_colind , MJ_D ( nJ ) , 1 ) XNV ( mjtNum , iefc_J , MJ_D ( nJ ) , 1 ) X ( mjtNum , iefc_frictionloss , MJ_D ( nefc ) , 1 ) X ( mjtNum , iefc_D , MJ_D ( nefc ) , 1 ) X ( mjtNum , iefc_R , MJ_D ( nefc ) , 1 ) X ( mjtNum , iefc_aref , MJ_D ( nefc ) , 1 ) X ( int , iefc_state , MJ_D ( nefc ) , 1 ) X ( mjtNum , iefc_force , MJ_D ( nefc ) , 1 ) X ( mjtNum , ifrc_constraint , MJ_D ( nidof ) , 1 )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 956
+    },
+    "MJDATA_ARENA_POINTERS": {
+      "value": "MJDATA_ARENA_POINTERS_CONTACT MJDATA_ARENA_POINTERS_SOLVER MJDATA_ARENA_POINTERS_DUAL MJDATA_ARENA_POINTERS_ISLAND",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 999
+    },
+    "MJDATA_SCALAR": {
+      "value": "X ( size_t , narena ) X ( size_t , nbuffer ) X ( int , nplugin ) X ( size_t , pstack ) X ( size_t , pbase ) X ( size_t , parena ) X ( size_t , maxuse_stack ) X ( size_t , maxuse_arena ) X ( int , maxuse_con ) X ( int , maxuse_efc ) X ( int , ncon ) X ( int , ne ) X ( int , nf ) X ( int , nl ) X ( int , nefc ) X ( int , nJ ) X ( int , nA ) X ( int , nisland ) X ( int , nidof ) X ( int , ntree_awake ) X ( int , nbody_awake ) X ( int , nparent_awake ) X ( int , nv_awake ) X ( mjtByte , flg_energypos ) X ( mjtByte , flg_energyvel ) X ( mjtByte , flg_subtreevel ) X ( mjtByte , flg_rnepost ) X ( mjtNum , time ) X ( uintptr_t , threadpool )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 1007
+    },
+    "MJDATA_VECTOR": {
+      "value": "X ( size_t , maxuse_threadstack , mjMAXTHREAD , 1 ) X ( mjSolverStat , solver , mjNISLAND , mjNSOLVER ) X ( int , solver_niter , mjNISLAND , 1 ) X ( int , solver_nnz , mjNISLAND , 1 ) X ( mjtNum , solver_fwdinv , 2 , 1 ) X ( mjWarningStat , warning , mjNWARNING , 1 ) X ( mjTimerStat , timer , mjNTIMER , 1 ) X ( mjtNum , energy , 2 , 1 )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 1040
+    },
+    "XNV": {
+      "value": "X",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjxmacro.h",
+      "line": 1053
+    },
+    "mjVERSION_HEADER": {
+      "value": "3008001",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 19
+    },
+    "mjMAXTHREAD": {
+      "value": "128",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjthread.h",
+      "line": 18
+    },
+    "MUJOCO_HELPER_DLL_IMPORT": {
+      "value": "__declspec ( dllimport )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjexport.h",
+      "line": 19
+    },
+    "MUJOCO_HELPER_DLL_EXPORT": {
+      "value": "__declspec ( dllexport )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjexport.h",
+      "line": 20
+    },
+    "mjMAX": {
+      "value": "( a , b ) ( ( ( a ) > ( b ) ) ? ( a ) : ( b ) )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmacro.h",
+      "line": 19
+    },
+    "mjMIN": {
+      "value": "( a , b ) ( ( ( a ) < ( b ) ) ? ( a ) : ( b ) )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmacro.h",
+      "line": 20
+    },
+    "mjDISABLED": {
+      "value": "( x ) ( m -> opt . disableflags & ( x ) )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmacro.h",
+      "line": 23
+    },
+    "mjENABLED": {
+      "value": "( x ) ( m -> opt . enableflags & ( x ) )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmacro.h",
+      "line": 24
+    },
+    "mjACTUATORDISABLED": {
+      "value": "( i ) ( m -> opt . disableactuator & ( 1 << m -> actuator_group [ i ] ) )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmacro.h",
+      "line": 27
+    },
+    "mjPRINTFLIKE": {
+      "value": "( n , m )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjmacro.h",
+      "line": 34
+    },
+    "mjNGROUP": {
+      "value": "6",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjvisualize.h",
+      "line": 23
+    },
+    "mjMAXLIGHT": {
+      "value": "100",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjvisualize.h",
+      "line": 24
+    },
+    "mjMAXOVERLAY": {
+      "value": "500",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjvisualize.h",
+      "line": 25
+    },
+    "mjMAXLINE": {
+      "value": "100",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjvisualize.h",
+      "line": 26
+    },
+    "mjMAXLINEPNT": {
+      "value": "1001",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjvisualize.h",
+      "line": 27
+    },
+    "mjMAXPLANEGRID": {
+      "value": "200",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjvisualize.h",
+      "line": 28
+    },
+    "mjPLUGIN_LIB_INIT": {
+      "value": "( n ) static void _mj_init_ ## n ( void ) __attribute__ ( ( constructor ) ) ; static void _mj_init_ ## n ( void )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjplugin.h",
+      "line": 189
+    },
+    "mjNAUX": {
+      "value": "10",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjrender.h",
+      "line": 24
+    },
+    "mjMAXTEXTURE": {
+      "value": "1000",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjrender.h",
+      "line": 25
+    },
+    "mjMAXMATERIAL": {
+      "value": "1000",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjrender.h",
+      "line": 26
+    },
+    "ASAN_POISON_MEMORY_REGION": {
+      "value": "( addr , size )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjsan.h",
+      "line": 33
+    },
+    "ASAN_UNPOISON_MEMORY_REGION": {
+      "value": "( addr , size )",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjsan.h",
+      "line": 34
+    },
+    "mjMAXUISECT": {
+      "value": "10",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 20
+    },
+    "mjMAXUIITEM": {
+      "value": "200",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 21
+    },
+    "mjMAXUITEXT": {
+      "value": "300",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 22
+    },
+    "mjMAXUINAME": {
+      "value": "40",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 23
+    },
+    "mjMAXUIMULTI": {
+      "value": "35",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 24
+    },
+    "mjMAXUIEDIT": {
+      "value": "7",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 25
+    },
+    "mjMAXUIRECT": {
+      "value": "25",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 26
+    },
+    "mjSEPCLOSED": {
+      "value": "1000",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 28
+    },
+    "mjPRESERVE": {
+      "value": "2000",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 29
+    },
+    "mjKEY_ESCAPE": {
+      "value": "256",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 33
+    },
+    "mjKEY_ENTER": {
+      "value": "257",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 34
+    },
+    "mjKEY_TAB": {
+      "value": "258",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 35
+    },
+    "mjKEY_BACKSPACE": {
+      "value": "259",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 36
+    },
+    "mjKEY_INSERT": {
+      "value": "260",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 37
+    },
+    "mjKEY_DELETE": {
+      "value": "261",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 38
+    },
+    "mjKEY_RIGHT": {
+      "value": "262",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 39
+    },
+    "mjKEY_LEFT": {
+      "value": "263",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 40
+    },
+    "mjKEY_DOWN": {
+      "value": "264",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 41
+    },
+    "mjKEY_UP": {
+      "value": "265",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 42
+    },
+    "mjKEY_PAGE_UP": {
+      "value": "266",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 43
+    },
+    "mjKEY_PAGE_DOWN": {
+      "value": "267",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 44
+    },
+    "mjKEY_HOME": {
+      "value": "268",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 45
+    },
+    "mjKEY_END": {
+      "value": "269",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 46
+    },
+    "mjKEY_F1": {
+      "value": "290",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 47
+    },
+    "mjKEY_F2": {
+      "value": "291",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 48
+    },
+    "mjKEY_F3": {
+      "value": "292",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 49
+    },
+    "mjKEY_F4": {
+      "value": "293",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 50
+    },
+    "mjKEY_F5": {
+      "value": "294",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 51
+    },
+    "mjKEY_F6": {
+      "value": "295",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 52
+    },
+    "mjKEY_F7": {
+      "value": "296",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 53
+    },
+    "mjKEY_F8": {
+      "value": "297",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 54
+    },
+    "mjKEY_F9": {
+      "value": "298",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 55
+    },
+    "mjKEY_F10": {
+      "value": "299",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 56
+    },
+    "mjKEY_F11": {
+      "value": "300",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 57
+    },
+    "mjKEY_F12": {
+      "value": "301",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 58
+    },
+    "mjKEY_NUMPAD_0": {
+      "value": "320",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 59
+    },
+    "mjKEY_NUMPAD_9": {
+      "value": "329",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mjui.h",
+      "line": 60
+    },
+    "mju_sqrt": {
+      "value": "sqrt",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1015
+    },
+    "mju_exp": {
+      "value": "exp",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1016
+    },
+    "mju_sin": {
+      "value": "sin",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1017
+    },
+    "mju_cos": {
+      "value": "cos",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1018
+    },
+    "mju_tan": {
+      "value": "tan",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1019
+    },
+    "mju_asin": {
+      "value": "asin",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1020
+    },
+    "mju_acos": {
+      "value": "acos",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1021
+    },
+    "mju_atan2": {
+      "value": "atan2",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1022
+    },
+    "mju_tanh": {
+      "value": "tanh",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1023
+    },
+    "mju_pow": {
+      "value": "pow",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1024
+    },
+    "mju_abs": {
+      "value": "fabs",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1025
+    },
+    "mju_log": {
+      "value": "log",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1026
+    },
+    "mju_log10": {
+      "value": "log10",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1027
+    },
+    "mju_floor": {
+      "value": "floor",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1028
+    },
+    "mju_ceil": {
+      "value": "ceil",
+      "doc": "",
+      "file": "third_party/install/mujoco/include/mujoco/mujoco.h",
+      "line": 1029
+    }
+  },
+  "hand_enums": {
+    "EMjActuatorType": {
+      "name": "EMjActuatorType",
+      "members": {
+        "Motor": 0,
+        "Position": 1,
+        "Velocity": 2,
+        "IntVelocity": 3,
+        "Damper": 4,
+        "Cylinder": 5,
+        "Muscle": 6,
+        "Adhesion": 7,
+        "DcMotor": 8
+      },
+      "member_docs": {},
+      "underlying_type": "uint8",
+      "doc": "",
+      "file": "source/urlab/public/mujoco/components/actuators/mjactuator.h",
+      "line": 42
+    },
+    "EMjDcMotorInput": {
+      "name": "EMjDcMotorInput",
+      "members": {
+        "Voltage": 0,
+        "Position": 1,
+        "Velocity": 2
+      },
+      "member_docs": {},
+      "underlying_type": "uint8",
+      "doc": "",
+      "file": "source/urlab/public/mujoco/components/actuators/mjdcmotoractuator.h",
+      "line": 35
+    },
+    "EMjBodySleepPolicy": {
+      "name": "EMjBodySleepPolicy",
+      "members": {
+        "Auto": 0,
+        "Never": 3,
+        "Allowed": 4,
+        "InitAsleep": 5
+      },
+      "member_docs": {},
+      "underlying_type": "uint8",
+      "doc": "",
+      "file": "source/urlab/public/mujoco/components/bodies/mjbody.h",
+      "line": 47
+    },
+    "EMjEqualityType": {
+      "name": "EMjEqualityType",
+      "members": {
+        "Connect": 0,
+        "Weld": 1,
+        "Joint": 2,
+        "Tendon": 3,
+        "Flex": 4,
+        "FlexVert": 5,
+        "FlexStrain": 6
+      },
+      "member_docs": {},
+      "underlying_type": "uint8",
+      "doc": "",
+      "file": "source/urlab/public/mujoco/components/constraints/mjequality.h",
+      "line": 36
+    },
+    "EMjFlexcompType": {
+      "name": "EMjFlexcompType",
+      "members": {
+        "Grid": 0,
+        "Box": 1,
+        "Cylinder": 2,
+        "Ellipsoid": 3,
+        "Square": 4,
+        "Disc": 5,
+        "Circle": 6,
+        "Mesh": 7,
+        "Gmsh": 8,
+        "Direct": 9
+      },
+      "member_docs": {},
+      "underlying_type": "uint8",
+      "doc": "",
+      "file": "source/urlab/public/mujoco/components/deformable/mjflexcomp.h",
+      "line": 36
+    },
+    "EMjGeomType": {
+      "name": "EMjGeomType",
+      "members": {
+        "Plane": 0,
+        "Hfield": 1,
+        "Sphere": 2,
+        "Capsule": 3,
+        "Ellipsoid": 4,
+        "Cylinder": 5,
+        "Box": 6,
+        "Mesh": 7,
+        "SDF": 8
+      },
+      "member_docs": {},
+      "underlying_type": "uint8",
+      "doc": "",
+      "file": "source/urlab/public/mujoco/components/geometry/mjgeom.h",
+      "line": 39
+    },
+    "EMjSiteType": {
+      "name": "EMjSiteType",
+      "members": {
+        "Sphere": 0,
+        "Capsule": 1,
+        "Ellipsoid": 2,
+        "Cylinder": 3,
+        "Box": 4,
+        "Mesh": 5,
+        "Hfield": 6
+      },
+      "member_docs": {},
+      "underlying_type": "uint8",
+      "doc": "",
+      "file": "source/urlab/public/mujoco/components/geometry/mjsite.h",
+      "line": 42
+    },
+    "EMjJointType": {
+      "name": "EMjJointType",
+      "members": {
+        "Free": 0,
+        "Ball": 1,
+        "Slide": 2,
+        "Hinge": 3
+      },
+      "member_docs": {},
+      "underlying_type": "uint8",
+      "doc": "",
+      "file": "source/urlab/public/mujoco/components/joints/mjjoint.h",
+      "line": 39
+    },
+    "EMjCameraMode": {
+      "name": "EMjCameraMode",
+      "members": {
+        "Real": 0,
+        "Depth": 1,
+        "SemanticSegmentation": 2,
+        "InstanceSegmentation": 3
+      },
+      "member_docs": {},
+      "underlying_type": "uint8",
+      "doc": "",
+      "file": "source/urlab/public/mujoco/components/sensors/mjcameratypes.h",
+      "line": 35
+    },
+    "EMjSensorType": {
+      "name": "EMjSensorType",
+      "members": {
+        "Touch": 0,
+        "Accelerometer": 1,
+        "Velocimeter": 2,
+        "Gyro": 3,
+        "Force": 4,
+        "Torque": 5,
+        "Magnetometer": 6,
+        "RangeFinder": 7,
+        "CamProjection": 8,
+        "JointPos": 9,
+        "JointVel": 10,
+        "TendonPos": 11,
+        "TendonVel": 12,
+        "ActuatorPos": 13,
+        "ActuatorVel": 14,
+        "ActuatorFrc": 15,
+        "JointActFrc": 16,
+        "TendonActFrc": 17,
+        "BallQuat": 18,
+        "BallAngVel": 19,
+        "JointLimitPos": 20,
+        "JointLimitVel": 21,
+        "JointLimitFrc": 22,
+        "TendonLimitPos": 23,
+        "TendonLimitVel": 24,
+        "TendonLimitFrc": 25,
+        "FramePos": 26,
+        "FrameQuat": 27,
+        "FrameXAxis": 28,
+        "FrameYAxis": 29,
+        "FrameZAxis": 30,
+        "FrameLinVel": 31,
+        "FrameAngVel": 32,
+        "FrameLinAcc": 33,
+        "FrameAngAcc": 34,
+        "SubtreeCom": 35,
+        "SubtreeLinVel": 36,
+        "SubtreeAngMom": 37,
+        "InsideSite": 38,
+        "GeomDist": 39,
+        "GeomNormal": 40,
+        "GeomFromTo": 41,
+        "Contact": 42,
+        "EPotential": 43,
+        "EKinetic": 44,
+        "Clock": 45,
+        "Tactile": 46,
+        "Plugin": 47,
+        "User": 48
+      },
+      "member_docs": {},
+      "underlying_type": "uint8",
+      "doc": "",
+      "file": "source/urlab/public/mujoco/components/sensors/mjsensor.h",
+      "line": 42
+    },
+    "EMjTendonWrapType": {
+      "name": "EMjTendonWrapType",
+      "members": {
+        "Joint": 0,
+        "Site": 1,
+        "Geom": 2,
+        "Pulley": 3
+      },
+      "member_docs": {},
+      "underlying_type": "uint8",
+      "doc": "",
+      "file": "source/urlab/public/mujoco/components/tendons/mjtendon.h",
+      "line": 40
+    },
+    "EMjDebugShaderMode": {
+      "name": "EMjDebugShaderMode",
+      "members": {
+        "Off": 0,
+        "Island": 1,
+        "InstanceSegmentation": 2,
+        "SemanticSegmentation": 3
+      },
+      "member_docs": {},
+      "underlying_type": "uint8",
+      "doc": "",
+      "file": "source/urlab/public/mujoco/core/mjdebugvisualizer.h",
+      "line": 39
+    },
+    "EMjGainType": {
+      "name": "EMjGainType",
+      "members": {
+        "Fixed": 0,
+        "Affine": 1,
+        "Muscle": 2,
+        "DcMotor": 3,
+        "User": 4
+      },
+      "member_docs": {},
+      "underlying_type": "uint8",
+      "doc": "",
+      "file": "source/urlab/public/mujoco/generated/mjarticulationenums.h",
+      "line": 33
+    },
+    "EMjBiasType": {
+      "name": "EMjBiasType",
+      "members": {
+        "None": 0,
+        "Affine": 1,
+        "Muscle": 2,
+        "DcMotor": 3,
+        "User": 4
+      },
+      "member_docs": {},
+      "underlying_type": "uint8",
+      "doc": "",
+      "file": "source/urlab/public/mujoco/generated/mjarticulationenums.h",
+      "line": 44
+    },
+    "EMjDynType": {
+      "name": "EMjDynType",
+      "members": {
+        "None": 0,
+        "Integrator": 1,
+        "Filter": 2,
+        "FilterExact": 3,
+        "Muscle": 4,
+        "DcMotor": 5,
+        "User": 6
+      },
+      "member_docs": {},
+      "underlying_type": "uint8",
+      "doc": "",
+      "file": "source/urlab/public/mujoco/generated/mjarticulationenums.h",
+      "line": 55
+    },
+    "EMjGeomInertia": {
+      "name": "EMjGeomInertia",
+      "members": {
+        "Volume": 0,
+        "Shell": 1
+      },
+      "member_docs": {},
+      "underlying_type": "uint8",
+      "doc": "",
+      "file": "source/urlab/public/mujoco/generated/mjarticulationenums.h",
+      "line": 68
+    },
+    "EMjFluidShape": {
+      "name": "EMjFluidShape",
+      "members": {
+        "None": 0,
+        "Ellipsoid": 1
+      },
+      "member_docs": {},
+      "underlying_type": "uint8",
+      "doc": "",
+      "file": "source/urlab/public/mujoco/generated/mjarticulationenums.h",
+      "line": 76
+    },
+    "EMjCameraTrackingMode": {
+      "name": "EMjCameraTrackingMode",
+      "members": {
+        "Fixed": 0,
+        "Track": 1,
+        "TrackCom": 2,
+        "TargetBody": 3,
+        "TargetBodyCom": 4
+      },
+      "member_docs": {},
+      "underlying_type": "uint8",
+      "doc": "",
+      "file": "source/urlab/public/mujoco/generated/mjarticulationenums.h",
+      "line": 84
+    },
+    "EMjCameraProjection": {
+      "name": "EMjCameraProjection",
+      "members": {
+        "Perspective": 0,
+        "Orthographic": 1
+      },
+      "member_docs": {},
+      "underlying_type": "uint8",
+      "doc": "",
+      "file": "source/urlab/public/mujoco/generated/mjarticulationenums.h",
+      "line": 95
+    },
+    "EMjObjType": {
+      "name": "EMjObjType",
+      "members": {
+        "Unknown": 0,
+        "Body": 1,
+        "XBody": 2,
+        "Joint": 3,
+        "DoF": 4,
+        "Geom": 5,
+        "Site": 6,
+        "Camera": 7,
+        "Light": 8,
+        "Flex": 9,
+        "Mesh": 10,
+        "Skin": 11,
+        "HField": 12,
+        "Texture": 13,
+        "Material": 14,
+        "Pair": 15,
+        "Exclude": 16,
+        "Equality": 17,
+        "Tendon": 18,
+        "Actuator": 19,
+        "Sensor": 20,
+        "Numeric": 21,
+        "Text": 22,
+        "Tuple": 23,
+        "Key": 24,
+        "Plugin": 25,
+        "Frame": 100
+      },
+      "member_docs": {},
+      "underlying_type": "uint8",
+      "doc": "",
+      "file": "source/urlab/public/mujoco/generated/mjarticulationenums.h",
+      "line": 103
+    },
+    "EMjActuatorTrnType": {
+      "name": "EMjActuatorTrnType",
+      "members": {
+        "Joint": 0,
+        "JointInParent": 1,
+        "SliderCrank": 2,
+        "Tendon": 3,
+        "Site": 4,
+        "Body": 5,
+        "Undefined": 6
+      },
+      "member_docs": {},
+      "underlying_type": "uint8",
+      "doc": "",
+      "file": "source/urlab/public/mujoco/generated/mjarticulationenums.h",
+      "line": 136
+    },
+    "EMjFlexcompDof": {
+      "name": "EMjFlexcompDof",
+      "members": {
+        "Full": 0,
+        "Radial": 1,
+        "Trilinear": 2,
+        "Quadratic": 3,
+        "TwoD": 4
+      },
+      "member_docs": {},
+      "underlying_type": "uint8",
+      "doc": "",
+      "file": "source/urlab/public/mujoco/generated/mjarticulationenums.h",
+      "line": 149
+    },
+    "EMjIntegrator": {
+      "name": "EMjIntegrator",
+      "members": {
+        "Euler": 0,
+        "RK4": 1,
+        "Implicit": 2,
+        "ImplicitFast": 3
+      },
+      "member_docs": {},
+      "underlying_type": "uint8",
+      "doc": "",
+      "file": "source/urlab/public/mujoco/generated/mjoptionenums.h",
+      "line": 33
+    },
+    "EMjCone": {
+      "name": "EMjCone",
+      "members": {
+        "Pyramidal": 0,
+        "Elliptic": 1
+      },
+      "member_docs": {},
+      "underlying_type": "uint8",
+      "doc": "",
+      "file": "source/urlab/public/mujoco/generated/mjoptionenums.h",
+      "line": 43
+    },
+    "EMjSolver": {
+      "name": "EMjSolver",
+      "members": {
+        "PGS": 0,
+        "CG": 1,
+        "Newton": 2
+      },
+      "member_docs": {},
+      "underlying_type": "uint8",
+      "doc": "",
+      "file": "source/urlab/public/mujoco/generated/mjoptionenums.h",
+      "line": 51
+    },
+    "EMjPropertyType": {
+      "name": "EMjPropertyType",
+      "members": {
+        "Slider": 0,
+        "Toggle": 1,
+        "LabelOnly": 2,
+        "Header": 3
+      },
+      "member_docs": {},
+      "underlying_type": "uint8",
+      "doc": "",
+      "file": "source/urlab/public/ui/mjpropertyrow.h",
+      "line": 38
+    }
+  }
+}
\ No newline at end of file
diff --git a/Scripts/mjcf_schema_snapshot.json b/Scripts/codegen/snapshots/mjcf_schema_snapshot.json
similarity index 56%
rename from Scripts/mjcf_schema_snapshot.json
rename to Scripts/codegen/snapshots/mjcf_schema_snapshot.json
index 05a5798..ef17273 100644
--- a/Scripts/mjcf_schema_snapshot.json
+++ b/Scripts/codegen/snapshots/mjcf_schema_snapshot.json
@@ -1,7 +1,7 @@
 {
   "_meta": {
     "mujoco_version": "3.8.1",
-    "snapshot_date": "2026-05-15",
+    "snapshot_date": "2026-05-28",
     "source": "https://github.com/google-deepmind/mujoco/blob/main/src/xml/xml_native_reader.cc",
     "description": "MJCF schema snapshot, auto-extracted by Scripts/codegen/build_mjcf_schema_snapshot.py from the MuJoCo submodule's xml_native_reader.cc."
   },
@@ -351,6 +351,400 @@
       "user"
     ]
   },
+  "sensor_per_type": {
+    "touch": {
+      "mj_type": "mjSENS_TOUCH",
+      "objtype": "mjOBJ_SITE",
+      "reftype": null,
+      "obj_attr": "site",
+      "ref_attr": null,
+      "computed": false
+    },
+    "accelerometer": {
+      "mj_type": "mjSENS_ACCELEROMETER",
+      "objtype": "mjOBJ_SITE",
+      "reftype": null,
+      "obj_attr": "site",
+      "ref_attr": null,
+      "computed": false
+    },
+    "velocimeter": {
+      "mj_type": "mjSENS_VELOCIMETER",
+      "objtype": "mjOBJ_SITE",
+      "reftype": null,
+      "obj_attr": "site",
+      "ref_attr": null,
+      "computed": false
+    },
+    "gyro": {
+      "mj_type": "mjSENS_GYRO",
+      "objtype": "mjOBJ_SITE",
+      "reftype": null,
+      "obj_attr": "site",
+      "ref_attr": null,
+      "computed": false
+    },
+    "force": {
+      "mj_type": "mjSENS_FORCE",
+      "objtype": "mjOBJ_SITE",
+      "reftype": null,
+      "obj_attr": "site",
+      "ref_attr": null,
+      "computed": false
+    },
+    "torque": {
+      "mj_type": "mjSENS_TORQUE",
+      "objtype": "mjOBJ_SITE",
+      "reftype": null,
+      "obj_attr": "site",
+      "ref_attr": null,
+      "computed": false
+    },
+    "magnetometer": {
+      "mj_type": "mjSENS_MAGNETOMETER",
+      "objtype": "mjOBJ_SITE",
+      "reftype": null,
+      "obj_attr": "site",
+      "ref_attr": null,
+      "computed": false
+    },
+    "camprojection": {
+      "mj_type": "mjSENS_CAMPROJECTION",
+      "objtype": "mjOBJ_SITE",
+      "reftype": "mjOBJ_CAMERA",
+      "obj_attr": "site",
+      "ref_attr": "camera",
+      "computed": false
+    },
+    "rangefinder": {
+      "mj_type": "mjSENS_RANGEFINDER",
+      "objtype": "computed",
+      "reftype": "computed",
+      "obj_attr": "site",
+      "ref_attr": null,
+      "computed": true
+    },
+    "jointpos": {
+      "mj_type": "mjSENS_JOINTPOS",
+      "objtype": "mjOBJ_JOINT",
+      "reftype": null,
+      "obj_attr": "joint",
+      "ref_attr": null,
+      "computed": false
+    },
+    "jointvel": {
+      "mj_type": "mjSENS_JOINTVEL",
+      "objtype": "mjOBJ_JOINT",
+      "reftype": null,
+      "obj_attr": "joint",
+      "ref_attr": null,
+      "computed": false
+    },
+    "tendonpos": {
+      "mj_type": "mjSENS_TENDONPOS",
+      "objtype": "mjOBJ_TENDON",
+      "reftype": null,
+      "obj_attr": "tendon",
+      "ref_attr": null,
+      "computed": false
+    },
+    "tendonvel": {
+      "mj_type": "mjSENS_TENDONVEL",
+      "objtype": "mjOBJ_TENDON",
+      "reftype": null,
+      "obj_attr": "tendon",
+      "ref_attr": null,
+      "computed": false
+    },
+    "actuatorpos": {
+      "mj_type": "mjSENS_ACTUATORPOS",
+      "objtype": "mjOBJ_ACTUATOR",
+      "reftype": null,
+      "obj_attr": "actuator",
+      "ref_attr": null,
+      "computed": false
+    },
+    "actuatorvel": {
+      "mj_type": "mjSENS_ACTUATORVEL",
+      "objtype": "mjOBJ_ACTUATOR",
+      "reftype": null,
+      "obj_attr": "actuator",
+      "ref_attr": null,
+      "computed": false
+    },
+    "actuatorfrc": {
+      "mj_type": "mjSENS_ACTUATORFRC",
+      "objtype": "mjOBJ_ACTUATOR",
+      "reftype": null,
+      "obj_attr": "actuator",
+      "ref_attr": null,
+      "computed": false
+    },
+    "jointactuatorfrc": {
+      "mj_type": "mjSENS_JOINTACTFRC",
+      "objtype": "mjOBJ_JOINT",
+      "reftype": null,
+      "obj_attr": "joint",
+      "ref_attr": null,
+      "computed": false
+    },
+    "tendonactuatorfrc": {
+      "mj_type": "mjSENS_TENDONACTFRC",
+      "objtype": "mjOBJ_TENDON",
+      "reftype": null,
+      "obj_attr": "tendon",
+      "ref_attr": null,
+      "computed": false
+    },
+    "ballquat": {
+      "mj_type": "mjSENS_BALLQUAT",
+      "objtype": "mjOBJ_JOINT",
+      "reftype": null,
+      "obj_attr": "joint",
+      "ref_attr": null,
+      "computed": false
+    },
+    "ballangvel": {
+      "mj_type": "mjSENS_BALLANGVEL",
+      "objtype": "mjOBJ_JOINT",
+      "reftype": null,
+      "obj_attr": "joint",
+      "ref_attr": null,
+      "computed": false
+    },
+    "jointlimitpos": {
+      "mj_type": "mjSENS_JOINTLIMITPOS",
+      "objtype": "mjOBJ_JOINT",
+      "reftype": null,
+      "obj_attr": "joint",
+      "ref_attr": null,
+      "computed": false
+    },
+    "jointlimitvel": {
+      "mj_type": "mjSENS_JOINTLIMITVEL",
+      "objtype": "mjOBJ_JOINT",
+      "reftype": null,
+      "obj_attr": "joint",
+      "ref_attr": null,
+      "computed": false
+    },
+    "jointlimitfrc": {
+      "mj_type": "mjSENS_JOINTLIMITFRC",
+      "objtype": "mjOBJ_JOINT",
+      "reftype": null,
+      "obj_attr": "joint",
+      "ref_attr": null,
+      "computed": false
+    },
+    "tendonlimitpos": {
+      "mj_type": "mjSENS_TENDONLIMITPOS",
+      "objtype": "mjOBJ_TENDON",
+      "reftype": null,
+      "obj_attr": "tendon",
+      "ref_attr": null,
+      "computed": false
+    },
+    "tendonlimitvel": {
+      "mj_type": "mjSENS_TENDONLIMITVEL",
+      "objtype": "mjOBJ_TENDON",
+      "reftype": null,
+      "obj_attr": "tendon",
+      "ref_attr": null,
+      "computed": false
+    },
+    "tendonlimitfrc": {
+      "mj_type": "mjSENS_TENDONLIMITFRC",
+      "objtype": "mjOBJ_TENDON",
+      "reftype": null,
+      "obj_attr": "tendon",
+      "ref_attr": null,
+      "computed": false
+    },
+    "framepos": {
+      "mj_type": "mjSENS_FRAMEPOS",
+      "objtype": "from_xml",
+      "reftype": null,
+      "obj_attr": "objname",
+      "ref_attr": "refname",
+      "computed": false
+    },
+    "framequat": {
+      "mj_type": "mjSENS_FRAMEQUAT",
+      "objtype": "from_xml",
+      "reftype": null,
+      "obj_attr": "objname",
+      "ref_attr": "refname",
+      "computed": false
+    },
+    "framexaxis": {
+      "mj_type": "mjSENS_FRAMEXAXIS",
+      "objtype": "from_xml",
+      "reftype": null,
+      "obj_attr": "objname",
+      "ref_attr": "refname",
+      "computed": false
+    },
+    "frameyaxis": {
+      "mj_type": "mjSENS_FRAMEYAXIS",
+      "objtype": "from_xml",
+      "reftype": null,
+      "obj_attr": "objname",
+      "ref_attr": "refname",
+      "computed": false
+    },
+    "framezaxis": {
+      "mj_type": "mjSENS_FRAMEZAXIS",
+      "objtype": "from_xml",
+      "reftype": null,
+      "obj_attr": "objname",
+      "ref_attr": "refname",
+      "computed": false
+    },
+    "framelinvel": {
+      "mj_type": "mjSENS_FRAMELINVEL",
+      "objtype": "from_xml",
+      "reftype": null,
+      "obj_attr": "objname",
+      "ref_attr": "refname",
+      "computed": false
+    },
+    "frameangvel": {
+      "mj_type": "mjSENS_FRAMEANGVEL",
+      "objtype": "from_xml",
+      "reftype": null,
+      "obj_attr": "objname",
+      "ref_attr": "refname",
+      "computed": false
+    },
+    "framelinacc": {
+      "mj_type": "mjSENS_FRAMELINACC",
+      "objtype": "from_xml",
+      "reftype": null,
+      "obj_attr": "objname",
+      "ref_attr": null,
+      "computed": false
+    },
+    "frameangacc": {
+      "mj_type": "mjSENS_FRAMEANGACC",
+      "objtype": "from_xml",
+      "reftype": null,
+      "obj_attr": "objname",
+      "ref_attr": null,
+      "computed": false
+    },
+    "subtreecom": {
+      "mj_type": "mjSENS_SUBTREECOM",
+      "objtype": "mjOBJ_BODY",
+      "reftype": null,
+      "obj_attr": "body",
+      "ref_attr": null,
+      "computed": false
+    },
+    "subtreelinvel": {
+      "mj_type": "mjSENS_SUBTREELINVEL",
+      "objtype": "mjOBJ_BODY",
+      "reftype": null,
+      "obj_attr": "body",
+      "ref_attr": null,
+      "computed": false
+    },
+    "subtreeangmom": {
+      "mj_type": "mjSENS_SUBTREEANGMOM",
+      "objtype": "mjOBJ_BODY",
+      "reftype": null,
+      "obj_attr": "body",
+      "ref_attr": null,
+      "computed": false
+    },
+    "insidesite": {
+      "mj_type": "mjSENS_INSIDESITE",
+      "objtype": "from_xml",
+      "reftype": "mjOBJ_SITE",
+      "obj_attr": "objname",
+      "ref_attr": "site",
+      "computed": false
+    },
+    "distance": {
+      "mj_type": "mjSENS_GEOMDIST",
+      "objtype": "computed",
+      "reftype": "computed",
+      "obj_attr": "body1",
+      "ref_attr": "body2",
+      "computed": true
+    },
+    "normal": {
+      "mj_type": "mjSENS_GEOMNORMAL",
+      "objtype": "computed",
+      "reftype": "computed",
+      "obj_attr": "body1",
+      "ref_attr": "body2",
+      "computed": true
+    },
+    "fromto": {
+      "mj_type": "mjSENS_GEOMFROMTO",
+      "objtype": "computed",
+      "reftype": "computed",
+      "obj_attr": "body1",
+      "ref_attr": "body2",
+      "computed": true
+    },
+    "contact": {
+      "mj_type": "mjSENS_CONTACT",
+      "objtype": "computed",
+      "reftype": "computed",
+      "obj_attr": "site",
+      "ref_attr": "body2",
+      "computed": true
+    },
+    "e_potential": {
+      "mj_type": "mjSENS_E_POTENTIAL",
+      "objtype": "mjOBJ_UNKNOWN",
+      "reftype": null,
+      "obj_attr": null,
+      "ref_attr": null,
+      "computed": false
+    },
+    "e_kinetic": {
+      "mj_type": "mjSENS_E_KINETIC",
+      "objtype": "mjOBJ_UNKNOWN",
+      "reftype": null,
+      "obj_attr": null,
+      "ref_attr": null,
+      "computed": false
+    },
+    "clock": {
+      "mj_type": "mjSENS_CLOCK",
+      "objtype": "mjOBJ_UNKNOWN",
+      "reftype": null,
+      "obj_attr": null,
+      "ref_attr": null,
+      "computed": false
+    },
+    "tactile": {
+      "mj_type": "mjSENS_TACTILE",
+      "objtype": "mjOBJ_MESH",
+      "reftype": "mjOBJ_GEOM",
+      "obj_attr": "mesh",
+      "ref_attr": "geom",
+      "computed": false
+    },
+    "user": {
+      "mj_type": "mjSENS_USER",
+      "objtype": "computed",
+      "reftype": "computed",
+      "obj_attr": "objname",
+      "ref_attr": null,
+      "computed": true
+    },
+    "plugin": {
+      "mj_type": "mjSENS_PLUGIN",
+      "objtype": "computed",
+      "reftype": "computed",
+      "obj_attr": "objname",
+      "ref_attr": "refname",
+      "computed": true
+    }
+  },
   "tendon": {
     "spatial": {
       "attrs": [
diff --git a/Scripts/mjxmacro_snapshot.json b/Scripts/codegen/snapshots/mjxmacro_snapshot.json
similarity index 69%
rename from Scripts/mjxmacro_snapshot.json
rename to Scripts/codegen/snapshots/mjxmacro_snapshot.json
index bdfe4c1..52f77d5 100644
--- a/Scripts/mjxmacro_snapshot.json
+++ b/Scripts/codegen/snapshots/mjxmacro_snapshot.json
@@ -1,7 +1,19 @@
 {
   "_meta": {
     "source": "third_party\\install\\MuJoCo\\include\\mujoco\\mjxmacro.h",
-    "schema_kind": "mjxmacro"
+    "schema_kind": "mjxmacro",
+    "struct_field_sources": {
+      "MJOPTION_FIELDS": "third_party\\install\\MuJoCo\\include\\mujoco\\mjxmacro.h",
+      "MJSTATISTIC_FIELDS": "third_party\\install\\MuJoCo\\include\\mujoco\\mjxmacro.h",
+      "MJVISUAL_GLOBAL_FIELDS": "third_party\\install\\MuJoCo\\include\\mujoco\\mjxmacro.h",
+      "MJVISUAL_QUALITY_FIELDS": "third_party\\install\\MuJoCo\\include\\mujoco\\mjxmacro.h",
+      "MJVISUAL_HEADLIGHT_FIELDS": "third_party\\install\\MuJoCo\\include\\mujoco\\mjxmacro.h",
+      "MJVISUAL_MAP_FIELDS": "third_party\\install\\MuJoCo\\include\\mujoco\\mjxmacro.h",
+      "MJVISUAL_SCALE_FIELDS": "third_party\\install\\MuJoCo\\include\\mujoco\\mjxmacro.h",
+      "MJVISUAL_RGBA_FIELDS": "third_party\\install\\MuJoCo\\include\\mujoco\\mjxmacro.h",
+      "MJSCOMPILER_FIELDS": "Scripts\\codegen\\_vendored\\mjspecmacro.h",
+      "MJSPEC_FIELDS": "Scripts\\codegen\\_vendored\\mjspecmacro.h"
+    }
   },
   "mjmodel_pointers": {
     "MJMODEL_POINTERS_BODY": [
@@ -552,6 +564,24 @@
         "name": "geom_gap",
         "outer_dim": "ngeom",
         "stride": "1"
+      },
+      {
+        "type": "mjtNum",
+        "name": "geom_fluid",
+        "outer_dim": "ngeom",
+        "stride": "mjNFLUID"
+      },
+      {
+        "type": "mjtNum",
+        "name": "geom_user",
+        "outer_dim": "ngeom",
+        "stride": "MJ_M(nuser_geom)"
+      },
+      {
+        "type": "float",
+        "name": "geom_rgba",
+        "outer_dim": "ngeom",
+        "stride": "4"
       }
     ],
     "MJMODEL_POINTERS_SITE": [
@@ -1396,6 +1426,126 @@
         "name": "mesh_graphadr",
         "outer_dim": "nmesh",
         "stride": "1"
+      },
+      {
+        "type": "float",
+        "name": "mesh_vert",
+        "outer_dim": "nmeshvert",
+        "stride": "3"
+      },
+      {
+        "type": "float",
+        "name": "mesh_normal",
+        "outer_dim": "nmeshnormal",
+        "stride": "3"
+      },
+      {
+        "type": "float",
+        "name": "mesh_texcoord",
+        "outer_dim": "nmeshtexcoord",
+        "stride": "2"
+      },
+      {
+        "type": "int",
+        "name": "mesh_face",
+        "outer_dim": "nmeshface",
+        "stride": "3"
+      },
+      {
+        "type": "int",
+        "name": "mesh_facenormal",
+        "outer_dim": "nmeshface",
+        "stride": "3"
+      },
+      {
+        "type": "int",
+        "name": "mesh_facetexcoord",
+        "outer_dim": "nmeshface",
+        "stride": "3"
+      },
+      {
+        "type": "int",
+        "name": "mesh_graph",
+        "outer_dim": "nmeshgraph",
+        "stride": "1"
+      },
+      {
+        "type": "mjtNum",
+        "name": "mesh_scale",
+        "outer_dim": "nmesh",
+        "stride": "3"
+      },
+      {
+        "type": "mjtNum",
+        "name": "mesh_pos",
+        "outer_dim": "nmesh",
+        "stride": "3"
+      },
+      {
+        "type": "mjtNum",
+        "name": "mesh_quat",
+        "outer_dim": "nmesh",
+        "stride": "4"
+      },
+      {
+        "type": "int",
+        "name": "mesh_pathadr",
+        "outer_dim": "nmesh",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "mesh_polynum",
+        "outer_dim": "nmesh",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "mesh_polyadr",
+        "outer_dim": "nmesh",
+        "stride": "1"
+      },
+      {
+        "type": "mjtNum",
+        "name": "mesh_polynormal",
+        "outer_dim": "nmeshpoly",
+        "stride": "3"
+      },
+      {
+        "type": "int",
+        "name": "mesh_polyvertadr",
+        "outer_dim": "nmeshpoly",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "mesh_polyvertnum",
+        "outer_dim": "nmeshpoly",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "mesh_polyvert",
+        "outer_dim": "nmeshpolyvert",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "mesh_polymapadr",
+        "outer_dim": "nmeshvert",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "mesh_polymapnum",
+        "outer_dim": "nmeshvert",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "mesh_polymap",
+        "outer_dim": "nmeshpolymap",
+        "stride": "1"
       }
     ],
     "MJMODEL_POINTERS_SKIN": [
@@ -1556,6 +1706,18 @@
         "name": "hfield_adr",
         "outer_dim": "nhfield",
         "stride": "1"
+      },
+      {
+        "type": "float",
+        "name": "hfield_data",
+        "outer_dim": "nhfielddata",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "hfield_pathadr",
+        "outer_dim": "nhfield",
+        "stride": "1"
       }
     ],
     "MJMODEL_POINTERS_TEXTURE": [
@@ -1594,6 +1756,18 @@
         "name": "tex_adr",
         "outer_dim": "ntex",
         "stride": "1"
+      },
+      {
+        "type": "mjtByte",
+        "name": "tex_data",
+        "outer_dim": "ntexdata",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "tex_pathadr",
+        "outer_dim": "ntex",
+        "stride": "1"
       }
     ],
     "MJMODEL_POINTERS_MATERIAL": [
@@ -2596,417 +2770,1574 @@
         "name": "actuator_moment",
         "outer_dim": "nJmom",
         "stride": "1"
-      }
-    ],
-    "MJDATA_ARENA_POINTERS_SOLVER": [
+      },
       {
-        "type": "int",
-        "name": "efc_type",
-        "outer_dim": "MJ_D(nefc)",
-        "stride": "1"
+        "type": "mjtNum",
+        "name": "crb",
+        "outer_dim": "nbody",
+        "stride": "10"
       },
       {
-        "type": "int",
-        "name": "efc_id",
-        "outer_dim": "MJ_D(nefc)",
+        "type": "mjtNum",
+        "name": "qM",
+        "outer_dim": "nM",
         "stride": "1"
-      }
-    ],
-    "MJDATA_ARENA_POINTERS_DUAL": [],
-    "MJDATA_ARENA_POINTERS_ISLAND": [
+      },
       {
-        "type": "int",
-        "name": "tree_island",
-        "outer_dim": "MJ_M(ntree)",
+        "type": "mjtNum",
+        "name": "M",
+        "outer_dim": "nC",
         "stride": "1"
       },
       {
-        "type": "int",
-        "name": "island_ntree",
-        "outer_dim": "MJ_D(nisland)",
+        "type": "mjtNum",
+        "name": "qLD",
+        "outer_dim": "nC",
         "stride": "1"
       },
       {
-        "type": "int",
-        "name": "island_itreeadr",
-        "outer_dim": "MJ_D(nisland)",
+        "type": "mjtNum",
+        "name": "qLDiagInv",
+        "outer_dim": "nv",
         "stride": "1"
       },
       {
-        "type": "int",
-        "name": "map_itree2tree",
-        "outer_dim": "MJ_M(ntree)",
+        "type": "mjtByte",
+        "name": "bvh_active",
+        "outer_dim": "nbvh",
         "stride": "1"
       },
       {
         "type": "int",
-        "name": "dof_island",
-        "outer_dim": "MJ_M(nv)",
+        "name": "tree_awake",
+        "outer_dim": "ntree",
         "stride": "1"
       },
       {
         "type": "int",
-        "name": "island_nv",
-        "outer_dim": "MJ_D(nisland)",
+        "name": "body_awake",
+        "outer_dim": "nbody",
         "stride": "1"
       },
       {
         "type": "int",
-        "name": "island_idofadr",
-        "outer_dim": "MJ_D(nisland)",
+        "name": "body_awake_ind",
+        "outer_dim": "nbody",
         "stride": "1"
       },
       {
         "type": "int",
-        "name": "island_dofadr",
-        "outer_dim": "MJ_D(nisland)",
+        "name": "parent_awake_ind",
+        "outer_dim": "nbody",
         "stride": "1"
       },
       {
         "type": "int",
-        "name": "map_dof2idof",
-        "outer_dim": "MJ_M(nv)",
+        "name": "dof_awake_ind",
+        "outer_dim": "nv",
         "stride": "1"
       },
       {
-        "type": "int",
-        "name": "map_idof2dof",
-        "outer_dim": "MJ_M(nv)",
+        "type": "mjtNum",
+        "name": "flexedge_velocity",
+        "outer_dim": "nflexedge",
         "stride": "1"
       },
       {
         "type": "mjtNum",
-        "name": "ifrc_smooth",
-        "outer_dim": "MJ_D(nidof)",
+        "name": "ten_velocity",
+        "outer_dim": "ntendon",
         "stride": "1"
       },
       {
         "type": "mjtNum",
-        "name": "iacc_smooth",
-        "outer_dim": "MJ_D(nidof)",
+        "name": "actuator_velocity",
+        "outer_dim": "nu",
         "stride": "1"
-      }
-    ],
-    "MJDATA_ARENA_POINTERS": []
-  },
-  "struct_fields": {
-    "MJOPTION_FIELDS": [
+      },
       {
         "type": "mjtNum",
-        "name": "timestep",
-        "count": "1",
-        "is_vec": false
+        "name": "cvel",
+        "outer_dim": "nbody",
+        "stride": "6"
       },
       {
         "type": "mjtNum",
-        "name": "impratio",
-        "count": "1",
-        "is_vec": false
+        "name": "cdof_dot",
+        "outer_dim": "nv",
+        "stride": "6"
       },
       {
         "type": "mjtNum",
-        "name": "tolerance",
-        "count": "1",
-        "is_vec": false
+        "name": "qfrc_bias",
+        "outer_dim": "nv",
+        "stride": "1"
       },
       {
         "type": "mjtNum",
-        "name": "ls_tolerance",
-        "count": "1",
-        "is_vec": false
+        "name": "qfrc_spring",
+        "outer_dim": "nv",
+        "stride": "1"
       },
       {
         "type": "mjtNum",
-        "name": "noslip_tolerance",
-        "count": "1",
-        "is_vec": false
+        "name": "qfrc_damper",
+        "outer_dim": "nv",
+        "stride": "1"
       },
       {
         "type": "mjtNum",
-        "name": "ccd_tolerance",
-        "count": "1",
-        "is_vec": false
+        "name": "qfrc_gravcomp",
+        "outer_dim": "nv",
+        "stride": "1"
       },
       {
         "type": "mjtNum",
-        "name": "sleep_tolerance",
-        "count": "1",
-        "is_vec": false
+        "name": "qfrc_fluid",
+        "outer_dim": "nv",
+        "stride": "1"
       },
       {
         "type": "mjtNum",
-        "name": "gravity",
-        "count": "3",
-        "is_vec": true
+        "name": "qfrc_passive",
+        "outer_dim": "nv",
+        "stride": "1"
       },
       {
         "type": "mjtNum",
-        "name": "wind",
-        "count": "3",
-        "is_vec": true
+        "name": "subtree_linvel",
+        "outer_dim": "nbody",
+        "stride": "3"
       },
       {
         "type": "mjtNum",
-        "name": "magnetic",
-        "count": "3",
-        "is_vec": true
+        "name": "subtree_angmom",
+        "outer_dim": "nbody",
+        "stride": "3"
       },
       {
         "type": "mjtNum",
-        "name": "density",
-        "count": "1",
-        "is_vec": false
+        "name": "qH",
+        "outer_dim": "nC",
+        "stride": "1"
       },
       {
         "type": "mjtNum",
-        "name": "viscosity",
-        "count": "1",
-        "is_vec": false
+        "name": "qHDiagInv",
+        "outer_dim": "nv",
+        "stride": "1"
       },
       {
         "type": "mjtNum",
-        "name": "o_margin",
-        "count": "1",
-        "is_vec": false
+        "name": "qDeriv",
+        "outer_dim": "nD",
+        "stride": "1"
       },
       {
         "type": "mjtNum",
-        "name": "o_solref",
-        "count": "mjNREF",
-        "is_vec": true
+        "name": "qLU",
+        "outer_dim": "nD",
+        "stride": "1"
       },
       {
         "type": "mjtNum",
-        "name": "o_solimp",
-        "count": "mjNIMP",
-        "is_vec": true
+        "name": "actuator_force",
+        "outer_dim": "nu",
+        "stride": "1"
+      },
+      {
+        "type": "mjtNum",
+        "name": "qfrc_actuator",
+        "outer_dim": "nv",
+        "stride": "1"
+      },
+      {
+        "type": "mjtNum",
+        "name": "qfrc_smooth",
+        "outer_dim": "nv",
+        "stride": "1"
+      },
+      {
+        "type": "mjtNum",
+        "name": "qacc_smooth",
+        "outer_dim": "nv",
+        "stride": "1"
+      },
+      {
+        "type": "mjtNum",
+        "name": "qfrc_constraint",
+        "outer_dim": "nv",
+        "stride": "1"
+      },
+      {
+        "type": "mjtNum",
+        "name": "qfrc_inverse",
+        "outer_dim": "nv",
+        "stride": "1"
+      },
+      {
+        "type": "mjtNum",
+        "name": "cacc",
+        "outer_dim": "nbody",
+        "stride": "6"
+      },
+      {
+        "type": "mjtNum",
+        "name": "cfrc_int",
+        "outer_dim": "nbody",
+        "stride": "6"
+      },
+      {
+        "type": "mjtNum",
+        "name": "cfrc_ext",
+        "outer_dim": "nbody",
+        "stride": "6"
+      }
+    ],
+    "MJDATA_ARENA_POINTERS_CONTACT": [
+      {
+        "type": "mjContact",
+        "name": "contact",
+        "outer_dim": "MJ_D(ncon)",
+        "stride": "1"
+      }
+    ],
+    "MJDATA_ARENA_POINTERS_SOLVER": [
+      {
+        "type": "int",
+        "name": "efc_type",
+        "outer_dim": "MJ_D(nefc)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "efc_id",
+        "outer_dim": "MJ_D(nefc)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "efc_J_rownnz",
+        "outer_dim": "MJ_D(nefc)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "efc_J_rowadr",
+        "outer_dim": "MJ_D(nefc)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "efc_J_rowsuper",
+        "outer_dim": "MJ_D(nefc)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "efc_J_colind",
+        "outer_dim": "MJ_D(nJ)",
+        "stride": "1"
+      },
+      {
+        "type": "mjtNum",
+        "name": "efc_J",
+        "outer_dim": "MJ_D(nJ)",
+        "stride": "1"
+      },
+      {
+        "type": "mjtNum",
+        "name": "efc_pos",
+        "outer_dim": "MJ_D(nefc)",
+        "stride": "1"
+      },
+      {
+        "type": "mjtNum",
+        "name": "efc_margin",
+        "outer_dim": "MJ_D(nefc)",
+        "stride": "1"
+      },
+      {
+        "type": "mjtNum",
+        "name": "efc_frictionloss",
+        "outer_dim": "MJ_D(nefc)",
+        "stride": "1"
+      },
+      {
+        "type": "mjtNum",
+        "name": "efc_diagApprox",
+        "outer_dim": "MJ_D(nefc)",
+        "stride": "1"
+      },
+      {
+        "type": "mjtNum",
+        "name": "efc_KBIP",
+        "outer_dim": "MJ_D(nefc)",
+        "stride": "4"
+      },
+      {
+        "type": "mjtNum",
+        "name": "efc_D",
+        "outer_dim": "MJ_D(nefc)",
+        "stride": "1"
+      },
+      {
+        "type": "mjtNum",
+        "name": "efc_R",
+        "outer_dim": "MJ_D(nefc)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "tendon_efcadr",
+        "outer_dim": "MJ_M(ntendon)",
+        "stride": "1"
+      },
+      {
+        "type": "mjtNum",
+        "name": "efc_vel",
+        "outer_dim": "MJ_D(nefc)",
+        "stride": "1"
+      },
+      {
+        "type": "mjtNum",
+        "name": "efc_aref",
+        "outer_dim": "MJ_D(nefc)",
+        "stride": "1"
+      },
+      {
+        "type": "mjtNum",
+        "name": "efc_b",
+        "outer_dim": "MJ_D(nefc)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "efc_state",
+        "outer_dim": "MJ_D(nefc)",
+        "stride": "1"
+      },
+      {
+        "type": "mjtNum",
+        "name": "efc_force",
+        "outer_dim": "MJ_D(nefc)",
+        "stride": "1"
+      }
+    ],
+    "MJDATA_ARENA_POINTERS_DUAL": [
+      {
+        "type": "int",
+        "name": "efc_AR_rownnz",
+        "outer_dim": "MJ_D(nefc)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "efc_AR_rowadr",
+        "outer_dim": "MJ_D(nefc)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "efc_AR_colind",
+        "outer_dim": "MJ_D(nA)",
+        "stride": "1"
+      },
+      {
+        "type": "mjtNum",
+        "name": "efc_AR",
+        "outer_dim": "MJ_D(nA)",
+        "stride": "1"
+      }
+    ],
+    "MJDATA_ARENA_POINTERS_ISLAND": [
+      {
+        "type": "int",
+        "name": "tree_island",
+        "outer_dim": "MJ_M(ntree)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "island_ntree",
+        "outer_dim": "MJ_D(nisland)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "island_itreeadr",
+        "outer_dim": "MJ_D(nisland)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "map_itree2tree",
+        "outer_dim": "MJ_M(ntree)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "dof_island",
+        "outer_dim": "MJ_M(nv)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "island_nv",
+        "outer_dim": "MJ_D(nisland)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "island_idofadr",
+        "outer_dim": "MJ_D(nisland)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "island_dofadr",
+        "outer_dim": "MJ_D(nisland)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "map_dof2idof",
+        "outer_dim": "MJ_M(nv)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "map_idof2dof",
+        "outer_dim": "MJ_M(nv)",
+        "stride": "1"
+      },
+      {
+        "type": "mjtNum",
+        "name": "ifrc_smooth",
+        "outer_dim": "MJ_D(nidof)",
+        "stride": "1"
+      },
+      {
+        "type": "mjtNum",
+        "name": "iacc_smooth",
+        "outer_dim": "MJ_D(nidof)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "iM_rownnz",
+        "outer_dim": "MJ_D(nidof)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "iM_rowadr",
+        "outer_dim": "MJ_D(nidof)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "iM_colind",
+        "outer_dim": "MJ_M(nC)",
+        "stride": "1"
+      },
+      {
+        "type": "mjtNum",
+        "name": "iM",
+        "outer_dim": "MJ_M(nC)",
+        "stride": "1"
+      },
+      {
+        "type": "mjtNum",
+        "name": "iLD",
+        "outer_dim": "MJ_M(nC)",
+        "stride": "1"
+      },
+      {
+        "type": "mjtNum",
+        "name": "iLDiagInv",
+        "outer_dim": "MJ_D(nidof)",
+        "stride": "1"
+      },
+      {
+        "type": "mjtNum",
+        "name": "iacc",
+        "outer_dim": "MJ_D(nidof)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "efc_island",
+        "outer_dim": "MJ_D(nefc)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "island_ne",
+        "outer_dim": "MJ_D(nisland)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "island_nf",
+        "outer_dim": "MJ_D(nisland)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "island_nefc",
+        "outer_dim": "MJ_D(nisland)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "island_iefcadr",
+        "outer_dim": "MJ_D(nisland)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "map_efc2iefc",
+        "outer_dim": "MJ_D(nefc)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "map_iefc2efc",
+        "outer_dim": "MJ_D(nefc)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "iefc_type",
+        "outer_dim": "MJ_D(nefc)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "iefc_id",
+        "outer_dim": "MJ_D(nefc)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "iefc_J_rownnz",
+        "outer_dim": "MJ_D(nefc)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "iefc_J_rowadr",
+        "outer_dim": "MJ_D(nefc)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "iefc_J_rowsuper",
+        "outer_dim": "MJ_D(nefc)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "iefc_J_colind",
+        "outer_dim": "MJ_D(nJ)",
+        "stride": "1"
+      },
+      {
+        "type": "mjtNum",
+        "name": "iefc_J",
+        "outer_dim": "MJ_D(nJ)",
+        "stride": "1"
+      },
+      {
+        "type": "mjtNum",
+        "name": "iefc_frictionloss",
+        "outer_dim": "MJ_D(nefc)",
+        "stride": "1"
+      },
+      {
+        "type": "mjtNum",
+        "name": "iefc_D",
+        "outer_dim": "MJ_D(nefc)",
+        "stride": "1"
+      },
+      {
+        "type": "mjtNum",
+        "name": "iefc_R",
+        "outer_dim": "MJ_D(nefc)",
+        "stride": "1"
+      },
+      {
+        "type": "mjtNum",
+        "name": "iefc_aref",
+        "outer_dim": "MJ_D(nefc)",
+        "stride": "1"
+      },
+      {
+        "type": "int",
+        "name": "iefc_state",
+        "outer_dim": "MJ_D(nefc)",
+        "stride": "1"
+      },
+      {
+        "type": "mjtNum",
+        "name": "iefc_force",
+        "outer_dim": "MJ_D(nefc)",
+        "stride": "1"
+      },
+      {
+        "type": "mjtNum",
+        "name": "ifrc_constraint",
+        "outer_dim": "MJ_D(nidof)",
+        "stride": "1"
+      }
+    ],
+    "MJDATA_ARENA_POINTERS": []
+  },
+  "struct_fields": {
+    "MJOPTION_FIELDS": [
+      {
+        "type": "mjtNum",
+        "name": "timestep",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "impratio",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "tolerance",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "ls_tolerance",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "noslip_tolerance",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "ccd_tolerance",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "sleep_tolerance",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "gravity",
+        "count": "3",
+        "is_vec": true
+      },
+      {
+        "type": "mjtNum",
+        "name": "wind",
+        "count": "3",
+        "is_vec": true
+      },
+      {
+        "type": "mjtNum",
+        "name": "magnetic",
+        "count": "3",
+        "is_vec": true
+      },
+      {
+        "type": "mjtNum",
+        "name": "density",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "viscosity",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "o_margin",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "o_solref",
+        "count": "mjNREF",
+        "is_vec": true
+      },
+      {
+        "type": "mjtNum",
+        "name": "o_solimp",
+        "count": "mjNIMP",
+        "is_vec": true
+      },
+      {
+        "type": "mjtNum",
+        "name": "o_friction",
+        "count": "5",
+        "is_vec": true
+      },
+      {
+        "type": "int",
+        "name": "integrator",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "int",
+        "name": "cone",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "int",
+        "name": "jacobian",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "int",
+        "name": "solver",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "int",
+        "name": "iterations",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "int",
+        "name": "ls_iterations",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "int",
+        "name": "noslip_iterations",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "int",
+        "name": "ccd_iterations",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "int",
+        "name": "disableflags",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "int",
+        "name": "enableflags",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "int",
+        "name": "disableactuator",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "int",
+        "name": "sdf_initpoints",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "int",
+        "name": "sdf_iterations",
+        "count": "1",
+        "is_vec": false
+      }
+    ],
+    "MJSTATISTIC_FIELDS": [
+      {
+        "type": "mjtNum",
+        "name": "meaninertia",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "meanmass",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "meansize",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "extent",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "center",
+        "count": "3",
+        "is_vec": true
+      }
+    ],
+    "MJVISUAL_GLOBAL_FIELDS": [
+      {
+        "type": "int",
+        "name": "cameraid",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "int",
+        "name": "orthographic",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "float",
+        "name": "fovy",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "float",
+        "name": "ipd",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "float",
+        "name": "azimuth",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "float",
+        "name": "elevation",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "float",
+        "name": "linewidth",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "float",
+        "name": "glow",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "float",
+        "name": "realtime",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "int",
+        "name": "offwidth",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "int",
+        "name": "offheight",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "int",
+        "name": "ellipsoidinertia",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "int",
+        "name": "bvactive",
+        "count": "1",
+        "is_vec": false
+      }
+    ],
+    "MJVISUAL_QUALITY_FIELDS": [
+      {
+        "type": "mjtNum",
+        "name": "shadowsize",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "offsamples",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "numslices",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "numstacks",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "numquads",
+        "count": "1",
+        "is_vec": false
+      }
+    ],
+    "MJVISUAL_HEADLIGHT_FIELDS": [
+      {
+        "type": "float",
+        "name": "ambient",
+        "count": "3",
+        "is_vec": true
+      },
+      {
+        "type": "float",
+        "name": "diffuse",
+        "count": "3",
+        "is_vec": true
+      },
+      {
+        "type": "float",
+        "name": "specular",
+        "count": "3",
+        "is_vec": true
+      },
+      {
+        "type": "int",
+        "name": "active",
+        "count": "1",
+        "is_vec": false
+      }
+    ],
+    "MJVISUAL_MAP_FIELDS": [
+      {
+        "type": "mjtNum",
+        "name": "stiffness",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "stiffnessrot",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "force",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "torque",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "alpha",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "fogstart",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "fogend",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "znear",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "zfar",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "haze",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "shadowclip",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "shadowscale",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "actuatortendon",
+        "count": "1",
+        "is_vec": false
+      }
+    ],
+    "MJVISUAL_SCALE_FIELDS": [
+      {
+        "type": "mjtNum",
+        "name": "forcewidth",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "contactwidth",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "contactheight",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "connect",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "com",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "camera",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "light",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "selectpoint",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "jointlength",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "jointwidth",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "actuatorlength",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "actuatorwidth",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "framelength",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "framewidth",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "constraint",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "slidercrank",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "frustum",
+        "count": "1",
+        "is_vec": false
+      }
+    ],
+    "MJVISUAL_RGBA_FIELDS": [
+      {
+        "type": "mjtNum",
+        "name": "fog",
+        "count": "1",
+        "is_vec": false
       },
       {
         "type": "mjtNum",
-        "name": "o_friction",
-        "count": "5",
-        "is_vec": true
+        "name": "haze",
+        "count": "1",
+        "is_vec": false
       },
       {
-        "type": "int",
-        "name": "integrator",
+        "type": "mjtNum",
+        "name": "force",
         "count": "1",
         "is_vec": false
       },
       {
-        "type": "int",
-        "name": "cone",
+        "type": "mjtNum",
+        "name": "inertia",
         "count": "1",
         "is_vec": false
       },
       {
-        "type": "int",
-        "name": "jacobian",
+        "type": "mjtNum",
+        "name": "joint",
         "count": "1",
         "is_vec": false
       },
       {
-        "type": "int",
-        "name": "solver",
+        "type": "mjtNum",
+        "name": "actuator",
         "count": "1",
         "is_vec": false
       },
       {
-        "type": "int",
-        "name": "iterations",
+        "type": "mjtNum",
+        "name": "actuatornegative",
         "count": "1",
         "is_vec": false
       },
       {
-        "type": "int",
-        "name": "ls_iterations",
+        "type": "mjtNum",
+        "name": "actuatorpositive",
         "count": "1",
         "is_vec": false
       },
       {
-        "type": "int",
-        "name": "noslip_iterations",
+        "type": "mjtNum",
+        "name": "com",
         "count": "1",
         "is_vec": false
       },
       {
-        "type": "int",
-        "name": "ccd_iterations",
+        "type": "mjtNum",
+        "name": "camera",
         "count": "1",
         "is_vec": false
       },
       {
-        "type": "int",
-        "name": "disableflags",
+        "type": "mjtNum",
+        "name": "light",
         "count": "1",
         "is_vec": false
       },
       {
-        "type": "int",
-        "name": "enableflags",
+        "type": "mjtNum",
+        "name": "selectpoint",
         "count": "1",
         "is_vec": false
       },
       {
-        "type": "int",
-        "name": "disableactuator",
+        "type": "mjtNum",
+        "name": "connect",
         "count": "1",
         "is_vec": false
       },
       {
-        "type": "int",
-        "name": "sdf_initpoints",
+        "type": "mjtNum",
+        "name": "contactpoint",
         "count": "1",
         "is_vec": false
       },
       {
-        "type": "int",
-        "name": "sdf_iterations",
+        "type": "mjtNum",
+        "name": "contactforce",
         "count": "1",
         "is_vec": false
-      }
-    ],
-    "MJSTATISTIC_FIELDS": [
+      },
       {
         "type": "mjtNum",
-        "name": "meaninertia",
+        "name": "contactfriction",
         "count": "1",
         "is_vec": false
       },
       {
         "type": "mjtNum",
-        "name": "meanmass",
+        "name": "contacttorque",
         "count": "1",
         "is_vec": false
       },
       {
         "type": "mjtNum",
-        "name": "meansize",
+        "name": "contactgap",
         "count": "1",
         "is_vec": false
       },
       {
         "type": "mjtNum",
-        "name": "extent",
+        "name": "rangefinder",
         "count": "1",
         "is_vec": false
       },
       {
         "type": "mjtNum",
-        "name": "center",
+        "name": "constraint",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "slidercrank",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "crankbroken",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "frustum",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "bv",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtNum",
+        "name": "bvactive",
+        "count": "1",
+        "is_vec": false
+      }
+    ],
+    "MJSCOMPILER_FIELDS": [
+      {
+        "type": "mjtByte",
+        "name": "autolimits",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "double",
+        "name": "boundmass",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "double",
+        "name": "boundinertia",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "double",
+        "name": "settotalmass",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtByte",
+        "name": "balanceinertia",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtByte",
+        "name": "fitaabb",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtByte",
+        "name": "degree",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "char",
+        "name": "eulerseq",
         "count": "3",
         "is_vec": true
+      },
+      {
+        "type": "mjtByte",
+        "name": "discardvisual",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtByte",
+        "name": "usethread",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtByte",
+        "name": "fusestatic",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "int",
+        "name": "inertiafromgeom",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "int",
+        "name": "inertiagrouprange",
+        "count": "2",
+        "is_vec": true
+      },
+      {
+        "type": "mjtByte",
+        "name": "saveinertial",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "int",
+        "name": "alignfree",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjLROpt",
+        "name": "LRopt",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjString*",
+        "name": "meshdir",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjString*",
+        "name": "texturedir",
+        "count": "1",
+        "is_vec": false
       }
     ],
-    "MJVISUAL_GLOBAL_FIELDS": [
+    "MJSPEC_FIELDS": [
+      {
+        "type": "mjsElement*",
+        "name": "element",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjString*",
+        "name": "modelname",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjsCompiler",
+        "name": "compiler",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtByte",
+        "name": "strippath",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjOption",
+        "name": "option",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjVisual",
+        "name": "visual",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjStatistic",
+        "name": "stat",
+        "count": "1",
+        "is_vec": false
+      },
+      {
+        "type": "mjtSize",
+        "name": "memory",
+        "count": "1",
+        "is_vec": false
+      },
       {
         "type": "int",
-        "name": "cameraid",
+        "name": "nemax",
         "count": "1",
         "is_vec": false
       },
       {
         "type": "int",
-        "name": "orthographic",
+        "name": "nuserdata",
         "count": "1",
         "is_vec": false
       },
       {
-        "type": "float",
-        "name": "fovy",
+        "type": "int",
+        "name": "nuser_body",
         "count": "1",
         "is_vec": false
       },
       {
-        "type": "float",
-        "name": "ipd",
+        "type": "int",
+        "name": "nuser_jnt",
         "count": "1",
         "is_vec": false
       },
       {
-        "type": "float",
-        "name": "azimuth",
+        "type": "int",
+        "name": "nuser_geom",
         "count": "1",
         "is_vec": false
       },
       {
-        "type": "float",
-        "name": "elevation",
+        "type": "int",
+        "name": "nuser_site",
         "count": "1",
         "is_vec": false
       },
       {
-        "type": "float",
-        "name": "linewidth",
+        "type": "int",
+        "name": "nuser_cam",
         "count": "1",
         "is_vec": false
       },
       {
-        "type": "float",
-        "name": "glow",
+        "type": "int",
+        "name": "nuser_tendon",
         "count": "1",
         "is_vec": false
       },
       {
-        "type": "float",
-        "name": "realtime",
+        "type": "int",
+        "name": "nuser_actuator",
         "count": "1",
         "is_vec": false
       },
       {
         "type": "int",
-        "name": "offwidth",
+        "name": "nuser_sensor",
         "count": "1",
         "is_vec": false
       },
       {
         "type": "int",
-        "name": "offheight",
+        "name": "nkey",
         "count": "1",
         "is_vec": false
       },
       {
         "type": "int",
-        "name": "ellipsoidinertia",
+        "name": "njmax",
         "count": "1",
         "is_vec": false
       },
       {
         "type": "int",
-        "name": "bvactive",
+        "name": "nconmax",
         "count": "1",
         "is_vec": false
-      }
-    ],
-    "MJVISUAL_QUALITY_FIELDS": [],
-    "MJVISUAL_HEADLIGHT_FIELDS": [
+      },
       {
-        "type": "float",
-        "name": "ambient",
-        "count": "3",
-        "is_vec": true
+        "type": "mjtSize",
+        "name": "nstack",
+        "count": "1",
+        "is_vec": false
       },
       {
-        "type": "float",
-        "name": "diffuse",
-        "count": "3",
-        "is_vec": true
+        "type": "mjString*",
+        "name": "comment",
+        "count": "1",
+        "is_vec": false
       },
       {
-        "type": "float",
-        "name": "specular",
-        "count": "3",
-        "is_vec": true
+        "type": "mjString*",
+        "name": "modelfiledir",
+        "count": "1",
+        "is_vec": false
       },
       {
-        "type": "int",
-        "name": "active",
+        "type": "mjtByte",
+        "name": "hasImplicitPluginElem",
         "count": "1",
         "is_vec": false
       }
-    ],
-    "MJVISUAL_MAP_FIELDS": [],
-    "MJVISUAL_SCALE_FIELDS": [],
-    "MJVISUAL_RGBA_FIELDS": []
+    ]
   }
 }
\ No newline at end of file
diff --git a/Scripts/codegen/sync_vendored.py b/Scripts/codegen/sync_vendored.py
new file mode 100644
index 0000000..f863bec
--- /dev/null
+++ b/Scripts/codegen/sync_vendored.py
@@ -0,0 +1,165 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""
+Refresh vendored third-party files listed in
+``Scripts/codegen/_vendored/_VENDORED_FROM.md``.
+
+Manifest format: GitHub-flavoured markdown table with columns
+``file | upstream_url | upstream_sha | retrieved_date | license | local_dest``.
+``upstream_url`` may contain ``{sha}`` which gets substituted with
+``upstream_sha`` at fetch time.
+
+Re-running on the same SHAs produces zero diff in the manifest (the
+script rewrites the table with stable column ordering + sorted-by-dest
+rows, and re-stamps ``retrieved_date`` only when the fetch changed the
+file content).
+
+Usage:
+    python Scripts/codegen/sync_vendored.py
+        [--manifest Scripts/codegen/_vendored/_VENDORED_FROM.md]
+        [--dry-run]   (don't write anything; print what would happen)
+"""
+
+from __future__ import annotations
+
+import argparse
+import datetime as _dt
+import hashlib
+import os
+import re
+import sys
+import urllib.request
+from typing import Dict, List
+
+_HERE = os.path.dirname(os.path.abspath(__file__))
+_PLUGIN_ROOT = os.path.normpath(os.path.join(_HERE, "..", ".."))
+_DEFAULT_MANIFEST = os.path.join(_HERE, "_vendored", "_VENDORED_FROM.md")
+
+
+# Markdown-table row: pipe-separated, with leading + trailing pipes
+# trimmed. Comments / preamble paragraphs are ignored.
+_ROW_RE = re.compile(r"^\s*\|(?P<row>.+)\|\s*$")
+
+
+def _parse_manifest(text: str) -> List[Dict[str, str]]:
+    """Return the table rows as dicts keyed by column header. Skips the
+    header row and the markdown separator row (``|---|---|...``)."""
+    rows: List[Dict[str, str]] = []
+    header: List[str] = []
+    for line in text.splitlines():
+        m = _ROW_RE.match(line)
+        if not m:
+            continue
+        cols = [c.strip() for c in m.group("row").split("|")]
+        if not header:
+            header = cols
+            continue
+        if all(c.startswith("-") or c == "" for c in cols):
+            continue  # separator
+        if len(cols) != len(header):
+            continue  # malformed; ignore quietly
+        rows.append(dict(zip(header, cols)))
+    return rows
+
+
+_COL_ORDER = ("file", "upstream_url", "upstream_sha", "retrieved_date",
+              "license", "local_dest")
+
+
+def _format_manifest(rows: List[Dict[str, str]], preamble: str) -> str:
+    """Render the table with stable column ordering + sorted-by-dest rows."""
+    rows = sorted(rows, key=lambda r: r.get("local_dest", ""))
+    header = " | ".join(_COL_ORDER)
+    sep = " | ".join("-" * max(3, len(c)) for c in _COL_ORDER)
+    lines = [f"| {header} |", f"| {sep} |"]
+    for r in rows:
+        cells = [r.get(c, "") for c in _COL_ORDER]
+        lines.append("| " + " | ".join(cells) + " |")
+    return preamble.rstrip() + "\n\n" + "\n".join(lines) + "\n"
+
+
+def _extract_preamble(text: str) -> str:
+    """Strip everything from the first table row onward."""
+    lines = text.splitlines()
+    out: List[str] = []
+    for line in lines:
+        if _ROW_RE.match(line):
+            break
+        out.append(line)
+    return "\n".join(out)
+
+
+def _fetch(url: str, sha: str) -> bytes:
+    real_url = url.replace("{sha}", sha)
+    print(f"  GET {real_url}", file=sys.stderr)
+    req = urllib.request.Request(real_url, headers={"User-Agent": "urlab-sync_vendored"})
+    with urllib.request.urlopen(req, timeout=30) as resp:  # nosec: trusted manifest URLs
+        return resp.read()
+
+
+def _maybe_write(path: str, content: bytes, dry_run: bool) -> bool:
+    """Returns True if the file content changed."""
+    existing = None
+    if os.path.exists(path):
+        with open(path, "rb") as f:
+            existing = f.read()
+    if existing == content:
+        return False
+    if dry_run:
+        print(f"  WOULD WRITE {path} ({len(content)} bytes)")
+        return True
+    os.makedirs(os.path.dirname(path), exist_ok=True)
+    with open(path, "wb") as f:
+        f.write(content)
+    print(f"  WROTE {path} ({len(content)} bytes; sha256="
+          f"{hashlib.sha256(content).hexdigest()[:12]})")
+    return True
+
+
+def main(argv: List[str] | None = None) -> int:
+    ap = argparse.ArgumentParser(description=__doc__)
+    ap.add_argument("--manifest", default=_DEFAULT_MANIFEST)
+    ap.add_argument("--dry-run", action="store_true",
+                    help="don't write any files; print what would happen")
+    args = ap.parse_args(argv)
+
+    if not os.path.exists(args.manifest):
+        print(f"manifest not found: {args.manifest}", file=sys.stderr)
+        return 1
+
+    with open(args.manifest, "r", encoding="utf-8") as f:
+        text = f.read()
+    preamble = _extract_preamble(text)
+    rows = _parse_manifest(text)
+
+    today = _dt.date.today().isoformat()
+    updated_rows: List[Dict[str, str]] = []
+    for row in rows:
+        url = row.get("upstream_url", "")
+        sha = row.get("upstream_sha", "")
+        dest_rel = row.get("local_dest", "")
+        if not (url and sha and dest_rel):
+            print(f"skipping malformed row: {row}", file=sys.stderr)
+            updated_rows.append(row)
+            continue
+        try:
+            content = _fetch(url, sha)
+        except Exception as exc:  # noqa: BLE001
+            print(f"  FAILED to fetch {url} @ {sha}: {exc}", file=sys.stderr)
+            updated_rows.append(row)
+            continue
+        dest_abs = os.path.join(_PLUGIN_ROOT, dest_rel.replace("/", os.sep))
+        changed = _maybe_write(dest_abs, content, args.dry_run)
+        if changed and not args.dry_run:
+            row["retrieved_date"] = today
+        updated_rows.append(row)
+
+    new_text = _format_manifest(updated_rows, preamble)
+    if new_text != text and not args.dry_run:
+        with open(args.manifest, "w", encoding="utf-8") as f:
+            f.write(new_text)
+        print(f"updated manifest: {args.manifest}")
+    return 0
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())
diff --git a/Scripts/codegen/tests/conftest.py b/Scripts/codegen/tests/conftest.py
index 1d55e7b..97eda15 100644
--- a/Scripts/codegen/tests/conftest.py
+++ b/Scripts/codegen/tests/conftest.py
@@ -24,7 +24,7 @@
 @pytest.fixture(scope="session")
 def real_schema() -> Dict[str, Any]:
     """The actual MJCF schema snapshot shipped with the plugin."""
-    path = os.path.join(_PLUGIN_ROOT, "Scripts", "mjcf_schema_snapshot.json")
+    path = os.path.join(_PLUGIN_ROOT, "Scripts", "codegen", "snapshots", "mjcf_schema_snapshot.json")
     with open(path, "r", encoding="utf-8") as f:
         return json.load(f)
 
@@ -38,11 +38,36 @@ def real_rules() -> Dict[str, Any]:
 
 @pytest.fixture(scope="session")
 def real_mjxmacro() -> Dict[str, Any]:
-    path = os.path.join(_PLUGIN_ROOT, "Scripts", "mjxmacro_snapshot.json")
+    path = os.path.join(_PLUGIN_ROOT, "Scripts", "codegen", "snapshots", "mjxmacro_snapshot.json")
     with open(path, "r", encoding="utf-8") as f:
         return json.load(f)
 
 
+@pytest.fixture(scope="session")
+def real_mjspec() -> Dict[str, Any]:
+    """The mjspec snapshot file was retired; the runtime
+    now projects the introspect snapshot into the legacy mjspec shape.
+    The fixture mirrors that projection so tests stay isomorphic."""
+    import generate_ue_components as gen
+    introspect_path = os.path.join(
+        _PLUGIN_ROOT, "Scripts", "codegen", "snapshots", "introspect_snapshot.json",
+    )
+    with open(introspect_path, "r", encoding="utf-8") as f:
+        introspect = json.load(f)
+    return gen._mjspec_from_introspect(introspect)
+
+
+@pytest.fixture(autouse=True)
+def _clear_diags():
+    """Each test starts with a clean diagnostics buffer + strict counter.
+    Module-level state survives across tests otherwise — autouse keeps
+    drift-check tests from leaking state into one another."""
+    import generate_ue_components as gen  # noqa: E402, local import
+    gen._reset_diags()
+    yield
+    gen._reset_diags()
+
+
 @pytest.fixture
 def minimal_rules() -> Dict[str, Any]:
     """A small synthetic rules block, useful for unit-testing emission
diff --git a/Scripts/codegen/tests/test_behavioral_emit_guarantees.py b/Scripts/codegen/tests/test_behavioral_emit_guarantees.py
new file mode 100644
index 0000000..fc13977
--- /dev/null
+++ b/Scripts/codegen/tests/test_behavioral_emit_guarantees.py
@@ -0,0 +1,198 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""Behavioural lock-ins for the codegen's emit surface.
+
+Substring-only assertions catch typos but not semantics — these tests
+pin three load-bearing properties of the generated C++:
+
+1. XML enum roundtrip — every ``value_map`` entry produces matching
+   import + export arms whose XML strings and UE members agree.
+2. ``mjs_setTo*`` argument order — the codegen feeds params to the
+   call in the exact order declared by the C signature in the
+   snapshot, not by rule-side iteration order. Misordered args here
+   would call ``mjs_setToPosition(kp_value, kv_value, ...)`` swapped
+   and silently miscalibrate every Position actuator.
+3. Override-toggle gate — every codegen-emitted UPROPERTY pair is
+   ``bOverride_X`` (toggle) + ``X`` (value). Both names must agree
+   so the editor InlineEditConditionToggle wiring binds correctly.
+"""
+
+from __future__ import annotations
+
+import os
+import sys
+
+_HERE = os.path.dirname(os.path.abspath(__file__))
+_CODEGEN_DIR = os.path.dirname(_HERE)
+if _CODEGEN_DIR not in sys.path:
+    sys.path.insert(0, _CODEGEN_DIR)
+
+import re
+
+import generate_ue_components as gen
+
+
+# ---------- xml_enum roundtrip ---------------------------------------------
+
+def test_xml_enum_roundtrip_arms_match(real_rules):
+    """Every value_map entry must be reachable from both directions:
+    ``XML string -> UE member`` (import) and ``UE member -> XML
+    string`` (export). A typo in either arm would silently misimport
+    or misexport the attr without a compile error."""
+    for elem, elem_rule in real_rules.get("element_rules", {}).items():
+        for attr, enum_def in elem_rule.get("xml_enum_attrs", {}).items():
+            value_map = enum_def.get("value_map") or {}
+            if not value_map:
+                continue
+            ue_enum_type = enum_def["ue_enum_type"]
+            import_cpp = gen._emit_xml_enum_import(attr, enum_def)
+            # Enums with no ``mjs_field`` (e.g. dcmotor.input, routed
+            # through mjs_setToDCMotor) emit nothing on the export side
+            # by design — skip the export arm in that case.
+            export_cpp = (
+                gen._emit_xml_enum_export(attr, enum_def)
+                if enum_def.get("mjs_field") else ""
+            )
+            for xml_val, mapping in value_map.items():
+                ue_member = mapping[0] if isinstance(mapping, list) else mapping
+                # Import side mentions both XML literal and UE member.
+                assert f'"{xml_val}"' in import_cpp, (
+                    f"{elem}.{attr}: import arm missing XML literal "
+                    f"'{xml_val}'\n{import_cpp}"
+                )
+                assert f"{ue_enum_type}::{ue_member}" in import_cpp, (
+                    f"{elem}.{attr}: import arm missing "
+                    f"{ue_enum_type}::{ue_member}\n{import_cpp}"
+                )
+                if export_cpp:
+                    assert f"{ue_enum_type}::{ue_member}" in export_cpp, (
+                        f"{elem}.{attr}: export arm missing "
+                        f"{ue_enum_type}::{ue_member}\n{export_cpp}"
+                    )
+
+
+# ---------- mjs_setTo* arg order -------------------------------------------
+
+def test_setto_call_args_follow_c_signature_order():
+    """The generated ``mjs_setToX(Element, a, b, c)`` call must list
+    args in the order declared by the C signature. Reordering by
+    rule-side iteration would silently swap kp/kv on actuator
+    subtypes."""
+    rules = {
+        "default_type": "float",
+        "type_mappings": {"kp": "float", "kv": "float", "dampratio": "float"},
+        "setto_param_defaults": {},
+    }
+    mjspec = {
+        "setto_functions": {
+            "mjs_setToPosition": {
+                "params": [
+                    {"name": "kp",        "c_type": "double", "array_dim": None},
+                    {"name": "kv",        "c_type": "double", "array_dim": None},
+                    {"name": "dampratio", "c_type": "double", "array_dim": None},
+                ],
+            },
+        },
+    }
+    setto_rules = {"call": "mjs_setToPosition"}
+    out = gen._emit_setto_call(
+        subtype_key="position",
+        setto_rules=setto_rules,
+        rules=rules,
+        mjspec=mjspec,
+        subtype_schema_attrs=["kp", "kv", "dampratio"],
+        base_schema_attrs=[],
+    )
+    assert "mjs_setToPosition(Element" in out
+    # Capture the argument list and confirm kp appears before kv before
+    # dampratio — exact textual order in the call.
+    match = re.search(r"mjs_setToPosition\(Element,(.*?)\);", out, re.DOTALL)
+    assert match is not None, out
+    args_blob = match.group(1)
+    kp_pos        = args_blob.find("(double)kp")
+    kv_pos        = args_blob.find("(double)kv")
+    dampratio_pos = args_blob.find("(double)dampratio")
+    assert 0 < kp_pos < kv_pos < dampratio_pos, (
+        f"setto args out of order: kp@{kp_pos} kv@{kv_pos} "
+        f"dampratio@{dampratio_pos}\n{out}"
+    )
+
+
+def test_setto_call_respects_signature_order_when_rule_lists_differently():
+    """Rule-side ``param_renames`` lookup order MUST NOT influence the
+    emitted arg order — the C signature is the source of truth."""
+    rules = {
+        "default_type": "float",
+        "type_mappings": {"PropB": "float", "PropA": "float"},
+        "setto_param_defaults": {},
+    }
+    mjspec = {
+        "setto_functions": {
+            "mjs_setToFake": {
+                "params": [
+                    {"name": "a", "c_type": "double", "array_dim": None},
+                    {"name": "b", "c_type": "double", "array_dim": None},
+                ],
+            },
+        },
+    }
+    # param_renames iteration order intentionally NOT matching signature order.
+    setto_rules = {"call": "mjs_setToFake",
+                   "param_renames": {"b": "PropB", "a": "PropA"}}
+    out = gen._emit_setto_call(
+        subtype_key="fake",
+        setto_rules=setto_rules,
+        rules=rules,
+        mjspec=mjspec,
+        subtype_schema_attrs=["PropA", "PropB"],
+        base_schema_attrs=[],
+    )
+    match = re.search(r"mjs_setToFake\(Element,(.*?)\);", out, re.DOTALL)
+    assert match is not None, out
+    args_blob = match.group(1)
+    a_pos = args_blob.find("PropA")
+    b_pos = args_blob.find("PropB")
+    assert 0 < a_pos < b_pos, (
+        f"renamed setto args out of signature order: A@{a_pos} B@{b_pos}\n{out}"
+    )
+
+
+# ---------- override-toggle gate -------------------------------------------
+
+_UPROPERTY_PAIR_RE = re.compile(
+    r"bool\s+(bOverride_(\w+))\s*=\s*false;\s*\n+"
+    r"\s*UPROPERTY\(.*?\)\)\s*\n"
+    r"\s*\S+\s+(\w+)\s*="
+)
+
+
+def test_override_toggle_name_matches_uproperty_for_all_categories(real_rules):
+    """For every category whose schema produces a UPROPERTY pair, the
+    ``bOverride_X`` toggle and the value property ``X`` must share the
+    same final identifier. A drift between them would still compile
+    but the InlineEditConditionToggle wiring in the editor would
+    silently dead-end."""
+    schema_path = os.path.join(_CODEGEN_DIR, "snapshots", "mjcf_schema_snapshot.json")
+    if not os.path.exists(schema_path):
+        import pytest
+        pytest.skip("mjcf_schema.json snapshot not present")
+    import json
+    with open(schema_path, "r", encoding="utf-8") as f:
+        schema = json.load(f)
+    saw_pair = False
+    for cat_name, cat_rules in real_rules.get("categories", {}).items():
+        attrs = gen.schema_attrs(schema, cat_rules.get("schema_common_block", ""))
+        if not attrs:
+            continue
+        out = gen.emit_schema_for_attrs(
+            attrs, real_rules,
+            element_name=cat_name,
+            category_label=cat_name,
+        )
+        for match in _UPROPERTY_PAIR_RE.finditer(out.properties_h):
+            full_toggle, toggle_suffix, value_name = match.groups()
+            assert toggle_suffix == value_name, (
+                f"category '{cat_name}': UPROPERTY pair drift — toggle "
+                f"'{full_toggle}' but value name is '{value_name}'"
+            )
+            saw_pair = True
+    assert saw_pair, "no UPROPERTY pairs found in any category — check broken"
diff --git a/Scripts/codegen/tests/test_camp_b_enum_migration.py b/Scripts/codegen/tests/test_camp_b_enum_migration.py
new file mode 100644
index 0000000..f2d73a1
--- /dev/null
+++ b/Scripts/codegen/tests/test_camp_b_enum_migration.py
@@ -0,0 +1,188 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""Locks the generated_enums extensions:
+``extra_members`` (URLab-extra members appended after mj-derived ones)
+and the ``disabled`` opt-out (skips emission so a rule can land before
+the corresponding hand-rolled enums are swapped out).
+
+Also asserts that the Camp B rule entries shipped under
+``MjArticulationEnums`` in codegen_rules.json resolve cleanly against
+the current mjspec snapshot — every ``from_mj_enum`` exists and the
+``ue_member_from_mj`` keys all map to real ``mjt*`` constants."""
+
+from __future__ import annotations
+
+import os
+
+import generate_ue_components as gen
+
+
+_HERE = os.path.dirname(os.path.abspath(__file__))
+_CODEGEN_DIR = os.path.dirname(_HERE)
+_PLUGIN_ROOT = os.path.dirname(os.path.dirname(_CODEGEN_DIR))
+
+
+# ---------- extra_members --------------------------------------------------
+
+def test_extra_members_append_after_mj_derived_members():
+    """URLab-extra members (DcMotor on EMjGainType etc) must follow
+    the mj-derived block so existing member ordering doesn't change."""
+    rules_def = {
+        "public_header": "Generated/MjT.h",
+        "enums": [{
+            "ue_name": "EMjT",
+            "from_mj_enum": "mjtT",
+            "ue_member_from_mj": {
+                "mjT_A": "A",
+                "mjT_B": "B",
+            },
+            "extra_members": [{"name": "DcMotor"}],
+        }],
+    }
+    mjspec = {"enums": {"mjtT": {"mjT_A": 0, "mjT_B": 1}}}
+    writes = gen._emit_generated_enum_file(
+        "MjArticulationEnums", rules_def, mjspec, "/tmp/public",
+    )
+    assert len(writes) == 1
+    content = writes[0].content
+    a_idx = content.index("A = 0")
+    b_idx = content.index("B = 1")
+    dc_idx = content.index("DcMotor = 2")
+    assert a_idx < b_idx < dc_idx
+
+
+def test_extra_member_explicit_value_overrides_auto_increment():
+    """``{"name": ..., "value": N}`` pins a specific underlying value
+    — needed for enums that mirror mjt* numbering exactly."""
+    rules_def = {
+        "public_header": "Generated/MjT.h",
+        "enums": [{
+            "ue_name": "EMjT",
+            "from_mj_enum": "mjtT",
+            "ue_member_from_mj": {},
+            "extra_members": [
+                {"name": "None",  "value": 0},
+                {"name": "Plane", "value": 7},
+            ],
+        }],
+    }
+    mjspec = {"enums": {"mjtT": {"mjT_DUMMY": 0}}}
+    writes = gen._emit_generated_enum_file(
+        "MjArticulationEnums", rules_def, mjspec, "/tmp/public",
+    )
+    assert writes
+    content = writes[0].content
+    assert "None = 0" in content
+    assert "Plane = 7" in content
+
+
+def test_extra_member_without_name_fires_diagnostic():
+    rules_def = {
+        "public_header": "Generated/MjT.h",
+        "enums": [{
+            "ue_name": "EMjT",
+            "from_mj_enum": "mjtT",
+            "ue_member_from_mj": {"mjT_A": "A"},
+            "extra_members": [{"value": 5}],  # missing name
+        }],
+    }
+    mjspec = {"enums": {"mjtT": {"mjT_A": 0}}}
+    gen._emit_generated_enum_file(
+        "MjArticulationEnums", rules_def, mjspec, "/tmp/public",
+    )
+    assert any("missing 'name'" in d.message for d in gen._DIAGS_BUFFER.pending)
+
+
+# ---------- disabled opt-out -----------------------------------------------
+
+def test_disabled_top_level_skips_emission():
+    """``disabled: true`` at the rule entry level emits no FileWrite —
+    used while staging a migration so a hand-rolled enum isn't
+    duplicated by the codegen output."""
+    rules_def = {
+        "disabled": True,
+        "public_header": "Generated/MjT.h",
+        "enums": [{
+            "ue_name": "EMjT",
+            "from_mj_enum": "mjtT",
+            "ue_member_from_mj": {"mjT_A": "A"},
+        }],
+    }
+    mjspec = {"enums": {"mjtT": {"mjT_A": 0}}}
+    writes = gen._emit_generated_enum_file(
+        "MjArticulationEnums", rules_def, mjspec, "/tmp/public",
+    )
+    assert writes == []
+
+
+def test_disabled_per_entry_skips_one_enum_in_a_rule():
+    """Per-entry ``disabled: true`` skips just that enum but lets the
+    rule's other entries still emit. Granular staging."""
+    rules_def = {
+        "public_header": "Generated/MjT.h",
+        "enums": [
+            {
+                "ue_name": "EMjEnabled",
+                "from_mj_enum": "mjtT",
+                "ue_member_from_mj": {"mjT_A": "Alpha"},
+            },
+            {
+                "ue_name": "EMjDisabled",
+                "disabled": True,
+                "from_mj_enum": "mjtT",
+                "ue_member_from_mj": {"mjT_A": "Beta"},
+            },
+        ],
+    }
+    mjspec = {"enums": {"mjtT": {"mjT_A": 0}}}
+    writes = gen._emit_generated_enum_file(
+        "MjArticulationEnums", rules_def, mjspec, "/tmp/public",
+    )
+    assert len(writes) == 1
+    content = writes[0].content
+    assert "EMjEnabled" in content
+    assert "EMjDisabled" not in content
+
+
+# ---------- Camp B rule audit ---------------------------------------------
+
+def test_camp_b_rule_resolves_against_real_mjspec(real_rules, real_mjspec):
+    """Every Camp B entry's from_mj_enum must exist in the mjspec
+    snapshot, and every ue_member_from_mj key must be a real mjt*
+    constant. Catches typos in the migration rule before the actual
+    flip lands."""
+    camp_b = real_rules.get("generated_enums", {}).get("MjArticulationEnums")
+    if not camp_b or not isinstance(camp_b, dict):
+        return  # rule not present yet
+    mj_enums = real_mjspec.get("enums", {})
+    for entry in camp_b.get("enums", []):
+        # Pure URLab enums (no mjt counterpart) omit from_mj_enum and
+        # rely entirely on extra_members. Skip the cross-check for those.
+        mj_enum_name = entry.get("from_mj_enum")
+        if not mj_enum_name:
+            continue
+        assert mj_enum_name in mj_enums, (
+            f"Camp B '{entry['ue_name']}' -> from_mj_enum "
+            f"'{mj_enum_name}' not in mjspec.enums"
+        )
+        members = mj_enums[mj_enum_name]
+        for mj_const in entry.get("ue_member_from_mj", {}):
+            assert mj_const in members, (
+                f"Camp B '{entry['ue_name']}' ue_member_from_mj key "
+                f"'{mj_const}' is not in mjtEnum '{mj_enum_name}'"
+            )
+
+
+def test_camp_b_rule_post_flip_state(real_rules):
+    """After the flip, the rule no longer carries ``disabled: true``
+    and the hand-rolled UENUM decls under
+    ``Source/URLab/Public/MuJoCo/Components/`` have been replaced
+    with includes of the generated header."""
+    camp_b = real_rules.get("generated_enums", {}).get("MjArticulationEnums")
+    if not camp_b or not isinstance(camp_b, dict):
+        return
+    assert not camp_b.get("disabled"), (
+        "MjArticulationEnums should be enabled after the Camp B flip."
+    )
+
+
+# On-disk presence is covered by test_regen_no_diff.py::test_codegen_check_clean.
diff --git a/Scripts/codegen/tests/test_canon_emitter_bodies.py b/Scripts/codegen/tests/test_canon_emitter_bodies.py
new file mode 100644
index 0000000..13b8170
--- /dev/null
+++ b/Scripts/codegen/tests/test_canon_emitter_bodies.py
@@ -0,0 +1,91 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""Behavioural lock-ins for the canon emitter bodies that previously
+only had registry-presence coverage. ``actuator_transmission`` and
+``fromto_decompose`` are inline (no rule-side helper string); their
+emitted C++ is hand-crafted, so any rename or signature change there
+should surface in pytest rather than at UE compile."""
+
+from __future__ import annotations
+
+import generate_ue_components as gen
+
+
+# ---------- actuator_transmission ---------------------------------------
+
+def test_actuator_transmission_import_reads_every_target_attr():
+    """The import block reads joint/jointinparent/tendon/site/body and
+    the slidersite/refsite siblings. A missing branch means a rule
+    author can't author that transmission type via XML."""
+    out = gen._canon_import_actuator_transmission(canon_def={}, element_name="actuator")
+    for attr in ("joint", "jointinparent", "tendon", "site", "body",
+                 "slidersite", "refsite"):
+        assert f'TEXT("{attr}")' in out, f"import missing TEXT(\"{attr}\")"
+    # Each path resolves to an EMjActuatorTrnType.
+    for trn in ("Joint", "JointInParent", "Tendon", "Site", "Body",
+                "SliderCrank"):
+        assert f"EMjActuatorTrnType::{trn}" in out, trn
+
+
+def test_actuator_transmission_export_emits_switch_with_undefined_default():
+    out = gen._canon_export_actuator_transmission(canon_def={})
+    # Every case writes Element->trntype.
+    for trn, mj_const in [
+        ("Joint", "mjTRN_JOINT"),
+        ("JointInParent", "mjTRN_JOINTINPARENT"),
+        ("Tendon", "mjTRN_TENDON"),
+        ("Site", "mjTRN_SITE"),
+        ("Body", "mjTRN_BODY"),
+        ("SliderCrank", "mjTRN_SLIDERCRANK"),
+    ]:
+        assert f"EMjActuatorTrnType::{trn}" in out, trn
+        assert mj_const in out, mj_const
+    assert "default:" in out and "mjTRN_UNDEFINED" in out
+    # slidersite / refsite only written when their type is set.
+    assert "SliderCrank && !SliderSite.IsEmpty()" in out
+    assert "Site && !RefSite.IsEmpty()" in out
+
+
+# ---------- fromto_decompose --------------------------------------------
+
+def test_fromto_decompose_import_uses_helper_and_writes_pos_quat_size():
+    canon_def = {
+        "import_helper": "MjUtils::DecomposeFromTo",
+        "category_specifics": {"geom": {
+            "z_slot_check": "(Type == EMjGeomType::Box || Type == EMjGeomType::Ellipsoid)"
+        }},
+    }
+    out = gen._canon_import_fromto_decompose(canon_def, element_name="geom")
+    assert "MjUtils::DecomposeFromTo(Node, FTPos, FTQuat, FTHalf)" in out
+    # Writes Pos / Quat with their override toggles.
+    assert "Pos = FTPos;" in out and "bOverride_Pos = true;" in out
+    assert "Quat = FTQuat;" in out and "bOverride_Quat = true;" in out
+    # Uses the category-specific z_slot_check inline.
+    assert "EMjGeomType::Box" in out and "EMjGeomType::Ellipsoid" in out
+    # Grows the size array to accommodate the slot.
+    assert "while (size.Num() <= FTSlot)" in out
+
+
+def test_fromto_decompose_import_falls_back_to_y_slot_for_capsule():
+    canon_def = {
+        "import_helper": "MjUtils::DecomposeFromTo",
+        "category_specifics": {"site": {
+            "z_slot_check": "(Type == EMjSiteType::Box || Type == EMjSiteType::Ellipsoid)"
+        }},
+    }
+    out = gen._canon_import_fromto_decompose(canon_def, element_name="site")
+    # The z_slot_check is element-specific; site uses EMjSiteType, not Geom.
+    assert "EMjSiteType::Box" in out
+    assert "EMjGeomType" not in out
+    # Without a matching category_specifics entry, the slot would be `false` (Y slot).
+    out_other = gen._canon_import_fromto_decompose(
+        {"import_helper": "MjUtils::DecomposeFromTo"},
+        element_name="unknown",
+    )
+    assert "bFTZSlot = false" in out_other
+
+
+def test_fromto_decompose_export_is_empty():
+    """The export path is a no-op: ``fromto`` decomposes to Pos/Quat/Size
+    on import; those properties carry the values through to export via
+    their own canonicalisations."""
+    assert gen._canon_export_fromto_decompose(canon_def={}) == ""
diff --git a/Scripts/codegen/tests/test_canon_registry.py b/Scripts/codegen/tests/test_canon_registry.py
new file mode 100644
index 0000000..f008188
--- /dev/null
+++ b/Scripts/codegen/tests/test_canon_registry.py
@@ -0,0 +1,64 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""Tests for the canonicalisation registry.
+
+The registry collapses the previous duplicated if-chains in
+``_canon_import_block`` / ``_canon_export_block`` into one dict.
+Adding a new canon means adding one ``Canonicalisation`` entry; an
+unknown canon name in the JSON rules still fires the same loud
+diagnostic instead of silently dropping its emission."""
+
+from __future__ import annotations
+
+import generate_ue_components as gen
+
+
+def test_registry_covers_every_canonicalisation_in_rules_json(real_rules):
+    """Every canonicalisation declared in codegen_rules.json must have
+    a registry entry. Mismatch fires the silent-drop diagnostic at
+    runtime — catch it here at test time instead."""
+    declared = set(real_rules.get("canonicalizations", {}).keys())
+    registered = set(gen._CANONICALISATIONS.keys())
+    missing = declared - registered
+    assert not missing, (
+        f"canonicalizations declared in rules JSON but not in the "
+        f"registry: {sorted(missing)}"
+    )
+
+
+def test_unknown_canon_fires_import_diagnostic():
+    out = gen._canon_import_block("nonexistent_canon", {}, element_name="geom")
+    assert out == ""
+    assert any("nonexistent_canon" in d.message and "import" in d.message
+               for d in gen._DIAGS_BUFFER.pending)
+
+
+def test_unknown_canon_fires_export_diagnostic():
+    out = gen._canon_export_block("nonexistent_canon", {})
+    assert out == ""
+    assert any("nonexistent_canon" in d.message and "export" in d.message
+               for d in gen._DIAGS_BUFFER.pending)
+
+
+def test_spatial_pose_import_is_one_liner_helper_call():
+    """Locks the trivial-shape canonicalisation so a regression in the
+    registry dispatch shows up directly."""
+    canon_def = {
+        "import_helper": "MjUtils::ReadVec3InMeters",
+        "emits_property": {"name": "Pos", "type": "FVector"},
+    }
+    out = gen._canon_import_block("spatial_pose", canon_def, "")
+    assert out.strip() == (
+        'MjUtils::ReadVec3InMeters(Node, TEXT("pos"), Pos, bOverride_Pos);'
+    )
+
+
+def test_canonicalisation_class_is_frozen():
+    """Registry entries must be frozen so a phase can't accidentally
+    swap an emitter at runtime."""
+    entry = next(iter(gen._CANONICALISATIONS.values()))
+    import dataclasses
+    assert dataclasses.is_dataclass(entry)
+    # FrozenInstanceError on attempted assignment.
+    import pytest
+    with pytest.raises(dataclasses.FrozenInstanceError):
+        entry.name = "mutated"  # type: ignore[misc]
diff --git a/Scripts/codegen/tests/test_canonicalization.py b/Scripts/codegen/tests/test_canonicalization.py
index e52b9a4..2fe93e2 100644
--- a/Scripts/codegen/tests/test_canonicalization.py
+++ b/Scripts/codegen/tests/test_canonicalization.py
@@ -7,36 +7,30 @@
 
 
 def test_orientation_absorbs_all_attrs(minimal_rules):
+    """Every orientation sibling (euler / axisangle / xyaxes / zaxis)
+    is absorbed into the canonical Quat UPROPERTY — none should leak
+    out as a standalone property."""
     attrs = ["quat", "euler", "axisangle", "xyaxes", "zaxis", "size"]
     out = emit_schema_for_attrs(attrs, minimal_rules, "geom", "Geom")
-    # Raw orientation attrs do NOT appear as per-attr properties.
-    for raw in ("Quat ", "Euler", "Axisangle", "Xyaxes", "Zaxis"):
-        # The canonical "Quat" property DOES appear once. Use the trailing
-        # space to disambiguate from `FQuat Quat`.
-        pass
-    # Canonical Quat is emitted once.
     assert "FQuat Quat" in out.properties_h
     assert "bOverride_Quat" in out.properties_h
-    # No per-attr Euler etc.
     assert "FQuat Euler" not in out.properties_h
     assert "AxisAngle " not in out.properties_h
     assert "Xyaxes " not in out.properties_h
 
 
-def test_orientation_import_uses_canonical_helper(minimal_rules):
+def test_orientation_roundtrips_through_canon_helpers(minimal_rules):
+    """A single ``quat`` schema attr drives both arms of the canon — the
+    import pulls through ``MjOrientationUtils::OrientationToMjQuat`` +
+    ``MjToUERotation``, and the export pushes through
+    ``UEToMjRotation``."""
     out = emit_schema_for_attrs(["quat"], minimal_rules, "geom", "Geom")
-    # Must invoke the canonicalisation helper, not a per-attr parser.
+    # Import arm.
     assert "MjOrientationUtils::OrientationToMjQuat(Node, CompilerSettings, TmpQuat)" in out.imports_cpp
-    # Must route the result through MjUtils::MjToUERotation so the UPROPERTY
-    # lands in UE coordinate frame (X/Z sign-flipped) plus wxyz->xyzw reorder.
     assert "Quat = MjUtils::MjToUERotation(TmpQuat)" in out.imports_cpp
     assert "bOverride_Quat = true" in out.imports_cpp
-
-
-def test_orientation_export_uses_canonical_helper(minimal_rules):
-    out = emit_schema_for_attrs(["quat"], minimal_rules, "geom", "Geom")
+    # Export arm.
     assert "MjUtils::UEToMjRotation(Quat, TmpQuat)" in out.exports_cpp
-    # Writes to Element->quat[0..3].
     assert "Element->quat[0] = TmpQuat[0]" in out.exports_cpp
     assert "Element->quat[3] = TmpQuat[3]" in out.exports_cpp
 
diff --git a/Scripts/codegen/tests/test_coverage_checks.py b/Scripts/codegen/tests/test_coverage_checks.py
new file mode 100644
index 0000000..099f7e0
--- /dev/null
+++ b/Scripts/codegen/tests/test_coverage_checks.py
@@ -0,0 +1,218 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""Behavioural coverage tests for the six drift checks that previously
+had no direct test — every check now has a "should fire" path and a
+"should be quiet" path. Each check fires through ``_diag_add`` with a
+specific ``source=`` tag so assertions can target the message bucket
+without depending on prose."""
+
+from __future__ import annotations
+
+import _codegen_checks as checks
+import generate_ue_components as gen
+
+
+def _pending_sources() -> list:
+    return [d.source for d in gen._DIAGS_BUFFER.pending]
+
+
+def _reset() -> None:
+    gen._reset_diags()
+
+
+# ---------- _check_top_level_elements_coverage --------------------------
+
+def test_top_level_elements_coverage_fires_for_unmodeled_element():
+    _reset()
+    schema = {"new_widget": {}, "joint": {}}
+    rules = {
+        "categories": {"joint": {"schema_common_block": "joint"}},
+        "intentionally_unmodeled_elements": {},
+    }
+    checks._check_top_level_elements_coverage(schema, rules)
+    assert any("new_widget" in d.message and d.source == "top_level_coverage"
+               for d in gen._DIAGS_BUFFER.pending)
+
+
+def test_top_level_elements_coverage_quiet_for_documented_element():
+    _reset()
+    schema = {"asset": {}, "joint": {}}
+    rules = {
+        "categories": {"joint": {"schema_common_block": "joint"}},
+        "intentionally_unmodeled_elements": {},
+    }
+    # 'asset' is in the container_keys allowlist.
+    checks._check_top_level_elements_coverage(schema, rules)
+    assert "top_level_coverage" not in _pending_sources()
+
+
+# ---------- _check_actuator_subtypes_coverage --------------------------
+
+def test_actuator_subtypes_coverage_fires_for_unmodeled_subtype():
+    _reset()
+    schema = {"actuator_types": {"newkind": []}}
+    rules = {"categories": {"actuator": {
+        "subtypes": [{"key": "motor"}, {"key": "position"}],
+    }}}
+    checks._check_actuator_subtypes_coverage(schema, rules)
+    assert any("newkind" in d.message and d.source == "actuator_subtypes_coverage"
+               for d in gen._DIAGS_BUFFER.pending)
+
+
+def test_actuator_subtypes_coverage_quiet_when_all_modeled():
+    _reset()
+    schema = {"actuator_types": {"motor": []}}
+    rules = {"categories": {"actuator": {
+        "subtypes": [{"key": "motor"}],
+    }}}
+    checks._check_actuator_subtypes_coverage(schema, rules)
+    assert "actuator_subtypes_coverage" not in _pending_sources()
+
+
+# ---------- _check_sensor_subtypes_coverage ----------------------------
+
+def test_sensor_subtypes_coverage_fires_for_unmodeled_subtype():
+    _reset()
+    schema = {"sensor_types": ["touch", "newsensor"]}
+    rules = {"categories": {"sensor": {
+        "subtypes": [{"key": "touch"}],
+    }}}
+    checks._check_sensor_subtypes_coverage(schema, rules)
+    assert any("newsensor" in d.message and d.source == "sensor_subtypes_coverage"
+               for d in gen._DIAGS_BUFFER.pending)
+
+
+def test_sensor_subtypes_coverage_quiet_when_all_modeled():
+    _reset()
+    schema = {"sensor_types": ["touch"]}
+    rules = {"categories": {"sensor": {
+        "subtypes": [{"key": "touch"}],
+    }}}
+    checks._check_sensor_subtypes_coverage(schema, rules)
+    assert "sensor_subtypes_coverage" not in _pending_sources()
+
+
+# ---------- _check_setto_param_coverage --------------------------------
+
+def test_setto_param_coverage_fires_for_unmapped_param():
+    _reset()
+    schema = {"actuator_common": {"attrs": ["kp"]}, "actuator_types": {"position": ["kv"]}}
+    rules = {
+        "categories": {"actuator": {
+            "schema_common_block": "actuator_common",
+            "subtype_setto": {"position": {"call": "mjs_setToPosition"}},
+        }},
+        "setto_param_defaults": {},
+    }
+    mjspec = {"setto_functions": {"mjs_setToPosition": {
+        "params": [
+            {"name": "kp",        "c_type": "double", "array_dim": None},
+            {"name": "kv",        "c_type": "double", "array_dim": None},
+            {"name": "noveltie",  "c_type": "double", "array_dim": None},
+        ],
+    }}}
+    checks._check_setto_param_coverage(schema, rules, mjspec)
+    assert any("noveltie" in d.message and d.source == "setto_param_coverage"
+               for d in gen._DIAGS_BUFFER.pending)
+
+
+def test_setto_param_coverage_quiet_when_all_mapped():
+    _reset()
+    schema = {"actuator_common": {"attrs": ["kp"]}, "actuator_types": {"position": ["kv"]}}
+    rules = {
+        "categories": {"actuator": {
+            "schema_common_block": "actuator_common",
+            "subtype_setto": {"position": {"call": "mjs_setToPosition"}},
+        }},
+        "setto_param_defaults": {"mjs_setToPosition": {"inheritrange": "0.0"}},
+    }
+    mjspec = {"setto_functions": {"mjs_setToPosition": {
+        "params": [
+            {"name": "kp",           "c_type": "double", "array_dim": None},
+            {"name": "kv",           "c_type": "double", "array_dim": None},
+            {"name": "inheritrange", "c_type": "double", "array_dim": None},
+        ],
+    }}}
+    checks._check_setto_param_coverage(schema, rules, mjspec)
+    assert "setto_param_coverage" not in _pending_sources()
+
+
+# ---------- _check_mjs_struct_field_drift ------------------------------
+
+def test_mjs_struct_field_drift_fires_for_uncovered_field():
+    _reset()
+    schema = {"joint_common": ["stiffness"]}
+    rules = {
+        "categories": {"joint": {
+            "schema_common_block": "joint_common",
+            "mjs_struct": "mjsJoint",
+        }},
+        "intentionally_unmodeled_mjs_fields": {},
+    }
+    mjspec = {"structs": {"mjsJoint": ["stiffness", "mystery_field"]}}
+    checks._check_mjs_struct_field_drift(schema, rules, mjspec)
+    assert any("mystery_field" in d.message and d.source == "mjs_field_drift"
+               for d in gen._DIAGS_BUFFER.pending)
+
+
+def test_mjs_struct_field_drift_quiet_when_all_modeled():
+    _reset()
+    schema = {"joint_common": ["stiffness"]}
+    rules = {
+        "categories": {"joint": {
+            "schema_common_block": "joint_common",
+            "mjs_struct": "mjsJoint",
+        }},
+        "intentionally_unmodeled_mjs_fields": {},
+    }
+    mjspec = {"structs": {"mjsJoint": ["stiffness", "name"]}}  # 'name' is structural
+    checks._check_mjs_struct_field_drift(schema, rules, mjspec)
+    assert "mjs_field_drift" not in _pending_sources()
+
+
+# ---------- _check_generated_enum_coverage -----------------------------
+
+def test_generated_enum_coverage_fires_for_unmapped_member():
+    _reset()
+    rules = {"generated_enums": {"MjOptionEnums": {
+        "enums": [{
+            "ue_name": "EMjIntegrator",
+            "from_mj_enum": "mjtIntegrator",
+            "ue_member_from_mj": {"mjINT_EULER": "Euler"},
+            # mjINT_RK4 missing from the rule -> should diag.
+        }],
+    }}}
+    mjspec = {"enums": {"mjtIntegrator": {"mjINT_EULER": 0, "mjINT_RK4": 1}}}
+    checks._check_generated_enum_coverage(rules, mjspec)
+    assert any("mjINT_RK4" in d.message and d.source == "enum_drift"
+               for d in gen._DIAGS_BUFFER.pending)
+
+
+def test_generated_enum_coverage_quiet_when_all_mapped():
+    _reset()
+    rules = {"generated_enums": {"MjOptionEnums": {
+        "enums": [{
+            "ue_name": "EMjIntegrator",
+            "from_mj_enum": "mjtIntegrator",
+            "ue_member_from_mj": {"mjINT_EULER": "Euler", "mjINT_RK4": "RK4"},
+        }],
+    }}}
+    mjspec = {"enums": {"mjtIntegrator": {"mjINT_EULER": 0, "mjINT_RK4": 1}}}
+    checks._check_generated_enum_coverage(rules, mjspec)
+    assert "enum_drift" not in _pending_sources()
+
+
+def test_generated_enum_coverage_fires_when_from_mj_enum_unknown():
+    """Snapshot drift: a from_mj_enum that's not in the projected
+    mjspec must produce a diagnostic instead of a silent skip."""
+    _reset()
+    rules = {"generated_enums": {"MjOptionEnums": {
+        "enums": [{
+            "ue_name": "EMjGone",
+            "from_mj_enum": "mjtGoneRenamed",
+            "ue_member_from_mj": {"mjGONE_X": "X"},
+        }],
+    }}}
+    mjspec = {"enums": {"mjtIntegrator": {"mjINT_EULER": 0}}}
+    checks._check_generated_enum_coverage(rules, mjspec)
+    assert any("mjtGoneRenamed" in d.message and d.source == "enum_drift"
+               for d in gen._DIAGS_BUFFER.pending)
diff --git a/Scripts/codegen/tests/test_dry_run.py b/Scripts/codegen/tests/test_dry_run.py
index 3f1eb7b..8a05fcb 100644
--- a/Scripts/codegen/tests/test_dry_run.py
+++ b/Scripts/codegen/tests/test_dry_run.py
@@ -15,12 +15,6 @@
 _PLUGIN_ROOT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
 
 
-@pytest.fixture(scope="session")
-def real_mjspec():
-    with open(os.path.join(_PLUGIN_ROOT, "Scripts", "mjspec_snapshot.json"), "r") as f:
-        return json.load(f)
-
-
 def test_collect_all_writes_includes_expected_categories(real_schema, real_rules, real_mjxmacro, real_mjspec):
     from generate_ue_components import collect_all_writes
     # The plugin's actual Public/Private roots (read-only — we don't write).
diff --git a/Scripts/codegen/tests/test_emit_enum_switch.py b/Scripts/codegen/tests/test_emit_enum_switch.py
new file mode 100644
index 0000000..34da527
--- /dev/null
+++ b/Scripts/codegen/tests/test_emit_enum_switch.py
@@ -0,0 +1,110 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""Tests for the ``emit_enum_switch`` codegen helper.
+
+The helper backs three sites: xml_enum exports (single-line cases +
+``default: break;``), objtype dispatch (multi-key groups + multi-line
+default), and geom FinalType resolution (single-line cases, no
+default). Each shape is locked here so a future tweak to the helper
+can't silently break byte-identity on the emitted .cpp."""
+
+from __future__ import annotations
+
+import generate_ue_components as gen
+
+
+def test_single_line_case_shape_matches_xml_enum_export():
+    """Single ``str`` member → one-line ``case Enum::M: <body>; break;``
+    — the shape every xml_enum_export emits."""
+    out = gen.emit_enum_switch(
+        "JointType", "EMjJointType",
+        [
+            ("Hinge", "Element->type = (mjtJoint)mjJNT_HINGE"),
+            ("Slide", "Element->type = (mjtJoint)mjJNT_SLIDE"),
+        ],
+        indent="    ",
+        default_break=True,
+    )
+    expected = (
+        "    switch (JointType)\n"
+        "    {\n"
+        "        case EMjJointType::Hinge: Element->type = (mjtJoint)mjJNT_HINGE; break;\n"
+        "        case EMjJointType::Slide: Element->type = (mjtJoint)mjJNT_SLIDE; break;\n"
+        "        default: break;\n"
+        "    }\n"
+    )
+    assert out == expected
+
+
+def test_multi_key_group_stacks_labels_with_multiline_body():
+    """A list-of-strings member stacks case labels and emits body +
+    break on indented separate lines — the shape objtype_dispatch
+    needs for ``case Body:`` / ``case XBody:`` collapsing to one expr."""
+    out = gen.emit_enum_switch(
+        "ObjType", "EMjObjType",
+        [
+            (["Body", "XBody"], "Target = mjOBJ_BODY"),
+            ("Geom",            "Target = mjOBJ_GEOM"),
+        ],
+        indent="    ",
+        default_body="Target = mjOBJ_UNKNOWN",
+    )
+    expected = (
+        "    switch (ObjType)\n"
+        "    {\n"
+        "        case EMjObjType::Body:\n"
+        "        case EMjObjType::XBody:\n"
+        "            Target = mjOBJ_BODY;\n"
+        "            break;\n"
+        "        case EMjObjType::Geom: Target = mjOBJ_GEOM; break;\n"
+        "        default:\n"
+        "            Target = mjOBJ_UNKNOWN;\n"
+        "            break;\n"
+        "    }\n"
+    )
+    assert out == expected
+
+
+def test_no_default_when_neither_flag_set():
+    """geom_final_type emits no default branch — the helper must
+    omit it cleanly."""
+    out = gen.emit_enum_switch(
+        "Type", "EMjGeomType",
+        [("Sphere", "FinalType = mjGEOM_SPHERE")],
+        indent="        ",
+    )
+    expected = (
+        "        switch (Type)\n"
+        "        {\n"
+        "            case EMjGeomType::Sphere: FinalType = mjGEOM_SPHERE; break;\n"
+        "        }\n"
+    )
+    assert out == expected
+
+
+def test_default_body_takes_priority_over_default_break():
+    """When both are set, the multi-line default wins. Keeps caller
+    intent obvious — ``default_break`` is the simple short-form."""
+    out = gen.emit_enum_switch(
+        "X", "EMjX",
+        [("A", "y = 1")],
+        default_break=True,
+        default_body="y = 0",
+    )
+    assert "default:\n" in out
+    assert "default: break;" not in out
+
+
+def test_indent_propagates_to_case_labels_and_body():
+    """Case labels sit at ``indent + 4 spaces``; multi-line case bodies
+    at ``indent + 8 spaces``. The lock prevents indentation drift."""
+    out = gen.emit_enum_switch(
+        "X", "EMjX",
+        [(["A", "B"], "z = 1")],
+        indent="            ",  # 12 spaces
+    )
+    # switch line at 12 spaces.
+    assert out.startswith("            switch (X)\n")
+    # case labels at 12 + 4 = 16 spaces.
+    assert "                case EMjX::A:\n" in out
+    # body at 12 + 8 = 20 spaces.
+    assert "                    z = 1;\n" in out
diff --git a/Scripts/codegen/tests/test_emit_property.py b/Scripts/codegen/tests/test_emit_property.py
index f636790..3b66f3d 100644
--- a/Scripts/codegen/tests/test_emit_property.py
+++ b/Scripts/codegen/tests/test_emit_property.py
@@ -8,22 +8,12 @@
 from generate_ue_components import (
     EmittedSchema,
     emit_schema_for_attrs,
-    pascal_case,
-    override_toggle_name,
 )
 
 
-def test_schema_verbatim_name():
-    """Codegen uses MuJoCo schema names AS-IS (no PascalCase, no renames)."""
-    assert pascal_case("limited") == "limited"
-    assert pascal_case("ctrlrange") == "ctrlrange"
-    assert pascal_case("solreflimit") == "solreflimit"
-    assert pascal_case("springref") == "springref"
-
-
-def test_override_toggle_name():
-    assert override_toggle_name("limited") == "bOverride_limited"
-    assert override_toggle_name("ctrlrange") == "bOverride_ctrlrange"
+# override_toggle_name is exercised everywhere emit_schema_for_attrs runs;
+# the cross-category guarantee is pinned in
+# test_behavioral_emit_guarantees::test_override_toggle_name_matches_uproperty_for_all_categories.
 
 
 def test_emit_basic_property(minimal_rules):
diff --git a/Scripts/codegen/tests/test_emitter_capabilities.py b/Scripts/codegen/tests/test_emitter_capabilities.py
new file mode 100644
index 0000000..24d8515
--- /dev/null
+++ b/Scripts/codegen/tests/test_emitter_capabilities.py
@@ -0,0 +1,106 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""
+Tests for opt-in emitter capabilities in generate_ue_components.py:
+
+1. ``_resolve_value_map``: returns the rule's literal ``value_map``;
+   missing ``value_map`` raises.
+2. ``xml_enum_attrs.emit_property_decl: true``: codegen emits the UE
+   enum UPROPERTY decl into the schema PROPERTIES block.
+
+Each test uses synthetic rules to exercise its capability in isolation.
+"""
+
+from __future__ import annotations
+
+import os
+import sys
+
+_HERE = os.path.dirname(os.path.abspath(__file__))
+_CODEGEN_DIR = os.path.dirname(_HERE)
+if _CODEGEN_DIR not in sys.path:
+    sys.path.insert(0, _CODEGEN_DIR)
+
+import pytest  # noqa: E402
+
+from generate_ue_components import (  # noqa: E402
+    _resolve_value_map,
+    _emit_xml_enum_import,
+    emit_schema_for_attrs,
+)
+
+
+# ----- value_map resolver ----------------------------------------------
+
+def test_value_map_returned_verbatim():
+    """The resolver returns the rule's literal ``value_map`` unchanged."""
+    enum_def = {
+        "ue_property": "Type",
+        "ue_enum_type": "EMjJointType",
+        "value_map":   {"hinge": ["Hinge", "mjJNT_HINGE"]},
+    }
+    resolved = _resolve_value_map("type", enum_def)
+    assert resolved == {"hinge": ["Hinge", "mjJNT_HINGE"]}
+
+
+def test_value_map_resolver_errors_when_missing():
+    enum_def = {"ue_property": "X", "ue_enum_type": "E"}
+    with pytest.raises(RuntimeError, match="missing value_map"):
+        _resolve_value_map("attr", enum_def)
+
+
+def test_xml_enum_import_emits_expected_blocks():
+    enum_def = {
+        "ue_property": "Type",
+        "ue_enum_type": "EMjJointType",
+        "value_map":   {"hinge": ["Hinge", "mjJNT_HINGE"], "slide": ["Slide", "mjJNT_SLIDE"]},
+    }
+    out = _emit_xml_enum_import("type", enum_def)
+    assert "Type = EMjJointType::Hinge" in out
+    assert "Type = EMjJointType::Slide" in out
+    assert "bOverride_Type = true" in out
+
+
+# ----- emit_property_decl opt-in ---------------------------------------
+
+def test_emit_property_decl_off_skips_uproperty():
+    """Default behaviour: xml_enum_attrs entries do NOT emit the UE
+    UPROPERTY decl (the .h hand-declares it)."""
+    rules = {
+        "element_rules": {
+            "joint": {
+                "xml_enum_attrs": {
+                    "type": {
+                        "ue_property": "Type",
+                        "ue_enum_type": "EMjJointType",
+                        "value_map": {"hinge": ["Hinge", "mjJNT_HINGE"]},
+                    }
+                }
+            }
+        }
+    }
+    out = emit_schema_for_attrs([], rules, element_name="joint", category_label="Joint")
+    assert "EMjJointType" not in out.properties_h
+
+
+def test_emit_property_decl_on_emits_uproperty_pair():
+    """With opt-in, codegen emits both the override toggle and the
+    EMjX UPROPERTY using the standard _emit_uproperty shape."""
+    rules = {
+        "element_rules": {
+            "joint": {
+                "xml_enum_attrs": {
+                    "type": {
+                        "ue_property": "Type",
+                        "ue_enum_type": "EMjJointType",
+                        "emit_property_decl": True,
+                        "value_map": {"hinge": ["Hinge", "mjJNT_HINGE"]},
+                    }
+                }
+            }
+        }
+    }
+    out = emit_schema_for_attrs([], rules, element_name="joint", category_label="Joint")
+    assert "EMjJointType Type" in out.properties_h
+    assert "bOverride_Type" in out.properties_h
+
+
diff --git a/Scripts/codegen/tests/test_enum_scrape.py b/Scripts/codegen/tests/test_enum_scrape.py
new file mode 100644
index 0000000..c405501
--- /dev/null
+++ b/Scripts/codegen/tests/test_enum_scrape.py
@@ -0,0 +1,53 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""Tests for the mjt* enum surface the codegen consumes.
+
+mjt* enums come from the clang-AST introspect snapshot — libclang
+gives us the real member values and handles every ``typedef enum
+mjtX_ { ... } mjtX;`` shape uniformly. The shipped snapshot has to
+carry every enum the codegen rules reference; the cross-check below
+is the guard."""
+
+from __future__ import annotations
+
+import json
+import os
+import sys
+
+_HERE = os.path.dirname(os.path.abspath(__file__))
+_CODEGEN_DIR = os.path.dirname(_HERE)
+if _CODEGEN_DIR not in sys.path:
+    sys.path.insert(0, _CODEGEN_DIR)
+
+
+def _load_projected_mjspec():
+    """Load introspect + project into the legacy mjspec shape via the
+    same helper the runtime uses. Mirrors the conftest fixture."""
+    import generate_ue_components as gen
+    plugin_root = os.path.normpath(os.path.join(_HERE, "..", "..", ".."))
+    snap_path = os.path.join(
+        plugin_root, "Scripts", "codegen", "snapshots",
+        "introspect_snapshot.json",
+    )
+    with open(snap_path, "r", encoding="utf-8") as f:
+        introspect = json.load(f)
+    return gen._mjspec_from_introspect(introspect)
+
+
+def test_real_snapshot_has_expected_enums():
+    """Introspect must carry every mjt* enum the codegen rules drift
+    checks rely on (the cross-walk between value_map entries and the
+    enum members the snapshot reports for that mjt* type)."""
+    mjspec = _load_projected_mjspec()
+    enums = mjspec.get("enums", {})
+    for expected in (
+        "mjtJoint",      # joint.type
+        "mjtGeom",       # geom.type
+        "mjtSensor",     # sensor types
+        "mjtObj",        # objtype / reftype
+        "mjtTrn",        # actuator transmission
+        "mjtIntegrator", # mjOption.integrator
+        "mjtCone",       # mjOption.cone
+        "mjtSolver",     # mjOption.solver
+    ):
+        assert expected in enums, f"missing enum {expected} from snapshot"
+        assert len(enums[expected]) > 0, f"enum {expected} has no members"
diff --git a/Scripts/codegen/tests/test_expand_stride_guard.py b/Scripts/codegen/tests/test_expand_stride_guard.py
new file mode 100644
index 0000000..58d156d
--- /dev/null
+++ b/Scripts/codegen/tests/test_expand_stride_guard.py
@@ -0,0 +1,54 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""Locks the ``_expand_stride`` guard. ``MJ_M(...)`` lowers to
+``m->...``; numeric and ``mjN*`` strides pass through unchanged. Any
+``MJ_*( ... )`` macro that survives is an unhandled shape and must
+surface a diagnostic before the generated MjBind.h fails to compile."""
+
+from __future__ import annotations
+
+import generate_ue_components as gen
+
+
+def test_mj_m_macro_lowers_to_pointer_field():
+    assert gen._expand_stride("MJ_M(nuser_jnt)") == "m->nuser_jnt"
+
+
+def test_numeric_stride_passes_through_unchanged():
+    for s in ("1", "3", "9"):
+        assert gen._expand_stride(s) == s
+
+
+def test_mj_n_constant_passes_through_unchanged():
+    assert gen._expand_stride("mjNREF") == "mjNREF"
+    assert gen._expand_stride("mjNBIAS") == "mjNBIAS"
+
+
+def test_compound_expression_expands_and_keeps_other_tokens():
+    """Real MuJoCo strides occasionally combine ``MJ_M(...)*N`` for
+    derived per-element block sizes. The guard must NOT fire because
+    the substitution removes the only MJ_ macro."""
+    assert gen._expand_stride("MJ_M(nuser_geom)*3") == "m->nuser_geom*3"
+    assert len(gen._DIAGS_BUFFER.pending) == 0
+
+
+def test_unknown_macro_fires_guard_diagnostic():
+    """A future MuJoCo bump that introduces e.g. ``MJ_D(foo)`` must
+    not silently land verbatim in MjBind.h."""
+    out = gen._expand_stride("MJ_D(foo)")
+    assert "MJ_D(foo)" in out
+    assert any("unhandled MuJoCo macro" in d.message and "MJ_D(foo)" in d.message
+               for d in gen._DIAGS_BUFFER.pending)
+
+
+def test_multiple_unknown_macros_listed_together():
+    gen._expand_stride("MJ_D(a) + MJ_V(b)")
+    diags = [d.message for d in gen._DIAGS_BUFFER.pending]
+    assert any("MJ_D(a)" in m and "MJ_V(b)" in m for m in diags), diags
+
+
+def test_known_and_unknown_mixed_still_fires_for_unknown():
+    """Known macro expanded, unknown surfaces a diag."""
+    out = gen._expand_stride("MJ_M(nuser_jnt) * MJ_D(foo)")
+    assert "m->nuser_jnt" in out
+    assert "MJ_D(foo)" in out
+    assert any("MJ_D(foo)" in d.message for d in gen._DIAGS_BUFFER.pending)
diff --git a/Scripts/codegen/tests/test_find_matching_brace.py b/Scripts/codegen/tests/test_find_matching_brace.py
new file mode 100644
index 0000000..61c2e70
--- /dev/null
+++ b/Scripts/codegen/tests/test_find_matching_brace.py
@@ -0,0 +1,80 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""Locks ``_find_matching_brace`` — the brace-aware extractor that
+backs the banner-overwrite audit. The previous ``\\{([^}]*)\\}`` regex
+truncated at the first inner ``}``, silently underreporting extra ctor
+content when a real ctor body contained a range-for loop or nested
+if. The extractor must track string literals and comments so a ``{``
+inside ``TEXT("...")`` does not shift the depth count."""
+
+from __future__ import annotations
+
+import generate_ue_components as gen
+
+
+def test_finds_simple_matching_brace():
+    text = "x { hello } y"
+    open_idx = text.index("{")
+    close_idx = gen._find_matching_brace(text, open_idx)
+    assert close_idx is not None
+    assert text[close_idx] == "}"
+    assert text[open_idx + 1:close_idx] == " hello "
+
+
+def test_handles_nested_braces():
+    text = "ctor() { if (x) { do(); } else { other(); } }"
+    open_idx = text.index("{")
+    close_idx = gen._find_matching_brace(text, open_idx)
+    # close_idx must be the FINAL '}', not the first inner one.
+    assert close_idx == len(text) - 1
+
+
+def test_ignores_braces_in_string_literal():
+    text = 'fn() { Print(TEXT("{nested}")); }'
+    open_idx = text.index("{")
+    close_idx = gen._find_matching_brace(text, open_idx)
+    # The string contains '{' and '}' but they must NOT change depth;
+    # the matcher should land on the final outer '}'.
+    assert close_idx == len(text) - 1
+
+
+def test_ignores_braces_in_char_literal():
+    text = "fn() { char c = '{'; print(c); }"
+    open_idx = text.index("{")
+    close_idx = gen._find_matching_brace(text, open_idx)
+    assert close_idx == len(text) - 1
+
+
+def test_ignores_braces_in_line_comment():
+    text = "fn() { // tail {\n    real(); }"
+    open_idx = text.index("{")
+    close_idx = gen._find_matching_brace(text, open_idx)
+    assert close_idx == len(text) - 1
+
+
+def test_ignores_braces_in_block_comment():
+    text = "fn() { /* tail { in comment */ real(); }"
+    open_idx = text.index("{")
+    close_idx = gen._find_matching_brace(text, open_idx)
+    assert close_idx == len(text) - 1
+
+
+def test_returns_none_for_unmatched_open():
+    """If the input is malformed (open without a matching close), the
+    helper returns None — caller decides whether that's a diag or a
+    silent skip."""
+    assert gen._find_matching_brace("fn() { unclosed", 5) is None
+
+
+def test_returns_none_when_index_is_not_open_brace():
+    assert gen._find_matching_brace("x }", 1) is None
+    assert gen._find_matching_brace("x", 5) is None  # off-end
+
+
+def test_escape_sequences_in_string_dont_terminate_early():
+    """A ``\\"`` inside a string literal must not close the string
+    early — the matcher would otherwise re-enter brace-counting mode
+    inside the literal and mis-count."""
+    text = r'fn() { print("a\"b{c"); real(); }'
+    open_idx = text.index("{")
+    close_idx = gen._find_matching_brace(text, open_idx)
+    assert close_idx == len(text) - 1
diff --git a/Scripts/codegen/tests/test_geom_final_type.py b/Scripts/codegen/tests/test_geom_final_type.py
new file mode 100644
index 0000000..b0f40af
--- /dev/null
+++ b/Scripts/codegen/tests/test_geom_final_type.py
@@ -0,0 +1,85 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""Tests for the ``geom_final_type`` rule shape — resolves an mjsGeom
+type at compile time + emits the mesh-name export under the right
+EMjGeomType condition."""
+
+from __future__ import annotations
+
+import generate_ue_components as gen
+from generate_ue_components import _emit_geom_final_type_block
+
+
+def _geom_value_map_rules():
+    """Minimal cat/element rules that exercise the FinalType switch."""
+    cat_rules = {
+        "geom_final_type": {
+            "xml_enum_ref": "type",
+            "override_field": "bOverride_Type",
+            "default_lookup": "Default->Type",
+            "default_fallback": "mjGEOM_SPHERE",
+            "name_export_for": "mjGEOM_MESH",
+            "name_field": "MeshName",
+            "name_setter": "Element->meshname.assign",
+            "name_target": "*Element",
+        },
+    }
+    element_rules = {
+        "xml_enum_attrs": {
+            "type": {
+                "ue_property": "Type",
+                "ue_enum": "EMjGeomType",
+                "ue_enum_type": "EMjGeomType",
+                "override_toggle": "bOverride_Type",
+                "default_lookup": "Default->Type",
+                "value_map": {
+                    "sphere": ["Sphere", "mjGEOM_SPHERE"],
+                    "box":    ["Box",    "mjGEOM_BOX"],
+                    "mesh":   ["Mesh",   "mjGEOM_MESH"],
+                },
+            },
+        },
+    }
+    return cat_rules, element_rules
+
+
+def test_geom_final_type_emits_full_block():
+    cat_rules, element_rules = _geom_value_map_rules()
+    out = _emit_geom_final_type_block(cat_rules, element_rules)
+    assert out is not None
+    assert "FinalType" in out
+    assert "case EMjGeomType::Sphere: FinalType = mjGEOM_SPHERE;" in out
+    assert "case EMjGeomType::Box: FinalType = mjGEOM_BOX;" in out
+    assert "case EMjGeomType::Mesh: FinalType = mjGEOM_MESH;" in out
+    assert "MeshName" in out
+    assert len(gen._DIAGS_BUFFER.pending) == 0
+
+
+def test_geom_final_type_returns_none_when_no_rule():
+    assert _emit_geom_final_type_block({}, {}) is None
+    assert len(gen._DIAGS_BUFFER.pending) == 0
+
+
+def test_geom_final_type_diagnoses_missing_enum_def():
+    cat_rules = {
+        "geom_final_type": {
+            "xml_enum_ref": "nonexistent",
+            "default_fallback": "mjGEOM_SPHERE",
+            "first_mj_const_token": "mjGEOM_PLANE",
+            "name_for": "mjGEOM_MESH",
+            "name_field": "MeshName",
+            "name_setter": "Element->meshname.assign",
+            "name_target": "*Element",
+        },
+    }
+    out = _emit_geom_final_type_block(cat_rules, {"xml_enum_attrs": {}})
+    assert out is None
+    assert any("nonexistent" in d.message for d in gen._DIAGS_BUFFER.pending)
+
+
+def test_geom_final_type_diagnoses_empty_value_map():
+    cat_rules, element_rules = _geom_value_map_rules()
+    # Wipe out the value_map and provide no mjspec to fall back to.
+    element_rules["xml_enum_attrs"]["type"]["value_map"] = {}
+    out = _emit_geom_final_type_block(cat_rules, element_rules)
+    assert out is None
+    assert any("empty value_map" in d.message for d in gen._DIAGS_BUFFER.pending)
diff --git a/Scripts/codegen/tests/test_hand_enum_drift.py b/Scripts/codegen/tests/test_hand_enum_drift.py
new file mode 100644
index 0000000..9c8d168
--- /dev/null
+++ b/Scripts/codegen/tests/test_hand_enum_drift.py
@@ -0,0 +1,234 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""Tests for the hand-enum drift checks: ``_scan_hand_enums`` scrapes
+URLab ``EMj*`` enum members from Source/, and ``_check_hand_enum_drift``
+cross-references them against MuJoCo's ``mjt*`` enums and the rules'
+``value_map`` UE-member references."""
+
+from __future__ import annotations
+
+import generate_ue_components as gen
+import _codegen_checks as _checks
+gen._scan_hand_enums = _checks._scan_hand_enums
+
+
+def _write_enum_header(tmp_path, filename: str, enum_text: str):
+    p = tmp_path / filename
+    p.write_text(enum_text, encoding="utf-8")
+    return p
+
+
+# ---------- _scan_hand_enums -----------------------------------------------
+
+def test_scan_hand_enums_parses_umeta_members(tmp_path):
+    """UMETA(DisplayName=...) on each member must NOT hide the member
+    from the scanner (was the original parser bug — EMjCameraTrackingMode
+    lost TrackCom and TargetBodyCom because the per-line regex required
+    the line to end at the member)."""
+    _write_enum_header(tmp_path, "Sample.h", """
+        UENUM(BlueprintType)
+        enum class EMjSample : uint8
+        {
+            Fixed         UMETA(DisplayName = "Fixed"),
+            Track         UMETA(DisplayName = "Track"),
+            TrackCom      UMETA(DisplayName = "Track (Centre of Mass)"),
+            TargetBody    UMETA(DisplayName = "Target Body"),
+            TargetBodyCom UMETA(DisplayName = "Target Body (Centre of Mass)"),
+        };
+    """)
+    enums = gen._scan_hand_enums(str(tmp_path))
+    assert "EMjSample" in enums
+    assert enums["EMjSample"] == [
+        "Fixed", "Track", "TrackCom", "TargetBody", "TargetBodyCom",
+    ]
+
+
+def test_scan_hand_enums_handles_explicit_values(tmp_path):
+    """Explicit ``= N`` initialisers must not break member capture."""
+    _write_enum_header(tmp_path, "WithVals.h", """
+        enum class EMjWithVals : uint8
+        {
+            Connect     = 0,
+            Weld        = 1,
+            Joint       = 2,
+        };
+    """)
+    enums = gen._scan_hand_enums(str(tmp_path))
+    assert enums["EMjWithVals"] == ["Connect", "Weld", "Joint"]
+
+
+# ---------- _check_hand_enum_drift -----------------------------------------
+
+def test_hand_enum_drift_diagnoses_subtype_enum_value_typo(tmp_path):
+    _write_enum_header(tmp_path, "MjJoint.h", """
+        enum class EMjJointType : uint8 { Hinge, Slide, Ball };
+    """)
+    rules = {
+        "categories": {
+            "joint": {
+                "type_enum_name": "EMjJointType",
+                "subtypes": [
+                    {"key": "hinge", "enum_value": "Hinge"},
+                    {"key": "screw", "enum_value": "Screwd"},  # typo
+                ],
+            },
+        },
+    }
+    gen._check_hand_enum_drift({}, rules, None, str(tmp_path))
+    assert any("Screwd" in d.message and "EMjJointType" in d.message
+               for d in gen._DIAGS_BUFFER.pending)
+
+
+def test_hand_enum_drift_diagnoses_value_map_ue_member_typo(tmp_path):
+    _write_enum_header(tmp_path, "MjGeom.h", """
+        enum class EMjGeomType : uint8 { Sphere, Box, Mesh };
+    """)
+    rules = {
+        "categories": {},
+        "element_rules": {
+            "geom": {
+                "xml_enum_attrs": {
+                    "type": {
+                        "ue_enum_type": "EMjGeomType",
+                        "value_map": {
+                            "sphere": ["Sphere", "mjGEOM_SPHERE"],
+                            "box":    ["Bxox",   "mjGEOM_BOX"],  # typo
+                        },
+                    },
+                },
+            },
+        },
+    }
+    gen._check_hand_enum_drift({}, rules, None, str(tmp_path))
+    assert any("Bxox" in d.message and "EMjGeomType" in d.message
+               for d in gen._DIAGS_BUFFER.pending)
+
+
+def test_hand_enum_drift_surfaces_new_mj_enum_value(tmp_path):
+    """A new mjJNT_SCREW upstream with no value_map entry fires a
+    diagnostic — closes the silent-runtime-fallthrough class."""
+    _write_enum_header(tmp_path, "MjJoint.h", """
+        enum class EMjJointType : uint8 { Hinge, Slide, Ball };
+    """)
+    rules = {
+        "categories": {},
+        "element_rules": {
+            "joint": {
+                "xml_enum_attrs": {
+                    "type": {
+                        "ue_enum_type": "EMjJointType",
+                        "mjs_cast": "mjtJoint",
+                        "value_map": {
+                            "hinge": ["Hinge", "mjJNT_HINGE"],
+                            "slide": ["Slide", "mjJNT_SLIDE"],
+                            "ball":  ["Ball",  "mjJNT_BALL"],
+                        },
+                    },
+                },
+            },
+        },
+    }
+    mjspec = {
+        "enums": {
+            "mjtJoint": ["mjJNT_FREE", "mjJNT_BALL", "mjJNT_SLIDE",
+                         "mjJNT_HINGE", "mjJNT_SCREW"],
+        },
+    }
+    gen._check_hand_enum_drift({}, rules, mjspec, str(tmp_path))
+    msgs = [d.message for d in gen._DIAGS_BUFFER.pending]
+    assert any("mjJNT_SCREW" in m for m in msgs), msgs
+    assert any("mjJNT_FREE" in m for m in msgs), msgs
+
+
+def test_hand_enum_drift_respects_intentional_skips(tmp_path):
+    _write_enum_header(tmp_path, "MjActuator.h", """
+        enum class EMjGainType : uint8 { Fixed, Affine, Muscle };
+    """)
+    rules = {
+        "categories": {},
+        "intentionally_unmapped_mj_enum_values": {
+            "mjtGain": ["mjGAIN_DCMOTOR"],
+        },
+        "element_rules": {
+            "actuator": {
+                "xml_enum_attrs": {
+                    "gaintype": {
+                        "ue_enum_type": "EMjGainType",
+                        "mjs_cast": "mjtGain",
+                        "value_map": {
+                            "fixed":   ["Fixed",   "mjGAIN_FIXED"],
+                            "affine":  ["Affine",  "mjGAIN_AFFINE"],
+                            "muscle":  ["Muscle",  "mjGAIN_MUSCLE"],
+                        },
+                    },
+                },
+            },
+        },
+    }
+    mjspec = {
+        "enums": {
+            "mjtGain": ["mjGAIN_FIXED", "mjGAIN_AFFINE", "mjGAIN_MUSCLE",
+                        "mjGAIN_USER", "mjGAIN_DCMOTOR"],
+        },
+    }
+    gen._check_hand_enum_drift({}, rules, mjspec, str(tmp_path))
+    msgs = [d.message for d in gen._DIAGS_BUFFER.pending]
+    # DCMOTOR is in the skip list -> no diag.
+    assert not any("mjGAIN_DCMOTOR" in m for m in msgs)
+    # USER is not -> diag fires.
+    assert any("mjGAIN_USER" in m for m in msgs), msgs
+
+
+def test_hand_enum_drift_per_element_overrides(tmp_path):
+    """Same mjt* enum (mjtGeom) admits different subsets for geom vs
+    site. The __per_element__ override must scope correctly."""
+    _write_enum_header(tmp_path, "MjGeom.h", """
+        enum class EMjGeomType : uint8 { Sphere, Box, Plane, Mesh };
+    """)
+    _write_enum_header(tmp_path, "MjSite.h", """
+        enum class EMjSiteType : uint8 { Sphere, Box };
+    """)
+    rules = {
+        "categories": {},
+        "intentionally_unmapped_mj_enum_values": {
+            "__per_element__": {
+                "site.type": ["mjGEOM_PLANE", "mjGEOM_MESH"],
+            },
+        },
+        "element_rules": {
+            "geom": {
+                "xml_enum_attrs": {
+                    "type": {
+                        "ue_enum_type": "EMjGeomType",
+                        "mjs_cast": "mjtGeom",
+                        "value_map": {
+                            "sphere": ["Sphere", "mjGEOM_SPHERE"],
+                            "box":    ["Box",    "mjGEOM_BOX"],
+                            "plane":  ["Plane",  "mjGEOM_PLANE"],
+                            "mesh":   ["Mesh",   "mjGEOM_MESH"],
+                        },
+                    },
+                },
+            },
+            "site": {
+                "xml_enum_attrs": {
+                    "type": {
+                        "ue_enum_type": "EMjSiteType",
+                        "mjs_cast": "mjtGeom",
+                        "value_map": {
+                            "sphere": ["Sphere", "mjGEOM_SPHERE"],
+                            "box":    ["Box",    "mjGEOM_BOX"],
+                        },
+                    },
+                },
+            },
+        },
+    }
+    mjspec = {"enums": {"mjtGeom": ["mjGEOM_SPHERE", "mjGEOM_BOX",
+                                      "mjGEOM_PLANE", "mjGEOM_MESH"]}}
+    gen._check_hand_enum_drift({}, rules, mjspec, str(tmp_path))
+    msgs = [d.message for d in gen._DIAGS_BUFFER.pending]
+    # Geom has all 4 mapped -> no diag.
+    assert not any("element 'geom'" in m for m in msgs)
+    # Site skips Plane + Mesh -> no diag for those either.
+    assert not any("mjGEOM_PLANE" in m and "site" in m for m in msgs)
+    assert not any("mjGEOM_MESH" in m and "site" in m for m in msgs)
diff --git a/Scripts/codegen/tests/test_inject_helpers.py b/Scripts/codegen/tests/test_inject_helpers.py
new file mode 100644
index 0000000..adfc451
--- /dev/null
+++ b/Scripts/codegen/tests/test_inject_helpers.py
@@ -0,0 +1,97 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""Tests for the codegen's inject-guard layer: ``_audit_banner_safety``
+on ``fully_emitted`` overwrites, layout dispatcher fall-through,
+and canonicalisation dispatch on unknown canon names."""
+
+from __future__ import annotations
+
+import generate_ue_components as gen
+from generate_ue_components import _audit_banner_safety
+
+
+# ---------- banner-safety audit ---------------------------------------------
+
+def test_audit_banner_safety_silent_on_template_only(tmp_path):
+    """A subclass file containing only its own header include and a
+    bare ``Type = ...`` constructor should not trigger a diagnostic."""
+    p = tmp_path / "MjBoxGeom.cpp"
+    p.write_text(
+        '#include "MuJoCo/Components/Geometry/MjBoxGeom.h"\n'
+        "UMjBoxGeom::UMjBoxGeom()\n"
+        "{\n"
+        "    Type = EMjGeomType::Box;\n"
+        "}\n",
+        encoding="utf-8",
+    )
+    subtype = {"class_name": "UMjBoxGeom"}
+    _audit_banner_safety(str(p), subtype)
+    assert len(gen._DIAGS_BUFFER.pending) == 0
+
+
+def test_audit_banner_safety_flags_extra_include(tmp_path):
+    """An extra non-template #include in a soon-to-be-overwritten file
+    triggers a diagnostic so the rule author knows hand-rolled content
+    will be discarded."""
+    p = tmp_path / "MjBoxGeom.cpp"
+    p.write_text(
+        '#include "MuJoCo/Components/Geometry/MjBoxGeom.h"\n'
+        '#include "SomeOther/Header.h"\n'
+        "UMjBoxGeom::UMjBoxGeom()\n"
+        "{\n"
+        "    Type = EMjGeomType::Box;\n"
+        "}\n",
+        encoding="utf-8",
+    )
+    subtype = {"class_name": "UMjBoxGeom"}
+    _audit_banner_safety(str(p), subtype)
+    assert any("SomeOther/Header.h" in d.message for d in gen._DIAGS_BUFFER.pending)
+
+
+def test_audit_banner_safety_flags_extra_ctor_body(tmp_path):
+    """Constructor body content beyond ``Type = EMj...::X`` and the
+    rule-provided ``extra_constructor`` is flagged — that's where the
+    joint-subtype bOverride_Type bug came from."""
+    p = tmp_path / "MjHingeJoint.cpp"
+    p.write_text(
+        '#include "MuJoCo/Components/Joints/MjHingeJoint.h"\n'
+        "UMjHingeJoint::UMjHingeJoint()\n"
+        "{\n"
+        "    Type = EMjJointType::Hinge;\n"
+        "    HandRolledHelperCall();\n"
+        "}\n",
+        encoding="utf-8",
+    )
+    subtype = {
+        "class_name": "UMjHingeJoint",
+        "extra_constructor": "",
+    }
+    _audit_banner_safety(str(p), subtype)
+    assert any("non-template ctor content" in d.message
+               and "HandRolledHelperCall" in d.message
+               for d in gen._DIAGS_BUFFER.pending)
+
+
+# ---------- inject-guard diagnostics ----------------------------------------
+
+def test_layout_dispatch_diagnoses_unknown_layout():
+    """`_phase_categories` else-branch fires a diag for typo'd layouts.
+    Catches the silent fall-through where mistyped 'single_uclass'
+    instead of 'single_uclass_per_file' produces zero output."""
+    writes = []
+    rules = {
+        "categories": {
+            "bogus": {"layout": "single_uclass"},
+        },
+    }
+    ctx = gen.PhaseContext(
+        schema={}, rules=rules, mjxmacro={}, mjspec={},
+        public_root="", private_root="", bind_h_path="", writes=writes,
+    )
+    gen._phase_categories(ctx)
+    assert any("unknown layout" in d.message and "bogus" in d.message
+               and "single_uclass" in d.message
+               for d in gen._DIAGS_BUFFER.pending)
+    assert writes == []
+
+
+# Unknown-canon diagnostic is pinned in test_canon_registry.py.
diff --git a/Scripts/codegen/tests/test_inject_helpers_brace.py b/Scripts/codegen/tests/test_inject_helpers_brace.py
new file mode 100644
index 0000000..e82fdf4
--- /dev/null
+++ b/Scripts/codegen/tests/test_inject_helpers_brace.py
@@ -0,0 +1,162 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""Tests for the codegen injection helpers: brace-balance gate +
+multi-tag inject + string/comment-aware brace stripping.
+
+The brace-balance check exists because a stray ``}`` lost inside a
+long composed block silently relocates the compile error to a place
+unrelated to the codegen site. We want the diagnostic to point at the
+exact tag instead."""
+
+from __future__ import annotations
+
+import os
+
+import generate_ue_components as gen
+import _codegen_inject as _inject
+gen._check_brace_balance = _inject._check_brace_balance
+
+
+def _write(tmp_path, name: str, body: str) -> str:
+    p = tmp_path / name
+    p.write_text(body, encoding="utf-8")
+    return str(p)
+
+
+# ---------- brace-balance --------------------------------------------------
+
+def test_brace_balance_flags_unbalanced_block():
+    body = "if (x) { do_thing(); "  # missing close
+    gen._check_brace_balance(body, "FOO", "unit_test")
+    assert any("unbalanced braces" in d.message and "CODEGEN_FOO" in d.message
+               for d in gen._DIAGS_BUFFER.pending)
+
+
+def test_brace_balance_quiet_on_balanced_block():
+    body = "if (x) { do_thing(); }"
+    gen._check_brace_balance(body, "FOO", "unit_test")
+    assert len(gen._DIAGS_BUFFER.pending) == 0
+
+
+def test_brace_balance_ignores_braces_in_strings_and_comments():
+    """``"{"`` inside a TEXT() literal must not count toward the open
+    tally; same for braces in // and /* */ comments."""
+    body = (
+        'if (x)\n'
+        '{\n'
+        '    Log(TEXT("brace { in string"));  // and { in line comment\n'
+        '    /* and { in block comment */\n'
+        '}\n'
+    )
+    gen._check_brace_balance(body, "FOO", "unit_test")
+    assert len(gen._DIAGS_BUFFER.pending) == 0
+
+
+def test_inject_between_tags_with_balance_flag_fires_diag():
+    """``balance_source`` kwarg routes through the brace-balance check."""
+    file_text = "before\n// --- CODEGEN_FOO_START ---\nold\n// --- CODEGEN_FOO_END ---\nafter\n"
+    new_inner = "if (x) {"  # unbalanced
+    _, ok = gen.inject_between_tags(file_text, "FOO", new_inner,
+                                    balance_source="unit_test")
+    assert ok is True
+    assert any("unbalanced braces" in d.message for d in gen._DIAGS_BUFFER.pending)
+
+
+def test_inject_between_tags_default_skips_balance_check():
+    """Without ``balance_source``, the helper does not run the gate —
+    callers writing non-C++ blocks (markdown, JSON) aren't forced to
+    opt out."""
+    file_text = "// --- CODEGEN_FOO_START ---\nold\n// --- CODEGEN_FOO_END ---\n"
+    gen.inject_between_tags(file_text, "FOO", "{ stray")
+    assert len(gen._DIAGS_BUFFER.pending) == 0
+
+
+# ---------- _inject_tags_into_cpp -----------------------------------------
+
+def test_inject_tags_into_cpp_handles_multiple_tags(tmp_path):
+    body = (
+        "// --- CODEGEN_FOO_START ---\n"
+        "old foo\n"
+        "// --- CODEGEN_FOO_END ---\n"
+        "between\n"
+        "// --- CODEGEN_BAR_START ---\n"
+        "old bar\n"
+        "// --- CODEGEN_BAR_END ---\n"
+    )
+    cpp_path = _write(tmp_path, "host.cpp", body)
+    writes: list = []
+    gen._inject_tags_into_cpp(
+        cpp_path,
+        [("FOO", "new foo body"), ("BAR", "new bar body")],
+        writes,
+        diag_source="unit_test",
+    )
+    assert len(writes) == 1
+    out = writes[0].content
+    assert "new foo body" in out
+    assert "new bar body" in out
+    assert "old foo" not in out
+    assert "old bar" not in out
+
+
+def test_inject_tags_into_cpp_diagnoses_missing_file(tmp_path):
+    missing = str(tmp_path / "nope.cpp")
+    writes: list = []
+    gen._inject_tags_into_cpp(
+        missing, [("FOO", "x")], writes, diag_source="unit_test",
+    )
+    assert writes == []
+    assert any("does not exist" in d.message for d in gen._DIAGS_BUFFER.pending)
+
+
+def test_inject_tags_into_cpp_diagnoses_missing_marker(tmp_path):
+    cpp_path = _write(tmp_path, "host.cpp",
+                      "// --- CODEGEN_FOO_START ---\nx\n// --- CODEGEN_FOO_END ---\n")
+    writes: list = []
+    gen._inject_tags_into_cpp(
+        cpp_path,
+        [("FOO", "new foo"), ("MISSING_TAG", "new missing")],
+        writes,
+        diag_source="unit_test",
+    )
+    # The FOO write still landed; MISSING_TAG fires a diagnostic.
+    assert len(writes) == 1
+    assert any("CODEGEN_MISSING_TAG" in d.message for d in gen._DIAGS_BUFFER.pending)
+
+
+def test_inject_tags_into_cpp_skips_write_when_unchanged(tmp_path):
+    """Idempotency: re-injecting the same body must not append a
+    FileWrite. The ``--check`` gate relies on this. The END marker's
+    indent mirrors the START marker's, so a START at column 0 lands
+    END at column 0 too."""
+    body = (
+        "// --- CODEGEN_FOO_START ---\n"
+        "same body\n"
+        "// --- CODEGEN_FOO_END ---\n"
+    )
+    cpp_path = _write(tmp_path, "host.cpp", body)
+    writes: list = []
+    gen._inject_tags_into_cpp(
+        cpp_path, [("FOO", "same body")], writes, diag_source="unit_test",
+    )
+    assert writes == []
+
+
+def test_inject_between_tags_normalises_end_indent_to_match_start(tmp_path):
+    """A hand-prepped source where START is at 8 spaces but END is
+    at 4 spaces (or any mismatch) gets its END normalised to match
+    START."""
+    text = (
+        "        // --- CODEGEN_FOO_START ---\n"
+        "        body content\n"
+        "    // --- CODEGEN_FOO_END ---\n"
+    )
+    new_text, ok = gen.inject_between_tags(text, "FOO", "new body")
+    assert ok is True
+    # Both marker lines now sit at the START's 8-space indent — assert
+    # the END's leading whitespace exactly so we don't accidentally
+    # match a substring of a deeper indent.
+    import re
+    match = re.search(r"^(\s*)// --- CODEGEN_FOO_END ---$", new_text,
+                      re.MULTILINE)
+    assert match is not None
+    assert match.group(1) == "        ", repr(match.group(1))
diff --git a/Scripts/codegen/tests/test_introspect_snapshot.py b/Scripts/codegen/tests/test_introspect_snapshot.py
new file mode 100644
index 0000000..54d7f0e
--- /dev/null
+++ b/Scripts/codegen/tests/test_introspect_snapshot.py
@@ -0,0 +1,43 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""
+Smoke tests for the clang-AST introspect snapshot.
+
+Skipped automatically when libclang isn't available (e.g. CI runners
+without the package). The real validation happens on the committed
+``Scripts/introspect_snapshot.json`` itself — these tests just spot-check
+that schema-evolution surprises don't slip through.
+"""
+
+from __future__ import annotations
+
+import json
+import os
+
+import pytest
+
+_HERE = os.path.dirname(os.path.abspath(__file__))
+_PLUGIN_ROOT = os.path.normpath(os.path.join(_HERE, "..", "..", ".."))
+_SNAPSHOT = os.path.join(_PLUGIN_ROOT, "Scripts", "codegen", "snapshots", "introspect_snapshot.json")
+
+
+def _load() -> dict:
+    if not os.path.exists(_SNAPSHOT):
+        pytest.skip("introspect_snapshot.json not built yet; run "
+                    "build_introspect_snapshot.py")
+    with open(_SNAPSHOT, "r", encoding="utf-8") as f:
+        return json.load(f)
+
+
+def test_snapshot_meta_and_top_level_keys_present():
+    """One combined smoke test: top-level sections are present, each is
+    non-empty, and the ``_meta`` block carries a stable header hash.
+    Substring presence of specific MuJoCo names (mjs_addBody, mjtJoint,
+    etc.) is covered by ``test_enum_scrape::test_real_snapshot_has_
+    expected_enums`` plus the regen --check gate."""
+    snap = _load()
+    for section in ("functions", "enums", "structs", "defines"):
+        assert section in snap, section
+        assert len(snap[section]) > 0, section
+    assert "_meta" in snap
+    assert snap["_meta"]["snapshot_version"] >= 1
+    assert len(snap["_meta"]["header_hash"]) == 64
diff --git a/Scripts/codegen/tests/test_kept_props_for_category.py b/Scripts/codegen/tests/test_kept_props_for_category.py
new file mode 100644
index 0000000..87232c0
--- /dev/null
+++ b/Scripts/codegen/tests/test_kept_props_for_category.py
@@ -0,0 +1,111 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""Locks ``kept_props_for_category`` — the ordered prop-list helper
+extracted from emit_schema_for_tests. Used by the
+schema-completeness header that drives URLab.Codegen.* automation
+tests, so the order + composition must stay deterministic."""
+
+from __future__ import annotations
+
+import generate_ue_components as gen
+
+
+def test_emits_canon_property_first_then_per_attr_in_schema_order():
+    rules = {
+        "global_exclusions": [],
+        "default_type": "float",
+        "property_renames": {},
+        "canonicalizations": {
+            "spatial_pose": {
+                "absorbs_attrs": ["pos"],
+                "emits_property": {"name": "Pos", "type": "FVector"},
+                "import_helper": "X",
+                "export_helper": "Y",
+            },
+        },
+        "element_rules": {
+            "joint": {
+                "applies_canonicalizations": ["spatial_pose"],
+            },
+        },
+    }
+    schema = {"joint": {"attrs": ["pos", "type", "axis", "limited"]}}
+    cat_rules = {"schema_common_block": "joint.attrs"}
+    out = gen.kept_props_for_category("joint", cat_rules, schema, rules)
+    # Pos comes first (canon-owned), then per-attr in schema order
+    # minus the canon-absorbed ``pos``.
+    assert out == ["Pos", "type", "axis", "limited"]
+
+
+def test_disable_schema_emission_returns_empty():
+    rules = {"global_exclusions": [], "canonicalizations": {},
+             "element_rules": {}}
+    cat_rules = {"disable_schema_emission": True,
+                 "schema_common_block": "joint.attrs"}
+    out = gen.kept_props_for_category("joint", cat_rules, {}, rules)
+    assert out == []
+
+
+def test_xml_enum_attr_lowers_to_ue_property():
+    rules = {
+        "global_exclusions": [],
+        "property_renames": {},
+        "canonicalizations": {},
+        "element_rules": {
+            "camera": {
+                "xml_enum_attrs": {
+                    "mode": {
+                        "ue_property": "TrackingMode",
+                        "ue_enum_type": "EMjCameraTrackingMode",
+                        "value_map": {"fixed": ["Fixed", "mjCAMLIGHT_FIXED"]},
+                    },
+                },
+            },
+        },
+    }
+    schema = {"camera": {"attrs": ["mode", "fovy"]}}
+    cat_rules = {"schema_common_block": "camera.attrs"}
+    out = gen.kept_props_for_category("camera", cat_rules, schema, rules)
+    # ``mode`` -> ``TrackingMode``; ``fovy`` passes through.
+    assert out == ["TrackingMode", "fovy"]
+
+
+def test_schema_common_extra_attrs_append_after_block():
+    rules = {"global_exclusions": [], "property_renames": {},
+             "canonicalizations": {}, "element_rules": {}}
+    schema = {"frame": {"attrs": []}}
+    cat_rules = {
+        "schema_common_block": "frame.attrs",
+        "schema_common_extra_attrs": ["pos", "quat", "childclass"],
+    }
+    out = gen.kept_props_for_category("frame", cat_rules, schema, rules)
+    assert out == ["pos", "quat", "childclass"]
+
+
+def test_global_and_element_exclusions_drop_attrs():
+    rules = {
+        "global_exclusions": ["type"],
+        "property_renames": {},
+        "canonicalizations": {},
+        "element_rules": {
+            "joint": {"exclude_attrs": ["limited"]},
+        },
+    }
+    schema = {"joint": {"attrs": ["type", "axis", "limited", "stiffness"]}}
+    cat_rules = {"schema_common_block": "joint.attrs"}
+    out = gen.kept_props_for_category("joint", cat_rules, schema, rules)
+    assert "type" not in out
+    assert "limited" not in out
+    assert out == ["axis", "stiffness"]
+
+
+def test_property_rename_lowers_to_renamed_prop():
+    rules = {
+        "global_exclusions": [],
+        "property_renames": {"qpos": "Qpos"},
+        "canonicalizations": {},
+        "element_rules": {},
+    }
+    schema = {"keyframe": {"attrs": ["qpos", "time"]}}
+    cat_rules = {"schema_common_block": "keyframe.attrs"}
+    out = gen.kept_props_for_category("keyframe", cat_rules, schema, rules)
+    assert out == ["Qpos", "time"]
diff --git a/Scripts/codegen/tests/test_libclang_hand_enum_scrape.py b/Scripts/codegen/tests/test_libclang_hand_enum_scrape.py
new file mode 100644
index 0000000..15f929a
--- /dev/null
+++ b/Scripts/codegen/tests/test_libclang_hand_enum_scrape.py
@@ -0,0 +1,164 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""Tests for the libclang-backed EMj* hand-enum scrape in
+``build_introspect_snapshot.py`` and the snapshot consumer
+``_hand_enums_from_snapshot``.
+
+The scrape:
+  - extracts every ``enum class EMj* : uint8 { ... }`` block under
+    ``Source/URLab/Public``;
+  - strips UE-only macros (UMETA / UENUM) so a minimal libclang TU
+    can parse the block — including nested parens like
+    ``UMETA(DisplayName="Track (Centre of Mass)")``;
+  - lands the result under ``snapshot.hand_enums`` in
+    ``introspect_snapshot.json``.
+
+The hand-enum drift check prefers this over the fallback regex
+scrape — the snapshot is byte-stable across machines because the
+macro stripping is deterministic."""
+
+from __future__ import annotations
+
+import json
+import os
+
+import pytest
+import generate_ue_components as gen
+import _codegen_checks as _checks
+gen._hand_enums_from_snapshot = _checks._hand_enums_from_snapshot
+
+# build_introspect_snapshot lives alongside generate_ue_components but is
+# not always import-clean (it would try to load clang.cindex at import
+# time). Test the strip helper via a tolerant import path.
+import importlib.util
+import sys
+
+_HERE = os.path.dirname(os.path.abspath(__file__))
+_CODEGEN_DIR = os.path.dirname(_HERE)
+_BUILD_SNAP = os.path.join(_CODEGEN_DIR, "build_introspect_snapshot.py")
+
+
+def _load_build_snapshot_module():
+    spec = importlib.util.spec_from_file_location(
+        "_build_introspect_snapshot_for_test", _BUILD_SNAP,
+    )
+    mod = importlib.util.module_from_spec(spec)
+    sys.modules[spec.name] = mod
+    spec.loader.exec_module(mod)
+    return mod
+
+
+@pytest.fixture(scope="module")
+def build_snap():
+    return _load_build_snapshot_module()
+
+
+# ---------- macro stripper -------------------------------------------------
+
+def test_strip_handles_nested_parens_in_umeta(build_snap):
+    """``UMETA(DisplayName="Track (Centre of Mass)")`` — the inner
+    ``(Centre`` opens a paren the regex must not bail on. The previous
+    naive ``UMETA\\([^)]*\\)`` regex stopped at the first ``)``, leaving
+    a stray ``)`` that broke libclang's enum-member walk for any member
+    after the first nested-paren UMETA."""
+    src = (
+        "enum class EMjSample : uint8 {\n"
+        "    Fixed         UMETA(DisplayName = \"Fixed\"),\n"
+        "    TrackCom      UMETA(DisplayName = \"Track (Centre of Mass)\"),\n"
+        "    TargetBody    UMETA(DisplayName = \"Target Body\"),\n"
+        "    TargetBodyCom UMETA(DisplayName = \"Target Body (Centre of Mass)\"),\n"
+        "};\n"
+    )
+    out = build_snap._strip_ue_macros_from_enum_block(src)
+    assert "UMETA" not in out
+    # No stray close-parens left behind after the strip.
+    assert ")" not in out
+    # All four members survive.
+    for m in ("Fixed", "TrackCom", "TargetBody", "TargetBodyCom"):
+        assert m in out, f"member {m} lost during strip; out={out!r}"
+
+
+def test_strip_handles_uenum_header(build_snap):
+    src = (
+        "UENUM(BlueprintType)\n"
+        "enum class EMjFoo : uint8 { A, B };\n"
+    )
+    out = build_snap._strip_ue_macros_from_enum_block(src)
+    assert "UENUM" not in out
+    assert "enum class EMjFoo : uint8 { A, B };" in out
+
+
+def test_strip_handles_line_and_block_comments(build_snap):
+    src = (
+        "enum class EMjFoo : uint8 {\n"
+        "    A,  // first\n"
+        "    B,  /* second */\n"
+        "    /* a multi\n"
+        "       line */ C,\n"
+        "};\n"
+    )
+    out = build_snap._strip_ue_macros_from_enum_block(src)
+    assert "first" not in out
+    assert "second" not in out
+    assert "multi" not in out
+    for m in ("A", "B", "C"):
+        assert m in out
+
+
+def test_strip_balanced_call_terminates_on_unbalanced_input(build_snap):
+    """An open-paren without a matching close should consume to EOF
+    rather than infinite-loop the macro stripper."""
+    src = "UMETA(unbalanced"
+    out = build_snap._strip_ue_macros_from_enum_block(src)
+    # Strip drove the cursor to EOF -> empty residue.
+    assert out == ""
+
+
+# ---------- snapshot output ------------------------------------------------
+
+def test_snapshot_contains_at_least_one_hand_enum():
+    """If the shipped snapshot has hand_enums, we trust the build step
+    ran libclang against URLab's Public/ tree."""
+    snap_path = os.path.join(
+        os.path.dirname(_CODEGEN_DIR),
+        "codegen", "snapshots", "introspect_snapshot.json",
+    )
+    if not os.path.exists(snap_path):
+        pytest.skip("introspect_snapshot.json not present")
+    with open(snap_path, "r", encoding="utf-8") as f:
+        snap = json.load(f)
+    hand_enums = snap.get("hand_enums", {})
+    if not hand_enums:
+        pytest.skip("hand_enums empty (libclang scrape disabled?)")
+    # At least one EMj* enum and every entry has members.
+    emj = [k for k in hand_enums if k.startswith("EMj")]
+    assert emj, f"hand_enums missing EMj* entries; keys={list(hand_enums)[:5]}"
+    for name, entry in hand_enums.items():
+        assert "members" in entry, name
+        assert entry["members"], f"{name} has no members"
+
+
+# ---------- runtime consumer ----------------------------------------------
+
+def test_hand_enums_from_snapshot_returns_flat_member_list():
+    """The runtime drift check expects ``{enum_name: [member,...]}`` —
+    the same shape the regex scrape produced. Confirm the converter
+    flattens the introspect dict shape correctly."""
+    mjspec = {
+        "introspect": {
+            "hand_enums": {
+                "EMjSample": {
+                    "members": {"A": 0, "B": 1, "C": 2},
+                },
+            },
+        },
+    }
+    out = gen._hand_enums_from_snapshot(mjspec)
+    assert out == {"EMjSample": ["A", "B", "C"]}
+
+
+def test_hand_enums_from_snapshot_empty_when_missing():
+    """No introspect / no hand_enums section -> empty dict (the drift
+    check then falls back to the regex scrape)."""
+    assert gen._hand_enums_from_snapshot({}) == {}
+    assert gen._hand_enums_from_snapshot({"introspect": {}}) == {}
+    assert gen._hand_enums_from_snapshot(None) == {}
diff --git a/Scripts/codegen/tests/test_mjxmacro_snapshot.py b/Scripts/codegen/tests/test_mjxmacro_snapshot.py
new file mode 100644
index 0000000..1ef3529
--- /dev/null
+++ b/Scripts/codegen/tests/test_mjxmacro_snapshot.py
@@ -0,0 +1,166 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""Tests for the mjxmacro snapshot parser.
+
+The XNV-accepting regex was added after a silent-drop bug — every entry
+listed AFTER an XNV line in the same block (e.g. ``geom_user``, ``geom_rgba``
+after ``XNV(mjtNum, geom_fluid, ...)`` in MJMODEL_POINTERS_GEOM) was
+discarded because the body-collector treated XNV as a non-entry sentinel
+and stopped the loop. These tests pin the regex behaviour so adding a
+new tag is a deliberate change with a test failure to acknowledge.
+"""
+
+from __future__ import annotations
+
+import textwrap
+
+from build_mjxmacro_snapshot import (
+    _BODY_ENTRY_RE,
+    _X_ENTRY_RE,
+    _X_TAG_RE,
+    _parse_block_body,
+    _parse_pointer_entry,
+)
+
+
+# ---------------------------------------------------------------------------
+# Regex behaviour
+# ---------------------------------------------------------------------------
+
+def test_x_tag_re_accepts_known_tags():
+    """X, XVEC, XNV are the three macro tags MuJoCo uses today."""
+    import re
+    rx = re.compile(rf"^{_X_TAG_RE}$")
+    assert rx.match("X")
+    assert rx.match("XVEC")
+    assert rx.match("XNV")
+    # Future tags should fail until added (the failure is the canary).
+    assert not rx.match("XSEG")
+    assert not rx.match("XMODEL")
+
+
+def test_body_entry_re_matches_all_three_tags():
+    assert _BODY_ENTRY_RE.match("    X   ( int,    foo,  nfoo, 1 )")
+    assert _BODY_ENTRY_RE.match("    XVEC( mjtNum, bar,  nbar, 3 )")
+    assert _BODY_ENTRY_RE.match("    XNV ( mjtNum, baz,  nbaz, mjNFLUID )")
+    # Tags that look similar but aren't in the set.
+    assert not _BODY_ENTRY_RE.match("    X3 ( ... )")
+
+
+def test_x_entry_re_captures_payload_for_all_tags():
+    m = _X_ENTRY_RE.match("    XNV ( mjtNum, geom_fluid, ngeom, mjNFLUID ) \\")
+    assert m
+    assert "geom_fluid" in m.group(1)
+    m2 = _X_ENTRY_RE.match("    X   ( int, geom_type, ngeom, 1 ) \\")
+    assert m2
+    assert "geom_type" in m2.group(1)
+
+
+# ---------------------------------------------------------------------------
+# End-to-end block parsing — the bug that motivated this file
+# ---------------------------------------------------------------------------
+
+def _block_lines(block_body: str) -> list:
+    """Strip the leading newline + dedent so test bodies read naturally."""
+    return textwrap.dedent(block_body).strip("\n").split("\n")
+
+
+def test_block_body_does_not_stop_at_xnv():
+    """The original regex matched (X|XVEC) only; XNV broke the body collector
+    so every subsequent entry in the block was silently dropped. This pins
+    the behaviour so MJMODEL_POINTERS_GEOM can never lose geom_user /
+    geom_rgba again."""
+    lines = _block_lines("""
+        X   ( int,    geom_type,     ngeom, 1 )
+        XNV ( mjtNum, geom_fluid,    ngeom, mjNFLUID )
+        X   ( mjtNum, geom_user,     ngeom, MJ_M(nuser_geom) )
+        X   ( float,  geom_rgba,     ngeom, 4 )
+    """)
+    entries = _parse_block_body(lines, 0)
+    assert len(entries) == 4, "XNV must not abort the body collector"
+    parsed = [_parse_pointer_entry(e) for e in entries]
+    names = [p["name"] for p in parsed if p]
+    assert names == ["geom_type", "geom_fluid", "geom_user", "geom_rgba"]
+
+
+def test_parse_pointer_entry_for_xnv():
+    """XNV entries parse as 4-tuples identical to X entries."""
+    entry = _parse_pointer_entry("XNV ( mjtNum, geom_fluid, ngeom, mjNFLUID )")
+    assert entry == {
+        "type": "mjtNum",
+        "name": "geom_fluid",
+        "outer_dim": "ngeom",
+        "stride": "mjNFLUID",
+    }
+
+
+# ---------------------------------------------------------------------------
+# Two-source fold-in: mjxmacro.h + mjspecmacro.h => one snapshot
+# ---------------------------------------------------------------------------
+
+def test_shipped_snapshot_has_struct_field_sources(real_mjxmacro):
+    """The snapshot writer should record which header each struct-field
+    block came from so consumers can tell mjmodel-side from mjspec-side."""
+    meta = real_mjxmacro.get("_meta", {})
+    sources = meta.get("struct_field_sources")
+    assert isinstance(sources, dict) and sources, (
+        "expected _meta.struct_field_sources to be populated"
+    )
+
+
+def test_shipped_snapshot_attributes_mjspec_blocks_to_mjspecmacro(real_mjxmacro):
+    """MJSCOMPILER_FIELDS + MJSPEC_FIELDS must come from mjspecmacro.h,
+    not mjxmacro.h — they only exist in the spec header."""
+    sources = real_mjxmacro["_meta"]["struct_field_sources"]
+    for block in ("MJSCOMPILER_FIELDS", "MJSPEC_FIELDS"):
+        assert block in sources, f"snapshot missing {block} source attribution"
+        assert "mjspecmacro.h" in sources[block], (
+            f"{block} should be sourced from mjspecmacro.h, got {sources[block]!r}"
+        )
+
+
+def test_shipped_snapshot_attributes_mjoption_blocks_to_mjxmacro(real_mjxmacro):
+    """MJOPTION_FIELDS / MJSTATISTIC_FIELDS live in mjxmacro.h."""
+    sources = real_mjxmacro["_meta"]["struct_field_sources"]
+    for block in ("MJOPTION_FIELDS", "MJSTATISTIC_FIELDS"):
+        assert block in sources, f"snapshot missing {block} source attribution"
+        assert "mjxmacro.h" in sources[block], (
+            f"{block} should be sourced from mjxmacro.h, got {sources[block]!r}"
+        )
+
+
+# ---------------------------------------------------------------------------
+# Defensive block-coverage tripwire
+# ---------------------------------------------------------------------------
+
+def test_shipped_snapshot_includes_arena_pointers_contact(real_mjxmacro):
+    """MJDATA_ARENA_POINTERS_CONTACT must round-trip. It was previously
+    dropped silently because POINTER_BLOCKS_MJDATA omitted it; the
+    unrecognised-block warning in build_mjxmacro_snapshot.py catches
+    similar gaps at parse time, and this test pins the result."""
+    blocks = real_mjxmacro["mjdata_pointers"]
+    assert "MJDATA_ARENA_POINTERS_CONTACT" in blocks, (
+        "MJDATA_ARENA_POINTERS_CONTACT was previously silently dropped — "
+        "if this fails again, check POINTER_BLOCKS_MJDATA in "
+        "build_mjxmacro_snapshot.py."
+    )
+    assert blocks["MJDATA_ARENA_POINTERS_CONTACT"], (
+        "block recognised but body collection produced 0 entries"
+    )
+
+
+def test_parse_mjxmacro_warns_on_unrecognised_block(tmp_path, capsys):
+    """An MJ-prefixed block not in any allow-list AND not in
+    KNOWN_UNCONSUMED_BLOCKS must print a stderr warning, NOT silently
+    appear in the snapshot output."""
+    from build_mjxmacro_snapshot import parse_mjxmacro
+    fake = tmp_path / "fake_mjxmacro.h"
+    fake.write_text(
+        "#define MJMODEL_POINTERS_FROBNOZ \\\n"
+        "    X   ( int, frob_a, nfrob, 1 ) \\\n"
+        "    X   ( int, frob_b, nfrob, 1 )\n",
+        encoding="utf-8",
+    )
+    parse_mjxmacro(str(fake))
+    captured = capsys.readouterr()
+    assert "MJMODEL_POINTERS_FROBNOZ" in captured.err
+    assert "unrecognised" in captured.err.lower()
diff --git a/Scripts/codegen/tests/test_new_rule_shapes.py b/Scripts/codegen/tests/test_new_rule_shapes.py
new file mode 100644
index 0000000..f46cb22
--- /dev/null
+++ b/Scripts/codegen/tests/test_new_rule_shapes.py
@@ -0,0 +1,247 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""Residual unit tests for rule shapes that don't have a dedicated
+file: ``property_default`` (with value_map cross-check),
+``_guarded_export(multiline=...)`` emission shapes, and the full-coverage
+drift checks (value_map stale, orphan type_mappings, compiler attrs,
+mjxmacro block coverage, mjs_array_caps_for).
+
+Larger concerns moved to dedicated files: see ``test_objtype_dispatch``,
+``test_geom_final_type``, ``test_inject_helpers``, ``test_hand_enum_drift``,
+``test_type_shape_drift``, ``test_property_units``.
+"""
+
+from __future__ import annotations
+
+import generate_ue_components as gen
+from generate_ue_components import _guarded_export
+
+
+# ---------- property_default + cross-check ---------------------------------
+
+def test_property_default_emitted_on_base():
+    """`property_default` ends up as the UPROPERTY initializer when
+    ``emit_property_decl: true`` is set on an xml_enum_attr."""
+    rules = {
+        "global_exclusions": [],
+        "default_type": "float",
+        "property_renames": {},
+        "type_mappings": {},
+        "canonicalizations": {},
+        "element_rules": {
+            "actuator": {
+                "xml_enum_attrs": {
+                    "gaintype": {
+                        "ue_property": "GainType",
+                        "ue_enum": "EMjGainType",
+                        "ue_enum_type": "EMjGainType",
+                        "override_toggle": "bOverride_GainType",
+                        "default_lookup": "Default->GainType",
+                        "emit_property_decl": True,
+                        "property_default": "Fixed",
+                        "value_map": {
+                            "fixed":  ["Fixed",  "mjGAIN_FIXED"],
+                            "affine": ["Affine", "mjGAIN_AFFINE"],
+                        },
+                    },
+                },
+            },
+        },
+    }
+    out = gen.emit_schema_for_attrs(
+        ["gaintype"], rules, "actuator", "Actuator",
+        apply_canonicalizations=True,
+    )
+    assert "EMjGainType GainType = EMjGainType::Fixed;" in out.properties_h
+
+
+def test_property_default_cross_check_diagnoses_bad_member():
+    rules = {
+        "global_exclusions": [],
+        "default_type": "float",
+        "property_renames": {},
+        "type_mappings": {},
+        "canonicalizations": {},
+        "element_rules": {
+            "actuator": {
+                "xml_enum_attrs": {
+                    "gaintype": {
+                        "ue_property": "GainType",
+                        "ue_enum": "EMjGainType",
+                        "ue_enum_type": "EMjGainType",
+                        "override_toggle": "bOverride_GainType",
+                        "default_lookup": "Default->GainType",
+                        "emit_property_decl": True,
+                        # NotAMember does not appear in value_map below.
+                        "property_default": "NotAMember",
+                        "value_map": {
+                            "fixed":  ["Fixed",  "mjGAIN_FIXED"],
+                            "affine": ["Affine", "mjGAIN_AFFINE"],
+                        },
+                    },
+                },
+            },
+        },
+    }
+    gen.emit_schema_for_attrs(
+        ["gaintype"], rules, "actuator", "Actuator",
+        apply_canonicalizations=True,
+    )
+    assert any("NotAMember" in d.message for d in gen._DIAGS_BUFFER.pending)
+
+
+# ---------- _guarded_export(multiline=True) --------------------------------
+
+def test_guarded_export_singleline_form():
+    """Single-line form: ``if (toggle) <body>`` on one line."""
+    out = _guarded_export("bOverride_x", "Element->x = X;")
+    assert out == "    if (bOverride_x) Element->x = X;\n"
+
+
+def test_guarded_export_multiline_brace_layout():
+    """Multiline form: brace on its own line; body is emitted verbatim
+    between braces (caller owns inner indentation)."""
+    body = "        line_a;\n        line_b;\n"
+    out = _guarded_export("bOverride_x", body, multiline=True)
+    expected = (
+        "    if (bOverride_x)\n"
+        "    {\n"
+        "        line_a;\n"
+        "        line_b;\n"
+        "    }\n"
+    )
+    assert out == expected
+
+
+def test_guarded_export_multiline_with_extra_cond():
+    """``extra_cond`` (default order) appears after the toggle."""
+    body = "        do_thing();\n"
+    out = _guarded_export(
+        "bOverride_x", body,
+        extra_cond="ready",
+        multiline=True,
+    )
+    assert out.startswith("    if (bOverride_x && ready)\n    {\n")
+    assert "        do_thing();\n" in out
+    assert out.endswith("    }\n")
+
+
+def test_guarded_export_extra_cond_first_order():
+    """``extra_cond_first=True`` flips the order and parenthesises
+    the extra cond."""
+    out = _guarded_export(
+        "bOverride_x", "Element->x = X;",
+        extra_cond="ready",
+        extra_cond_first=True,
+    )
+    assert out == "    if ((ready) && bOverride_x) Element->x = X;\n"
+
+
+# ---------- full-coverage drift checks -------------------------------------
+
+def test_value_map_stale_mj_const_flagged():
+    """A value_map entry referencing a constant NOT in any mjspec enum
+    is stale — surface it before the C++ compile fails."""
+    rules = {
+        "element_rules": {
+            "joint": {
+                "xml_enum_attrs": {
+                    "type": {
+                        "value_map": {
+                            "hinge":  ["Hinge",  "mjJNT_HINGE"],
+                            "screwd": ["Screwd", "mjJNT_NONEXISTENT"],
+                        },
+                    },
+                },
+            },
+        },
+    }
+    mjspec = {"enums": {"mjtJoint": {"mjJNT_HINGE": 0, "mjJNT_SLIDE": 1}}}
+    gen._check_value_map_stale_mj_consts(rules, mjspec)
+    assert any("mjJNT_NONEXISTENT" in d.message for d in gen._DIAGS_BUFFER.pending)
+
+
+def test_orphan_type_mappings_flagged():
+    schema = {"joint": {"attrs": ["stiffness"]}}
+    rules = {
+        "type_mappings": {
+            "stiffness": "TArray<float>",
+            "vanishedattr": "TArray<float>",
+        },
+        "categories": {"joint": {"schema_common_block": "joint.attrs"}},
+        "element_rules": {},
+    }
+    gen._check_orphan_rule_entries(schema, rules)
+    msgs = [d.message for d in gen._DIAGS_BUFFER.pending]
+    assert any("'vanishedattr'" in m for m in msgs)
+    assert not any("'stiffness'" in m for m in msgs)
+
+
+def test_compiler_attrs_coverage_flags_unhandled():
+    schema = {"compiler": {"attrs": ["angle", "newflag2030", "eulerseq"]}}
+    rules = {
+        "compiler_attr_field_map": {"angle": "bAngleInDegrees", "eulerseq": "EulerSeq"},
+        "intentionally_unmodeled_compiler_attrs": [],
+    }
+    gen._check_compiler_attrs_coverage(schema, rules)
+    msgs = [d.message for d in gen._DIAGS_BUFFER.pending]
+    assert any("newflag2030" in m for m in msgs), msgs
+    assert not any("'angle'" in m for m in msgs)
+
+
+def test_compiler_attrs_coverage_respects_intentional_skip():
+    schema = {"compiler": {"attrs": ["angle", "balanceinertia"]}}
+    rules = {
+        "compiler_attr_field_map": {"angle": "bAngleInDegrees"},
+        "intentionally_unmodeled_compiler_attrs": ["balanceinertia"],
+    }
+    gen._check_compiler_attrs_coverage(schema, rules)
+    assert len(gen._DIAGS_BUFFER.pending) == 0
+
+
+def test_mjxmacro_block_coverage_forward():
+    rules = {
+        "synthetic_categories": {
+            "MjOption": {"mjxmacro_block": "MJOPTION_FIELDS"},
+        },
+    }
+    mjxmacro = {
+        "struct_fields": {
+            "MJOPTION_FIELDS": [{"name": "timestep"}],
+            "MJNEW_FIELDS":    [{"name": "wat"}],  # no rule entry
+        },
+    }
+    gen._check_mjxmacro_block_coverage(rules, mjxmacro)
+    msgs = [d.message for d in gen._DIAGS_BUFFER.pending]
+    assert any("MJNEW_FIELDS" in m for m in msgs)
+    assert not any("MJOPTION_FIELDS" in m for m in msgs)
+
+
+def test_mjxmacro_block_coverage_reverse():
+    rules = {
+        "synthetic_categories": {
+            "MjGhost": {"mjxmacro_block": "MJGHOST_FIELDS"},
+        },
+    }
+    mjxmacro = {"struct_fields": {}}
+    gen._check_mjxmacro_block_coverage(rules, mjxmacro)
+    msgs = [d.message for d in gen._DIAGS_BUFFER.pending]
+    assert any("MJGHOST_FIELDS" in m and "MjGhost" in m for m in msgs)
+
+
+def test_mjs_array_caps_for_extracts_fixed_size_arrays():
+    cat_rules = {"mjs_struct": "mjsActuator"}
+    mjspec = {
+        "introspect": {
+            "structs": {
+                "mjsActuator": {
+                    "fields": [
+                        {"name": "gainprm", "c_type": "double [10]", "array_dim": 10},
+                        {"name": "scalar",  "c_type": "double",      "array_dim": None},
+                        {"name": "noslot",  "c_type": "double [1]",  "array_dim": 1},
+                    ],
+                },
+            },
+        },
+    }
+    caps = gen._mjs_array_caps_for(cat_rules, mjspec)
+    assert caps == {"gainprm": 10}
diff --git a/Scripts/codegen/tests/test_objtype_dispatch.py b/Scripts/codegen/tests/test_objtype_dispatch.py
new file mode 100644
index 0000000..b633c8f
--- /dev/null
+++ b/Scripts/codegen/tests/test_objtype_dispatch.py
@@ -0,0 +1,90 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""Tests for the ``objtype_dispatch`` rule shape — emits a switch on a
+UE enum that assigns each case's ``expr`` to ``target``."""
+
+from __future__ import annotations
+
+import generate_ue_components as gen
+from generate_ue_components import _emit_objtype_dispatch_block
+
+
+def test_objtype_dispatch_emits_switch_with_default():
+    cat_rules = {
+        "type_enum_name": "EMjEqualityType",
+        "objtype_dispatch": {
+            "discriminator": "Type",
+            "target": "Element->objtype",
+            "default": "mjOBJ_UNKNOWN",
+            "cases": [
+                {"keys": ["Connect", "Weld"], "expr": "mjOBJ_BODY"},
+                {"keys": ["Joint"], "expr": "mjOBJ_JOINT"},
+            ],
+        },
+    }
+    out = _emit_objtype_dispatch_block(cat_rules)
+    assert "switch (Type)" in out
+    assert "case EMjEqualityType::Connect:" in out
+    assert "case EMjEqualityType::Weld:" in out
+    assert "Element->objtype = mjOBJ_BODY;" in out
+    assert "case EMjEqualityType::Joint:" in out
+    assert "Element->objtype = mjOBJ_JOINT;" in out
+    assert "default:" in out
+    assert "Element->objtype = mjOBJ_UNKNOWN;" in out
+    assert len(gen._DIAGS_BUFFER.pending) == 0
+
+
+def test_objtype_dispatch_returns_none_when_no_rule():
+    assert _emit_objtype_dispatch_block({}) is None
+    assert len(gen._DIAGS_BUFFER.pending) == 0
+
+
+def test_objtype_dispatch_diagnoses_missing_type_enum_name():
+    out = _emit_objtype_dispatch_block({
+        "objtype_dispatch": {
+            "discriminator": "Type",
+            "target": "Element->objtype",
+            "cases": [{"keys": ["A"], "expr": "X"}],
+        },
+    })
+    assert out is None
+    assert any("type_enum_name" in d.message for d in gen._DIAGS_BUFFER.pending)
+
+
+def test_objtype_dispatch_diagnoses_empty_cases():
+    out = _emit_objtype_dispatch_block({
+        "type_enum_name": "EMjEqualityType",
+        "objtype_dispatch": {
+            "discriminator": "Type",
+            "target": "Element->objtype",
+            "cases": [],
+        },
+    })
+    assert out is None
+    assert any("no cases" in d.message for d in gen._DIAGS_BUFFER.pending)
+
+
+def test_objtype_dispatch_diagnoses_empty_keys():
+    out = _emit_objtype_dispatch_block({
+        "type_enum_name": "EMjEqualityType",
+        "objtype_dispatch": {
+            "discriminator": "Type",
+            "target": "Element->objtype",
+            "cases": [{"keys": [], "expr": "mjOBJ_BODY"}],
+        },
+    })
+    assert out is None
+    assert any("empty" in d.message and "keys" in d.message
+               for d in gen._DIAGS_BUFFER.pending)
+
+
+def test_objtype_dispatch_diagnoses_missing_expr():
+    out = _emit_objtype_dispatch_block({
+        "type_enum_name": "EMjEqualityType",
+        "objtype_dispatch": {
+            "discriminator": "Type",
+            "target": "Element->objtype",
+            "cases": [{"keys": ["Connect"]}],
+        },
+    })
+    assert out is None
+    assert any("missing 'expr'" in d.message for d in gen._DIAGS_BUFFER.pending)
diff --git a/Scripts/codegen/tests/test_phase_2_6_checks.py b/Scripts/codegen/tests/test_phase_2_6_checks.py
new file mode 100644
index 0000000..a9df2f8
--- /dev/null
+++ b/Scripts/codegen/tests/test_phase_2_6_checks.py
@@ -0,0 +1,221 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""Tests for the three new drift checks:
+``_check_apply_mode_validity``, ``_check_embedded_cpp_references``,
+and ``_check_allowlist_staleness`` (narrowed to 4 of 6 allowlists per
+the rule design)."""
+
+from __future__ import annotations
+
+import generate_ue_components as gen
+import _codegen_checks as _checks
+gen._collect_every_schema_attr = _checks._collect_every_schema_attr
+
+
+# ---------- _check_apply_mode_validity -------------------------------------
+
+def test_apply_mode_unknown_value_fires_diag():
+    """A typo'd mode (``"guarderd"`` instead of ``"guarded"``) would
+    silently fall through to the unconditional branch — the check
+    surfaces it before the codegen emits a bad write path."""
+    rules = {
+        "synthetic_categories": {
+            "MjOption": {
+                "field_apply_to_spec_mode": {"timestep": "guarderd"},
+            },
+        },
+    }
+    gen._check_apply_mode_validity(rules)
+    assert any("guarderd" in d.message and "timestep" in d.message
+               for d in gen._DIAGS_BUFFER.pending)
+
+
+def test_apply_mode_known_values_silent():
+    rules = {
+        "synthetic_categories": {
+            "MjOption": {
+                "field_apply_to_spec_mode": {
+                    "timestep": "unconditional",
+                    "gravity":  "guarded_pos",
+                    "wind":     "vec3_y_flip",
+                },
+            },
+        },
+    }
+    gen._check_apply_mode_validity(rules)
+    assert len(gen._DIAGS_BUFFER.pending) == 0
+
+
+def test_apply_mode_skips_note_keys():
+    """The phase iterator must ignore ``_note_*`` sibling keys; they
+    appear as strings, not dicts."""
+    rules = {
+        "synthetic_categories": {
+            "_note_intent": "doc string for the section",
+            "MjOption": {
+                "field_apply_to_spec_mode": {"timestep": "unconditional"},
+            },
+        },
+    }
+    gen._check_apply_mode_validity(rules)
+    assert len(gen._DIAGS_BUFFER.pending) == 0
+
+
+# ---------- _check_embedded_cpp_references ---------------------------------
+
+def test_embedded_cpp_refs_flags_stale_mj_const():
+    """``value_map[...][1]`` referencing a constant not in any mjspec
+    enum is a UHT-bound failure — the check moves it forward into
+    codegen-time."""
+    rules = {
+        "element_rules": {
+            "joint": {
+                "xml_enum_attrs": {
+                    "type": {
+                        "value_map": {
+                            "hinge":  ["Hinge",  "mjJNT_HINGE"],
+                            "screw":  ["Screw",  "mjJNT_FXED"],  # typo
+                        },
+                    },
+                },
+            },
+        },
+    }
+    mjspec = {"enums": {"mjtJoint": {"mjJNT_HINGE": 0, "mjJNT_SLIDE": 1}}}
+    gen._check_embedded_cpp_references(rules, mjspec)
+    assert any("mjJNT_FXED" in d.message for d in gen._DIAGS_BUFFER.pending)
+
+
+def test_embedded_cpp_refs_silent_on_real_const():
+    rules = {
+        "element_rules": {
+            "joint": {
+                "xml_enum_attrs": {
+                    "type": {
+                        "value_map": {
+                            "hinge": ["Hinge", "mjJNT_HINGE"],
+                        },
+                    },
+                },
+            },
+        },
+    }
+    mjspec = {"enums": {"mjtJoint": {"mjJNT_HINGE": 0}}}
+    gen._check_embedded_cpp_references(rules, mjspec)
+    assert len(gen._DIAGS_BUFFER.pending) == 0
+
+
+def test_embedded_cpp_refs_flags_bad_geom_final_type_fallback():
+    rules = {
+        "categories": {
+            "geom": {
+                "geom_final_type": {
+                    "xml_enum_ref": "type",
+                    "override_field": "bOverride_Type",
+                    "default_lookup": "Default->Type",
+                    "default_fallback": "mjGEOM_FXED",  # typo
+                    "name_field": "MeshName",
+                    "name_export_for": "mjGEOM_MESH",
+                    "name_target": "Element->mesh",
+                    "name_setter": "MjSetString",
+                },
+            },
+        },
+    }
+    mjspec = {"enums": {"mjtGeom": {"mjGEOM_SPHERE": 0, "mjGEOM_MESH": 7}}}
+    gen._check_embedded_cpp_references(rules, mjspec)
+    assert any("mjGEOM_FXED" in d.message for d in gen._DIAGS_BUFFER.pending)
+
+
+def test_embedded_cpp_refs_quiet_without_snapshot():
+    """No mjspec / no enums -> can't check; the helper no-ops without
+    crashing."""
+    gen._check_embedded_cpp_references({}, None)
+    assert len(gen._DIAGS_BUFFER.pending) == 0
+    gen._check_embedded_cpp_references({}, {"enums": {}})
+    assert len(gen._DIAGS_BUFFER.pending) == 0
+
+
+# ---------- _check_allowlist_staleness -------------------------------------
+
+def test_unmodeled_element_must_exist_in_schema():
+    rules = {
+        "intentionally_unmodeled_elements": {
+            "skin": "URLab uses UE skeletal meshes instead",
+            "phantom_element": "no longer exists",  # stale
+        },
+    }
+    schema = {"body": {"attrs": ["pos"]}, "skin": {"attrs": []}}
+    gen._check_allowlist_staleness(schema, rules, None)
+    msgs = [d.message for d in gen._DIAGS_BUFFER.pending]
+    assert any("phantom_element" in m and "no longer carries" in m for m in msgs)
+    assert not any("'skin'" in m for m in msgs)
+
+
+def test_unmodeled_mjs_field_must_exist_on_struct():
+    rules = {
+        "intentionally_unmodeled_mjs_fields": {
+            "mjsBody": ["explicitinertial", "phantom_field"],
+            "mjsGhost": ["any"],  # stale struct
+        },
+    }
+    mjspec = {
+        "structs": {"mjsBody": ["explicitinertial", "name", "parent"]},
+    }
+    gen._check_allowlist_staleness({}, rules, mjspec)
+    msgs = [d.message for d in gen._DIAGS_BUFFER.pending]
+    assert any("phantom_field" in m and "mjsBody" in m for m in msgs)
+    assert any("mjsGhost" in m for m in msgs)
+    assert not any("'explicitinertial'" in m and "mjsBody" in m for m in msgs)
+
+
+def test_default_typed_attr_must_exist_somewhere_in_schema():
+    rules = {
+        "intentionally_default_typed_attrs": [
+            "kp",                # in actuator_types.position list
+            "torquescale",       # nested under equality.weld
+            "phantom_attr",      # stale
+        ],
+    }
+    schema = {
+        "actuator_types": {"position": ["kp", "kv"]},
+        "equality": {"weld": ["torquescale", "anchor"]},
+    }
+    gen._check_allowlist_staleness(schema, rules, None)
+    msgs = [d.message for d in gen._DIAGS_BUFFER.pending]
+    assert any("phantom_attr" in m for m in msgs)
+    assert not any("'kp'" in m for m in msgs)
+    assert not any("'torquescale'" in m for m in msgs)
+
+
+def test_unmapped_mj_enum_must_exist_in_mjspec():
+    rules = {
+        "intentionally_unmapped_mj_enum_values": {
+            "mjtJoint": ["mjJNT_SLIDE"],
+            "mjtGhost": ["mjGHOST_X"],
+            "__per_element__": {"site.type": ["mjGEOM_PLANE"]},  # skipped
+        },
+    }
+    mjspec = {"enums": {"mjtJoint": {"mjJNT_HINGE": 0, "mjJNT_SLIDE": 1}}}
+    gen._check_allowlist_staleness({}, rules, mjspec)
+    msgs = [d.message for d in gen._DIAGS_BUFFER.pending]
+    assert any("mjtGhost" in m for m in msgs)
+    assert not any("mjtJoint" in m for m in msgs)
+    # __per_element__ never fires its own staleness diag.
+    assert not any("__per_element__" in m for m in msgs)
+
+
+# ---------- _collect_every_schema_attr ------------------------------------
+
+def test_collect_every_schema_attr_picks_up_three_shapes():
+    """Cover the three list shapes the walker handles:
+    nested-attrs key, flat list at any depth (actuator_types,
+    equality.weld), nested-attrs inside sensor_types entries."""
+    schema = {
+        "body": {"attrs": ["pos", "quat"]},
+        "actuator_types": {"position": ["kp", "kv"]},
+        "equality": {"weld": ["torquescale"]},
+        "sensor_types": [{"name": "accel", "attrs": ["cutoff"]}],
+    }
+    out = gen._collect_every_schema_attr(schema)
+    for needed in ("pos", "quat", "kp", "kv", "torquescale", "cutoff"):
+        assert needed in out, f"missing {needed} from {out}"
diff --git a/Scripts/codegen/tests/test_phase_context.py b/Scripts/codegen/tests/test_phase_context.py
new file mode 100644
index 0000000..ea87bce
--- /dev/null
+++ b/Scripts/codegen/tests/test_phase_context.py
@@ -0,0 +1,39 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""Tests for the ``PhaseContext``. Every
+emission phase used to take 7 (or 8) positional args; they now take
+one ``PhaseContext`` so adding a new field to the pipeline doesn't
+touch a dozen signatures."""
+
+from __future__ import annotations
+
+import generate_ue_components as gen
+
+
+def test_every_emission_phase_accepts_phase_context():
+    """Every entry in EMISSION_PHASES.fn must be callable with one
+    PhaseContext arg. Catches refactor drift where one phase keeps the
+    old 7-positional-arg signature."""
+    ctx = gen.PhaseContext(
+        schema={}, rules={}, mjxmacro={}, mjspec={},
+        public_root="", private_root="", bind_h_path="",
+        writes=[],
+    )
+    for phase in gen.EMISSION_PHASES:
+        # No assertion on side effects — just that the call doesn't
+        # TypeError on signature mismatch.
+        phase.fn(ctx)
+
+
+def test_phase_context_is_dataclass_with_writes_list():
+    """Quick sanity: ``writes`` must be a mutable list, not a frozen
+    sequence — phases append to it."""
+    ctx = gen.PhaseContext(
+        schema={}, rules={}, mjxmacro={}, mjspec={},
+        public_root="", private_root="", bind_h_path="",
+        writes=[],
+    )
+    ctx.writes.append("sentinel")
+    assert ctx.writes == ["sentinel"]
+
+
+# Unknown-layout diagnostic is pinned in test_inject_helpers::test_layout_dispatch_diagnoses_unknown_layout.
diff --git a/Scripts/codegen/tests/test_property_units.py b/Scripts/codegen/tests/test_property_units.py
new file mode 100644
index 0000000..a7a904b
--- /dev/null
+++ b/Scripts/codegen/tests/test_property_units.py
@@ -0,0 +1,130 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""Tests for the editor display-unit rule shapes: ``property_units``
+(UE-native ``Units="m"`` meta on scalars, custom ``MjUnit="m"`` on
+TArrays), ``hidden_attrs`` / ``hidden_canon_properties``
+(EditConditionHides), and the validity drift check."""
+
+from __future__ import annotations
+
+import generate_ue_components as gen
+
+
+def test_property_units_emits_units_meta_on_scalar():
+    """Scalar/struct UE types get UE-native meta=(Units="...")."""
+    rules = {
+        "global_exclusions": [],
+        "default_type": "float",
+        "property_renames": {},
+        "type_mappings": {"margin": "float"},
+        "canonicalizations": {},
+        "element_rules": {
+            "geom": {"property_units": {"margin": "m"}},
+        },
+    }
+    out = gen.emit_schema_for_attrs(["margin"], rules, "geom", "Geom")
+    assert 'Units="m"' in out.properties_h, out.properties_h
+    assert out.properties_h.count('Units="m"') == 1
+
+
+def test_property_units_emits_mjunit_meta_on_tarray():
+    """TArray UE types get MjUnit="..." — UHT rejects Units on arrays;
+    URLab's detail customization reads MjUnit at runtime."""
+    rules = {
+        "global_exclusions": [],
+        "default_type": "float",
+        "property_renames": {},
+        "type_mappings": {"size": "TArray<float>"},
+        "canonicalizations": {},
+        "element_rules": {
+            "geom": {"property_units": {"size": "m"}},
+        },
+    }
+    out = gen.emit_schema_for_attrs(["size"], rules, "geom", "Geom")
+    assert 'MjUnit="m"' in out.properties_h, out.properties_h
+    assert 'Units="m"' not in out.properties_h
+
+
+def test_property_units_coexists_with_property_meta():
+    """Units composes with existing property_meta (e.g. DisplayName)
+    without one clobbering the other."""
+    rules = {
+        "global_exclusions": [],
+        "default_type": "float",
+        "property_renames": {},
+        "type_mappings": {},
+        "canonicalizations": {},
+        "element_rules": {
+            "geom": {
+                "property_meta": {"foo": 'DisplayName="Foo Bar"'},
+                "property_units": {"foo": "m"},
+            },
+        },
+    }
+    out = gen.emit_schema_for_attrs(["foo"], rules, "geom", "Geom")
+    assert 'DisplayName="Foo Bar"' in out.properties_h
+    assert 'Units="m"' in out.properties_h
+
+
+def test_property_units_validity_diagnoses_bad_unit():
+    rules = {
+        "element_rules": {
+            "geom": {"property_units": {"size": "meters"}},
+        },
+    }
+    gen._check_property_units_validity(rules)
+    assert any("'meters'" in d.message and "geom" in d.message
+               for d in gen._DIAGS_BUFFER.pending)
+
+
+def test_property_units_validity_silent_on_known_units():
+    rules = {
+        "element_rules": {
+            "geom": {"property_units": {"size": "m", "fovy": "deg"}},
+        },
+    }
+    gen._check_property_units_validity(rules)
+    assert len(gen._DIAGS_BUFFER.pending) == 0
+
+
+def test_hidden_attrs_emits_edit_condition_hides():
+    """hidden_attrs entries emit EditCondition=false + EditConditionHides
+    so the field stays in C++ but the Details panel widget vanishes."""
+    rules = {
+        "global_exclusions": [],
+        "default_type": "float",
+        "property_renames": {},
+        "type_mappings": {"size": "TArray<float>"},
+        "canonicalizations": {},
+        "element_rules": {
+            "geom": {"hidden_attrs": ["size"]},
+        },
+    }
+    out = gen.emit_schema_for_attrs(["size"], rules, "geom", "Geom")
+    assert 'EditCondition="false"' in out.properties_h
+    assert 'EditConditionHides' in out.properties_h
+
+
+def test_hidden_canon_properties_emits_edit_condition_hides():
+    rules = {
+        "global_exclusions": [],
+        "default_type": "float",
+        "property_renames": {},
+        "type_mappings": {},
+        "canonicalizations": {
+            "spatial_pose": {
+                "absorbs_attrs": ["pos"],
+                "emits_property": {"name": "Pos", "type": "FVector"},
+                "import_helper": "X",
+                "export_helper": "Y",
+            },
+        },
+        "element_rules": {
+            "geom": {
+                "applies_canonicalizations": ["spatial_pose"],
+                "hidden_canon_properties": ["spatial_pose"],
+            },
+        },
+    }
+    out = gen.emit_schema_for_attrs([], rules, "geom", "Geom")
+    assert 'FVector Pos' in out.properties_h
+    assert 'EditConditionHides' in out.properties_h
diff --git a/Scripts/codegen/tests/test_regen_no_diff.py b/Scripts/codegen/tests/test_regen_no_diff.py
new file mode 100644
index 0000000..fdea920
--- /dev/null
+++ b/Scripts/codegen/tests/test_regen_no_diff.py
@@ -0,0 +1,46 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""
+Golden-file gate.
+
+Runs ``generate_ue_components.py --check`` against the real schema /
+mjxmacro / rules / mjspec snapshots and asserts:
+  1. Every emitted file is byte-identical to what's on disk (exit 0).
+  2. The run produced zero diagnostics in stderr — catches new warn-mode
+     drift introduced by a rule change that the byte-identity check
+     can't see (drift fires on stderr without changing the emit output).
+"""
+
+from __future__ import annotations
+
+import os
+import subprocess
+import sys
+
+_HERE = os.path.dirname(os.path.abspath(__file__))
+_GENERATOR = os.path.normpath(os.path.join(_HERE, "..", "generate_ue_components.py"))
+
+
+def test_codegen_check_clean() -> None:
+    """`generate_ue_components.py --check` exits 0 with no diagnostics."""
+    proc = subprocess.run(
+        [sys.executable, _GENERATOR, "--check"],
+        capture_output=True,
+        text=True,
+    )
+    stdout = proc.stdout or ""
+    stderr = proc.stderr or ""
+    assert proc.returncode == 0, (
+        "codegen drift detected: re-running the generator would rewrite one or "
+        "more on-disk files. Re-run `python Scripts/codegen/generate_ue_components.py` "
+        "to regenerate, or update the emit functions if hand-edits to "
+        "CODEGEN_*_START/END regions slipped past the generator.\n"
+        f"--- stdout ---\n{stdout}\n--- stderr ---\n{stderr}"
+    )
+    # Diagnostics fire on stderr without changing emit output — catches the
+    # warn-mode drift class that the byte-identity check is blind to.
+    assert "[diagnostic]" not in stderr, (
+        "codegen ran clean (no file rewrites) but emitted one or more "
+        "diagnostics. A rule entry or snapshot likely drifted; rules JSON or "
+        "an intentionally_* allowlist needs updating.\n"
+        f"--- stderr ---\n{stderr}"
+    )
diff --git a/Scripts/codegen/tests/test_require_introspect.py b/Scripts/codegen/tests/test_require_introspect.py
new file mode 100644
index 0000000..92c975d
--- /dev/null
+++ b/Scripts/codegen/tests/test_require_introspect.py
@@ -0,0 +1,61 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""Verifies the ``--require-introspect`` flag's exit-code contract.
+
+The clang-AST introspect snapshot drives the embedded-C++ drift checks
+(value_map / hand-enum / mjs_array shape). Letting codegen silently fall
+back to a stale or missing snapshot turned subtle rule typos into runtime
+fall-through; this flag is the hard-fail gate.
+
+These tests shell out to ``generate_ue_components.py`` so the real
+``main()`` arg-parsing + early-exit branches are exercised.
+"""
+
+from __future__ import annotations
+
+import os
+import subprocess
+import sys
+
+_HERE = os.path.dirname(os.path.abspath(__file__))
+_CODEGEN_DIR = os.path.dirname(_HERE)
+_PLUGIN_ROOT = os.path.dirname(os.path.dirname(_CODEGEN_DIR))
+_GENERATOR = os.path.join(_CODEGEN_DIR, "generate_ue_components.py")
+
+
+def _run(extra_args: list[str], introspect_path: str) -> subprocess.CompletedProcess:
+    return subprocess.run(
+        [sys.executable, _GENERATOR,
+         "--check", "--introspect", introspect_path,
+         *extra_args],
+        capture_output=True, text=True, cwd=_PLUGIN_ROOT,
+    )
+
+
+def test_require_introspect_default_hard_fails_when_missing(tmp_path):
+    """Default behaviour (no flag = require_introspect=True) MUST exit
+    non-zero with the documented exit code (3) when the snapshot is
+    absent. Closes the silent-fallback path."""
+    missing = str(tmp_path / "introspect_snapshot.json")
+    assert not os.path.exists(missing)
+    proc = _run([], missing)
+    assert proc.returncode == 3, (
+        f"expected exit 3, got {proc.returncode}\n"
+        f"stdout={proc.stdout!r}\nstderr={proc.stderr!r}"
+    )
+    assert "introspect snapshot missing" in proc.stderr
+
+
+def test_no_require_introspect_skips_the_exit_3_gate(tmp_path):
+    """``--no-require-introspect`` skips the up-front exit-3 hard-fail
+    so the run reaches the codegen loop. The loop itself raises
+    downstream once it needs the setto signatures the introspect
+    carries — the flag isn't a way to make the codegen succeed
+    without introspect, it's a way to defer the failure for triage."""
+    missing = str(tmp_path / "introspect_snapshot.json")
+    proc = _run(["--no-require-introspect"], missing)
+    # The up-front gate didn't fire (exit 3), and the run reached the
+    # loop. Exit 1 (the downstream RuntimeError) is the documented
+    # failure mode.
+    assert proc.returncode != 3
+    assert "warning" in proc.stderr.lower()
+    assert "introspect snapshot missing" in proc.stderr
diff --git a/Scripts/codegen/tests/test_resolve_mjs_field.py b/Scripts/codegen/tests/test_resolve_mjs_field.py
new file mode 100644
index 0000000..8aa8042
--- /dev/null
+++ b/Scripts/codegen/tests/test_resolve_mjs_field.py
@@ -0,0 +1,91 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""Locks ``_resolve_mjs_field``'s 5-step resolution chain and the
+per-strategy helpers. Each strategy lives in
+its own function and is composed via ``_MJS_FIELD_RESOLVERS``."""
+
+from __future__ import annotations
+
+import _codegen_core as gen  # private resolver surface lives in _codegen_core
+
+
+# ---------- per-strategy resolvers ----------------------------------------
+
+def test_resolve_direct_match():
+    assert gen._resolve_direct("condim", {"condim", "type"}) == "condim"
+    assert gen._resolve_direct("absent", {"other"}) is None
+
+
+def test_resolve_name_suffix():
+    """MuJoCo name-ref fields uniformly append 'name'."""
+    assert gen._resolve_name_suffix("mesh", {"meshname"}) == "meshname"
+    assert gen._resolve_name_suffix("scalar", {"scalarvalue"}) is None
+
+
+def test_resolve_root_name_digits_variants():
+    """``body1`` -> ``bodyname1`` and the underscore variant
+    ``body_name1`` both resolve."""
+    assert gen._resolve_root_name_digits("body1", {"bodyname1"}) == "bodyname1"
+    assert gen._resolve_root_name_digits("body1", {"body_name1"}) == "body_name1"
+    # No-digit suffix -> no match.
+    assert gen._resolve_root_name_digits("body", {"bodyname"}) is None
+
+
+def test_resolve_underscore_norm_matches_drop_underscores():
+    assert gen._resolve_underscore_norm("solreflimit", {"solref_limit"}) == "solref_limit"
+    assert gen._resolve_underscore_norm("solreflimit", {"solref_friction"}) is None
+
+
+def test_resolve_actuator_prefix_with_underscore_norm():
+    """schema ``actuatorfrclimited`` -> mjs ``actfrclimited`` (direct)
+    or ``act_frc_limited`` (via underscore norm)."""
+    assert gen._resolve_actuator_prefix("actuatorfrclimited", {"actfrclimited"}) == "actfrclimited"
+    assert gen._resolve_actuator_prefix("actuatorfrclimited", {"act_frc_limited"}) == "act_frc_limited"
+    assert gen._resolve_actuator_prefix("notanactuator", {"anything"}) is None
+
+
+# ---------- composition through _resolve_mjs_field ------------------------
+
+def test_resolve_mjs_field_prefers_override_table():
+    """Per-element ``attr_to_mjs_field`` rules ALWAYS win — no
+    convention can override a rule author's explicit mapping."""
+    out = gen._resolve_mjs_field(
+        "target", {"target", "targetbody"},
+        attr_to_mjs_field={"target": "targetbody"},
+    )
+    assert out == "targetbody"
+
+
+def test_resolve_mjs_field_falls_through_strategy_order():
+    """Each strategy fires in declared order; only the first match
+    wins. Strategies that don't apply return None and fall through."""
+    # No direct match; falls through to root+name+digits.
+    out = gen._resolve_mjs_field("body1", {"bodyname1", "geomname1"})
+    assert out == "bodyname1"
+
+
+def test_resolve_mjs_field_falls_back_to_attr_when_nothing_matches():
+    """If every strategy returns None, return the attr verbatim — the
+    downstream C++ build will fail loudly on the bad field name."""
+    out = gen._resolve_mjs_field(
+        "novel_attr", {"unrelated_field"},
+    )
+    assert out == "novel_attr"
+
+
+def test_resolve_mjs_field_empty_fields_returns_attr():
+    """When ``mjs_fields`` is empty, no resolution is possible — the
+    attr passes through unchanged."""
+    assert gen._resolve_mjs_field("any", set()) == "any"
+
+
+def test_resolvers_tuple_is_module_constant():
+    """The resolver chain order must be a module-level constant so a
+    test can assert it explicitly. Catches accidental reordering that
+    would change which strategy wins on ambiguous inputs."""
+    assert gen._MJS_FIELD_RESOLVERS == (
+        gen._resolve_direct,
+        gen._resolve_name_suffix,
+        gen._resolve_root_name_digits,
+        gen._resolve_underscore_norm,
+        gen._resolve_actuator_prefix,
+    )
diff --git a/Scripts/codegen/tests/test_rule_iter_helpers.py b/Scripts/codegen/tests/test_rule_iter_helpers.py
new file mode 100644
index 0000000..d2e92c4
--- /dev/null
+++ b/Scripts/codegen/tests/test_rule_iter_helpers.py
@@ -0,0 +1,76 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""Tests for the rule-iteration helpers ``compute_canon_absorbed`` /
+``iter_canon_absorbed`` / ``iter_category_attrs``. They are the single
+source of truth for the standard three-skip exclusion gate that every
+per-attr emission site used to inline."""
+
+from __future__ import annotations
+
+import generate_ue_components as gen
+import _codegen_core as _core
+gen.iter_canon_absorbed = _core.iter_canon_absorbed
+
+
+# ---------- iter_canon_absorbed / compute_canon_absorbed -------------------
+
+def test_compute_canon_absorbed_unions_applied_canons():
+    canons = {
+        "spatial_pose": {"absorbs_attrs": ["pos", "quat", "euler", "axisangle"]},
+        "fromto_decompose": {"absorbs_attrs": ["fromto"]},
+        "unused": {"absorbs_attrs": ["other"]},
+    }
+    got = gen.compute_canon_absorbed(["spatial_pose", "fromto_decompose"], canons)
+    assert got == {"pos", "quat", "euler", "axisangle", "fromto"}
+    # An unapplied canon stays out — gating is by applies_canonicalizations.
+    assert "other" not in got
+
+
+def test_compute_canon_absorbed_silent_on_missing_canon():
+    canons = {"spatial_pose": {"absorbs_attrs": ["pos"]}}
+    got = gen.compute_canon_absorbed(["spatial_pose", "ghost_canon"], canons)
+    assert got == {"pos"}
+
+
+def test_compute_canon_absorbed_handles_empty_inputs():
+    assert gen.compute_canon_absorbed([], {}) == set()
+    assert gen.compute_canon_absorbed(["any"], {}) == set()
+    assert gen.compute_canon_absorbed([], {"spatial_pose": {"absorbs_attrs": ["pos"]}}) == set()
+
+
+def test_iter_canon_absorbed_is_lazy_view():
+    canons = {"c": {"absorbs_attrs": ["a", "b"]}}
+    it = gen.iter_canon_absorbed(["c"], canons)
+    # Confirm it's an iterator, not a materialised list — call sites that
+    # only want a single membership check shouldn't pay for a list build.
+    assert iter(it) is it
+    assert list(it) == ["a", "b"]
+
+
+# ---------- iter_category_attrs --------------------------------------------
+
+def test_iter_category_attrs_filters_three_gates():
+    attrs = ["a", "b", "c", "d", "e"]
+    got = list(gen.iter_category_attrs(
+        attrs,
+        global_excl={"a"},
+        elem_excl={"b"},
+        canon_absorbed={"c"},
+    ))
+    assert got == ["d", "e"]
+
+
+def test_iter_category_attrs_preserves_order():
+    """Codegen output depends on attr order — schema order must survive
+    the filter so emitted .h/.cpp stay byte-identical to the schema."""
+    attrs = ["zeta", "alpha", "mu"]
+    got = list(gen.iter_category_attrs(
+        attrs, global_excl=set(), elem_excl=set(), canon_absorbed=set(),
+    ))
+    assert got == ["zeta", "alpha", "mu"]
+
+
+def test_iter_category_attrs_handles_empty_attrs():
+    got = list(gen.iter_category_attrs(
+        [], global_excl={"a"}, elem_excl=set(), canon_absorbed=set(),
+    ))
+    assert got == []
diff --git a/Scripts/codegen/tests/test_rule_shape_contract.py b/Scripts/codegen/tests/test_rule_shape_contract.py
new file mode 100644
index 0000000..fc1cd6f
--- /dev/null
+++ b/Scripts/codegen/tests/test_rule_shape_contract.py
@@ -0,0 +1,326 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""Contract tests for codegen_rules.json. The rules file is the
+single source of truth for the codegen — these tests pin the
+structural invariants every emitter depends on, so a hand-edit
+that breaks them surfaces in pytest instead of as a UE compile
+failure far downstream."""
+
+from __future__ import annotations
+
+import generate_ue_components as gen  # noqa: F401  (kept for module discovery)
+from _codegen_core import _UE_TYPE_INFO
+
+
+_KNOWN_TOP_LEVEL_KEYS = {
+    # data driving the emit path
+    "global_exclusions",
+    "default_type",
+    "property_renames",
+    "setto_param_defaults",
+    "type_mappings",
+    "categories",
+    "element_rules",
+    "canonicalizations",
+    "synthetic_categories",
+    "generated_enums",
+    "editor_option_helpers",
+    "views",
+    # drift-suppression allowlists
+    "intentionally_unmodeled_elements",
+    "intentionally_unmodeled_mjs_fields",
+    "intentionally_unmodeled_compiler_attrs",
+    "intentionally_default_typed_attrs",
+    "intentionally_unmapped_mj_enum_values",
+    # shared per-rule namespaces
+    "compiler_attr_field_map",
+}
+
+
+def test_top_level_keys_are_recognised(real_rules):
+    """Every top-level key in the rules JSON is either a known shape
+    or a ``_note_*`` sibling string. Catches typo'd renames at edit
+    time."""
+    unknown = []
+    for key in real_rules:
+        if key.startswith("_") or key in _KNOWN_TOP_LEVEL_KEYS:
+            continue
+        unknown.append(key)
+    assert not unknown, (
+        f"unknown top-level keys in codegen_rules.json: {unknown}. "
+        f"Either add them to _KNOWN_TOP_LEVEL_KEYS in this test or "
+        f"_note_-prefix them to mark as documentation."
+    )
+
+
+def test_every_category_carries_required_keys(real_rules):
+    """Every entry under ``categories`` needs a schema_common_block.
+    Missing the block silently produces an empty emit; the test
+    surfaces the missing key at JSON-edit time."""
+    for cat, cat_rules in real_rules.get("categories", {}).items():
+        if cat.startswith("_"):
+            continue
+        assert "schema_common_block" in cat_rules, (
+            f"category '{cat}' missing schema_common_block")
+
+
+def test_canonicalisations_referenced_by_element_rules_exist(real_rules):
+    """element_rules[X].applies_canonicalizations must list canon
+    names that exist in the top-level ``canonicalizations`` block.
+    Otherwise per-element XML import silently drops absorbed attrs."""
+    canon_keys = set(real_rules.get("canonicalizations", {}).keys())
+    for elem, elem_rule in real_rules.get("element_rules", {}).items():
+        for canon in elem_rule.get("applies_canonicalizations", []):
+            assert canon in canon_keys, (
+                f"element_rules['{elem}'].applies_canonicalizations "
+                f"references '{canon}' which is not declared in the "
+                f"top-level canonicalizations block.")
+
+
+def test_xml_enum_attr_value_maps_have_correct_shape(real_rules):
+    """Each ``value_map`` entry must be a 2-element ``[ue_member,
+    mj_const]`` list, non-empty xml key, and the map itself non-empty.
+    An empty map silently emits a dead block that toggles the override
+    flag without ever assigning the enum; empty xml keys match every
+    missing attr at runtime via ``GetAttribute()`` returning ``""``."""
+    for elem, elem_rule in real_rules.get("element_rules", {}).items():
+        for attr, enum_def in elem_rule.get("xml_enum_attrs", {}).items():
+            value_map = enum_def.get("value_map", {})
+            assert value_map, (
+                f"element_rules['{elem}'].xml_enum_attrs['{attr}']"
+                f".value_map is empty — emit would set the override "
+                f"toggle without ever assigning the enum.")
+            for xml_val, mapping in value_map.items():
+                assert xml_val != "", (
+                    f"element_rules['{elem}'].xml_enum_attrs['{attr}']"
+                    f".value_map has an empty-string xml key — at "
+                    f"runtime this matches every missing attr.")
+                assert isinstance(mapping, (list, tuple)), (
+                    f"element_rules['{elem}'].xml_enum_attrs['{attr}']"
+                    f".value_map['{xml_val}'] must be a list, got "
+                    f"{type(mapping).__name__}")
+                assert len(mapping) == 2, (
+                    f"element_rules['{elem}'].xml_enum_attrs['{attr}']"
+                    f".value_map['{xml_val}'] must be "
+                    f"[ue_member, mj_const]; got {mapping!r}")
+
+
+def test_synthetic_categories_carry_block_or_skip_flag(real_rules):
+    """Every synthetic_categories entry must either name an
+    mjxmacro_block or carry a disabled flag. A bare entry would
+    silently emit nothing."""
+    synth = real_rules.get("synthetic_categories", {})
+    for cat_name, def_ in synth.items():
+        if cat_name.startswith("_") or not isinstance(def_, dict):
+            continue
+        assert def_.get("mjxmacro_block") or def_.get("disabled"), (
+            f"synthetic_categories['{cat_name}'] needs either an "
+            f"mjxmacro_block or a disabled flag")
+
+
+# ---------- inner-key shape contracts ----------------------------------
+#
+# The top-level allowlist catches typo'd renames at the rules root, but
+# inner-key typos (e.g. ``attr_to_mjs_fields`` instead of
+# ``attr_to_mjs_field``) silently fall through every reader's
+# ``.get(..., {})``. The contracts below close that loop: every reader
+# fetches by bracketed access against the keys listed here, so a typo
+# surfaces in pytest instead of as a KeyError mid-emit (or worse — a
+# silent no-op that ships wrong-runtime C++).
+
+
+_CATEGORY_REQUIRED = {"schema_common_block", "base_class_name", "base_class_header", "mjs_struct"}
+_CATEGORY_OPTIONAL = {
+    "subtypes", "layout", "subtype_setto", "type_enum_name",
+    "schema_subtypes_block", "schema_common_extra_attrs",
+    "objtype_dispatch", "geom_final_type", "applies_canonicalizations",
+    "subclass_directory_pub", "subclass_directory_priv",
+    "xml_passthrough_emission", "disable_mjs_export_emission",
+}
+_SUBTYPE_REQUIRED = {"key", "enum_value", "class_name"}
+_SUBTYPE_OPTIONAL = {
+    "header", "fully_emitted", "case_body_override", "extra_constructor",
+}
+_ELEMENT_RULE_OPTIONAL = {
+    "exclude_attrs", "applies_canonicalizations", "xml_enum_attrs",
+    "attr_to_mjs_field", "property_renames", "property_meta",
+    "property_units", "property_defaults", "target_collations",
+    "common_imports", "mjs_data_packed_attrs", "mjs_double_vec_attrs",
+    "sentinel_skip_export", "export_skip_attrs", "unit_conversion",
+    "vec3_convert", "hidden_canon_properties", "hidden_attrs",
+    "canon_export_skip", "xml_passthrough_emission", "common_attrs",
+    "field_apply_to_spec_mode", "field_defaults",
+}
+_XML_ENUM_ATTR_REQUIRED = {"ue_property", "ue_enum_type", "value_map"}
+_XML_ENUM_ATTR_OPTIONAL = {
+    "mjs_field", "mjs_cast", "has_override_toggle",
+    "emit_property_decl", "property_default", "property_meta",
+    "from_mj_enum",
+}
+_CANON_REQUIRED = {"absorbs_attrs"}
+_CANON_OPTIONAL = {
+    "emits_property", "category_specifics",
+    "import_helper", "export_helper",
+}
+
+
+def _assert_inner_keys(path: str, obj: dict, required: set, optional: set):
+    for k in required:
+        assert k in obj, f"{path}: missing required key '{k}'"
+    allowed = required | optional
+    bad = [k for k in obj if not k.startswith("_") and k not in allowed]
+    assert not bad, (
+        f"{path}: unknown key(s) {bad}. Either add to the contract "
+        f"allowlist in test_rule_shape_contract.py or _note_-prefix "
+        f"as documentation."
+    )
+
+
+def test_category_inner_keys_match_contract(real_rules):
+    for cat, cat_rules in real_rules.get("categories", {}).items():
+        if cat.startswith("_"):
+            continue
+        _assert_inner_keys(
+            f"categories['{cat}']", cat_rules,
+            _CATEGORY_REQUIRED, _CATEGORY_OPTIONAL,
+        )
+
+
+def test_category_subtype_inner_keys_match_contract(real_rules):
+    for cat, cat_rules in real_rules.get("categories", {}).items():
+        if cat.startswith("_"):
+            continue
+        for i, sub in enumerate(cat_rules.get("subtypes", [])):
+            if not isinstance(sub, dict):
+                continue
+            _assert_inner_keys(
+                f"categories['{cat}'].subtypes[{i}] ({sub.get('key', '?')})",
+                sub, _SUBTYPE_REQUIRED, _SUBTYPE_OPTIONAL,
+            )
+
+
+def test_element_rule_inner_keys_match_contract(real_rules):
+    for elem, elem_rule in real_rules.get("element_rules", {}).items():
+        if elem.startswith("_"):
+            continue
+        _assert_inner_keys(
+            f"element_rules['{elem}']", elem_rule,
+            set(), _ELEMENT_RULE_OPTIONAL,
+        )
+
+
+def test_xml_enum_attr_inner_keys_match_contract(real_rules):
+    for elem, elem_rule in real_rules.get("element_rules", {}).items():
+        for attr, enum_def in elem_rule.get("xml_enum_attrs", {}).items():
+            if not isinstance(enum_def, dict):
+                continue
+            _assert_inner_keys(
+                f"element_rules['{elem}'].xml_enum_attrs['{attr}']",
+                enum_def,
+                _XML_ENUM_ATTR_REQUIRED, _XML_ENUM_ATTR_OPTIONAL,
+            )
+
+
+def test_canonicalisation_inner_keys_match_contract(real_rules):
+    for canon, canon_def in real_rules.get("canonicalizations", {}).items():
+        if canon.startswith("_"):
+            continue
+        _assert_inner_keys(
+            f"canonicalizations['{canon}']", canon_def,
+            _CANON_REQUIRED, _CANON_OPTIONAL,
+        )
+
+
+def test_synthetic_category_required_public_header(real_rules):
+    """Every active synthetic_categories entry needs ``public_header``
+    pointing under ``MuJoCo/Generated/``. Missing key would KeyError at
+    emit; a stray absolute path would write outside the plugin tree."""
+    for cat_name, def_ in real_rules.get("synthetic_categories", {}).items():
+        if cat_name.startswith("_") or not isinstance(def_, dict):
+            continue
+        if def_.get("disabled"):
+            continue
+        assert "public_header" in def_, (
+            f"synthetic_categories['{cat_name}']: missing public_header"
+        )
+        ph = def_["public_header"]
+        assert ph.startswith("MuJoCo/Generated/") and ph.endswith(".h"), (
+            f"synthetic_categories['{cat_name}'].public_header = "
+            f"'{ph}' must be 'MuJoCo/Generated/*.h'"
+        )
+
+
+def test_generated_enums_carry_ue_name_and_mapping(real_rules):
+    """generated_enums entries must declare ue_name (starts with EMj)
+    and at least one mapping source (ue_member_from_mj or
+    extra_members)."""
+    for group, group_def in real_rules.get("generated_enums", {}).items():
+        if group.startswith("_") or not isinstance(group_def, dict):
+            continue
+        for i, entry in enumerate(group_def.get("enums", [])):
+            if entry.get("disabled"):
+                continue
+            ue_name = entry.get("ue_name", "")
+            assert ue_name.startswith("EMj"), (
+                f"generated_enums['{group}'].enums[{i}].ue_name = "
+                f"'{ue_name}' must start with 'EMj'"
+            )
+            has_mj = entry.get("from_mj_enum")
+            has_members = entry.get("ue_member_from_mj") or entry.get("extra_members")
+            assert has_mj or has_members, (
+                f"generated_enums['{group}'].enums[{i}] ({ue_name}): "
+                f"needs from_mj_enum or extra_members; otherwise emits "
+                f"nothing."
+            )
+
+
+def test_setto_param_defaults_are_finite_numeric_literals(real_rules):
+    """Values land VERBATIM in the generated C++ as a ternary's else
+    branch (see ``_emit_setto_call``). Allowed shapes: int / signed-or-
+    -unsigned float with optional decimal + exponent. Reject ``"inf"``,
+    ``"nan"``, hex, or empty strings — they compile to non-finite or
+    invalid C++."""
+    import re
+    finite_re = re.compile(r"^-?\d+(\.\d*)?([eE][+-]?\d+)?$")
+    defaults = real_rules.get("setto_param_defaults", {})
+    for fn_name, params in defaults.items():
+        if fn_name.startswith("_") or not isinstance(params, dict):
+            continue
+        for pname, value in params.items():
+            assert isinstance(value, str), (
+                f"setto_param_defaults['{fn_name}']['{pname}'] = "
+                f"{value!r}: must be a string (drops verbatim into C++)"
+            )
+            assert finite_re.match(value), (
+                f"setto_param_defaults['{fn_name}']['{pname}'] = "
+                f"'{value}': must be a finite decimal literal "
+                f"('inf' / 'nan' / hex / empty rejected)"
+            )
+
+
+def test_type_mappings_use_known_ue_types(real_rules):
+    """Every type in type_mappings must be a UE type the codegen
+    can emit (i.e. resolves through _UE_TYPE_INFO or is a known
+    TArray<...>). Surfacing typo'd types here catches "TArray<flot>"
+    long before UHT fails."""
+    known = set(_UE_TYPE_INFO.keys())
+    for attr, ue_type in real_rules.get("type_mappings", {}).items():
+        if attr.startswith("_"):
+            continue
+        if ue_type in known:
+            continue
+        if ue_type.startswith("TArray<") and ue_type.endswith(">"):
+            inner = ue_type[len("TArray<"):-1]
+            assert (inner in known or inner.startswith("U")
+                    or inner.startswith("EMj")), (
+                f"type_mappings['{attr}'] = '{ue_type}' has unknown "
+                f"inner type '{inner}'")
+            continue
+        # URLab-side hand-rolled enums (EMj*) are legitimate UPROPERTY
+        # types but live outside the registry's UE type universe.
+        if ue_type.startswith("EMj"):
+            continue
+        # Unrecognised types fall through to default_type with a diag;
+        # the test surfaces the JSON-edit-time intent.
+        assert False, (
+            f"type_mappings['{attr}'] = '{ue_type}' is not a "
+            f"recognised UE type")
diff --git a/Scripts/codegen/tests/test_sensor_per_type_extractor.py b/Scripts/codegen/tests/test_sensor_per_type_extractor.py
new file mode 100644
index 0000000..3197e0c
--- /dev/null
+++ b/Scripts/codegen/tests/test_sensor_per_type_extractor.py
@@ -0,0 +1,198 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""
+Unit tests for the per-sensor objtype/reftype extractor in
+build_mjcf_schema_snapshot.py.
+
+The extractor scrapes xml_native_reader.cc's ``Sensor()`` if/else cascade
+so codegen rules don't have to hand-list each sensor's mjsSensor object
+defaults. Tests cover the four shape classes the extractor produces:
+literal default, from_xml, computed (multi-attr derivation), and the
+nested-branch suppression case (distance/normal/fromto share an outer
+branch whose inner ``if`` would otherwise overwrite the entry).
+"""
+
+from __future__ import annotations
+
+import os
+import sys
+
+_HERE = os.path.dirname(os.path.abspath(__file__))
+_CODEGEN_DIR = os.path.dirname(_HERE)
+if _CODEGEN_DIR not in sys.path:
+    sys.path.insert(0, _CODEGEN_DIR)
+
+from build_mjcf_schema_snapshot import _extract_sensor_per_type  # noqa: E402
+
+
+_FAKE_SOURCE = r"""
+void mjXReader::Sensor(XMLElement* section) {
+  while (elem) {
+    mjsSensor* sensor = mjs_addSensor(spec);
+    string type = elem->Value();
+    string text, name, objname, refname;
+
+    if (type == "touch") {
+      sensor->type = mjSENS_TOUCH;
+      sensor->objtype = mjOBJ_SITE;
+      ReadAttrTxt(elem, "site", objname, true);
+    } else if (type == "framepos") {
+      sensor->type = mjSENS_FRAMEPOS;
+      ReadAttrTxt(elem, "objtype", text, true);
+      sensor->objtype = (mjtObj)mju_str2Type(text.c_str());
+      ReadAttrTxt(elem, "objname", objname, true);
+      if (ReadAttrTxt(elem, "reftype", text)) {
+        sensor->reftype = (mjtObj)mju_str2Type(text.c_str());
+        ReadAttrTxt(elem, "refname", refname, true);
+      }
+    } else if (type == "distance" || type == "normal" || type == "fromto") {
+      bool has_body1 = ReadAttrTxt(elem, "body1", objname);
+      sensor->objtype = has_body1 ? mjOBJ_BODY : mjOBJ_GEOM;
+      ReadAttrTxt(elem, "body2", refname);
+      sensor->reftype = mjOBJ_BODY;
+      if (type == "distance") {
+        sensor->type = mjSENS_GEOMDIST;
+      } else if (type == "normal") {
+        sensor->type = mjSENS_GEOMNORMAL;
+      } else {
+        sensor->type = mjSENS_GEOMFROMTO;
+      }
+    } else if (type == "clock") {
+      sensor->type = mjSENS_CLOCK;
+      sensor->objtype = mjOBJ_UNKNOWN;
+    }
+    elem = NextSiblingElement(elem);
+  }
+}
+"""
+
+
+def test_literal_objtype_with_site_name_read():
+    result = _extract_sensor_per_type(_FAKE_SOURCE)
+    assert result["touch"] == {
+        "mj_type":  "mjSENS_TOUCH",
+        "objtype":  "mjOBJ_SITE",
+        "reftype":  None,
+        "obj_attr": "site",
+        "ref_attr": None,
+        "computed": False,
+    }
+
+
+def test_from_xml_objtype_picks_up_objname_attr():
+    result = _extract_sensor_per_type(_FAKE_SOURCE)
+    framepos = result["framepos"]
+    assert framepos["objtype"] == "from_xml"
+    assert framepos["obj_attr"] == "objname"
+    assert framepos["ref_attr"] == "refname"
+    assert framepos["computed"] is False
+
+
+def test_multi_name_branch_emits_one_entry_per_name():
+    result = _extract_sensor_per_type(_FAKE_SOURCE)
+    for name in ("distance", "normal", "fromto"):
+        assert name in result, f"missing {name}"
+        assert result[name]["computed"] is True
+        assert result[name]["objtype"] == "computed"
+        # All three share the same body1/body2 attr-read.
+        assert result[name]["obj_attr"] == "body1"
+        assert result[name]["ref_attr"] == "body2"
+
+
+def test_inner_if_inside_multi_name_branch_does_not_overwrite():
+    """Regression: the body of the distance/normal/fromto branch contains
+    ``if (type == "distance")`` and ``else if (type == "normal")`` which set
+    only ->type. Those inner matches would override the outer entry's
+    obj_attr/ref_attr with None if the extractor walked them naively."""
+    result = _extract_sensor_per_type(_FAKE_SOURCE)
+    assert result["distance"]["obj_attr"] == "body1"
+    assert result["normal"]["obj_attr"] == "body1"
+
+
+def test_clock_has_unknown_objtype_no_name_read():
+    result = _extract_sensor_per_type(_FAKE_SOURCE)
+    assert result["clock"] == {
+        "mj_type":  "mjSENS_CLOCK",
+        "objtype":  "mjOBJ_UNKNOWN",
+        "reftype":  None,
+        "obj_attr": None,
+        "ref_attr": None,
+        "computed": False,
+    }
+
+
+def test_missing_sensor_method_returns_empty():
+    assert _extract_sensor_per_type("// no Sensor method here") == {}
+
+
+# ---------- sensor switch + TagToType codegen ----------------------------
+
+def test_sensor_switch_body_emits_static_objtype_when_present():
+    from generate_ue_components import _emit_sensor_switch_block  # noqa
+    cat_rules = {
+        "type_enum_name": "EMjSensorType",
+        "subtypes": [
+            {"key": "touch",     "enum_value": "Touch"},
+            {"key": "framepos",  "enum_value": "FramePos"},
+        ],
+    }
+    sensor_per_type = {
+        "touch":         {"mj_type": "mjSENS_TOUCH",         "objtype": "mjOBJ_SITE", "reftype": None},
+        "framepos":      {"mj_type": "mjSENS_FRAMEPOS",      "objtype": "from_xml",   "reftype": None},
+        "accelerometer": {"mj_type": "mjSENS_ACCELEROMETER", "objtype": "mjOBJ_SITE", "reftype": None},
+    }
+    out = _emit_sensor_switch_block(cat_rules, sensor_per_type)
+    # Touch gets static objtype literal.
+    assert "case EMjSensorType::Touch: Element->type = mjSENS_TOUCH; Element->objtype = mjOBJ_SITE; break;" in out
+    # framepos has objtype=from_xml -> NO static objtype emitted (handled in post-switch block).
+    assert "case EMjSensorType::FramePos: Element->type = mjSENS_FRAMEPOS; break;" in out
+    # Default fallback is hard-wired to accelerometer.
+    assert "default: Element->type = mjSENS_ACCELEROMETER; Element->objtype = mjOBJ_SITE; break;" in out
+
+
+def test_sensor_switch_body_honours_case_override():
+    from generate_ue_components import _emit_sensor_switch_block
+    cat_rules = {
+        "type_enum_name": "EMjSensorType",
+        "subtypes": [
+            {"key": "rangefinder", "enum_value": "RangeFinder",
+             "case_body_override": "Element->type = mjSENS_RANGEFINDER; Element->objtype = X;"},
+        ],
+    }
+    out = _emit_sensor_switch_block(cat_rules, {"rangefinder": {"mj_type": "mjSENS_RANGEFINDER"}})
+    # Override appears verbatim; the emitter's automatic mj_type / objtype
+    # lines are SKIPPED for this case.
+    assert "Element->objtype = X;" in out
+    assert out.count("RangeFinder") >= 1
+
+
+def test_sensor_tag_to_type_map_uses_xml_key_verbatim():
+    from generate_ue_components import _emit_sensor_tag_to_type_block
+    cat_rules = {
+        "type_enum_name": "EMjSensorType",
+        "subtypes": [
+            {"key": "touch",            "enum_value": "Touch"},
+            {"key": "e_potential",      "enum_value": "EPotential"},
+            {"key": "jointactuatorfrc", "enum_value": "JointActFrc"},
+        ],
+    }
+    out = _emit_sensor_tag_to_type_block(cat_rules)
+    assert '{TEXT("touch"), EMjSensorType::Touch},' in out
+    # Underscore-containing XML keys preserved literally.
+    assert '{TEXT("e_potential"), EMjSensorType::EPotential},' in out
+    # XML key and UE enum_value can diverge; map uses XML key.
+    assert '{TEXT("jointactuatorfrc"), EMjSensorType::JointActFrc},' in out
+
+
+def test_real_snapshot_covers_every_schema_sensor():
+    """End-to-end: the snapshot shipped in Scripts/ should have one
+    per_type entry for every sensor_types entry — and vice versa. Catches
+    drift between the static MJCF[] schema and the parser cascade."""
+    import json
+    plugin_root = os.path.normpath(os.path.join(_HERE, "..", "..", ".."))
+    snap = json.load(open(os.path.join(plugin_root, "Scripts", "codegen", "snapshots", "mjcf_schema_snapshot.json")))
+    schema_names = set(snap["sensor_types"])
+    per_type_names = set(snap.get("sensor_per_type", {}).keys())
+    assert schema_names == per_type_names, (
+        f"schema/per_type drift: only-in-schema={sorted(schema_names - per_type_names)}; "
+        f"only-in-per_type={sorted(per_type_names - schema_names)}"
+    )
diff --git a/Scripts/codegen/tests/test_setto_introspection.py b/Scripts/codegen/tests/test_setto_introspection.py
index ace30cc..9206fdf 100644
--- a/Scripts/codegen/tests/test_setto_introspection.py
+++ b/Scripts/codegen/tests/test_setto_introspection.py
@@ -3,17 +3,16 @@
 
 The 9 hand-written C++ literal blocks that used to live in
 ``codegen_rules.json`` were replaced by ``_emit_setto_call``, which derives
-the call body from the parsed mjs_setTo* signature in
-``mjspec_snapshot.json``. This test makes sure the emission keeps producing
-functional output for every actuator subtype that has a setTo rule.
-"""
+the call body from the parsed mjs_setTo* signature. The signature now
+comes from the clang-AST introspect snapshot, projected into the
+legacy mjspec shape via ``_mjspec_from_introspect``."""
 
 from __future__ import annotations
 
 import json
 import os
 
-from generate_ue_components import _emit_setto_call
+from generate_ue_components import _emit_setto_call, _mjspec_from_introspect
 
 
 _HERE = os.path.dirname(os.path.abspath(__file__))
@@ -21,8 +20,8 @@
 
 
 def _load_mjspec():
-    with open(os.path.join(_PLUGIN_ROOT, "Scripts", "mjspec_snapshot.json"), "r") as f:
-        return json.load(f)
+    with open(os.path.join(_PLUGIN_ROOT, "Scripts", "codegen", "snapshots", "introspect_snapshot.json"), "r") as f:
+        return _mjspec_from_introspect(json.load(f))
 
 
 def _setto_for(subtype_key, real_rules, real_schema, mjspec):
diff --git a/Scripts/codegen/tests/test_should_emit_xml_enum_at_layer.py b/Scripts/codegen/tests/test_should_emit_xml_enum_at_layer.py
new file mode 100644
index 0000000..8bff7f1
--- /dev/null
+++ b/Scripts/codegen/tests/test_should_emit_xml_enum_at_layer.py
@@ -0,0 +1,46 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""Locks the two-case ``_should_emit_xml_enum_at_this_layer`` decision
+the previous closure inside ``emit_schema_for_attrs`` encoded
+implicitly:
+  - subtype-specific xml_enum attrs fire on the subtype's own emission
+  - "type"-like globally-excluded xml_enum attrs fire on the base
+    (apply_canonicalizations=True) layer."""
+
+from __future__ import annotations
+
+import generate_ue_components as gen
+
+
+def test_attr_in_attrs_list_fires_at_any_layer():
+    """Subtype case — dcmotor.input is a subtype attr the base doesn't
+    carry; the subtype layer (apply_canonicalizations=False) owns it."""
+    assert gen._should_emit_xml_enum_at_this_layer(
+        "input", attrs=["input"], global_excl=set(),
+        apply_canonicalizations=False,
+    )
+    assert gen._should_emit_xml_enum_at_this_layer(
+        "input", attrs=["input"], global_excl=set(),
+        apply_canonicalizations=True,
+    )
+
+
+def test_globally_excluded_attr_fires_only_on_base_layer():
+    """Always-at-base case — joint.type is globally excluded from
+    per-attr UPROPERTY emission but xml_enum still owns its import/
+    export at the base. Subtype layer must NOT re-emit it (would cause
+    duplicate switch case + UHT shadowing)."""
+    assert gen._should_emit_xml_enum_at_this_layer(
+        "type", attrs=[], global_excl={"type"},
+        apply_canonicalizations=True,
+    )
+    assert not gen._should_emit_xml_enum_at_this_layer(
+        "type", attrs=[], global_excl={"type"},
+        apply_canonicalizations=False,
+    )
+
+
+def test_unrelated_attr_never_fires():
+    assert not gen._should_emit_xml_enum_at_this_layer(
+        "stranger", attrs=["unrelated"], global_excl={"other"},
+        apply_canonicalizations=True,
+    )
diff --git a/Scripts/codegen/tests/test_strict_exit_code.py b/Scripts/codegen/tests/test_strict_exit_code.py
new file mode 100644
index 0000000..08c9d0b
--- /dev/null
+++ b/Scripts/codegen/tests/test_strict_exit_code.py
@@ -0,0 +1,42 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""Verifies that ``--strict`` (the default) maps fired diagnostics to
+exit code 2. ``--no-strict`` lets the same rules surface diagnostics
+without failing the run — local-triage workflow.
+
+These tests drive the public DiagBuffer surface directly rather than
+shelling out so they stay deterministic across CI without a libclang
+install (the introspect snapshot is read from disk regardless of CI
+state)."""
+
+from __future__ import annotations
+
+import generate_ue_components as gen
+
+
+def test_strict_exit_2_when_diagnostics_fire():
+    """A single diagnostic + ``--strict`` semantics yields exit 2 in
+    main()'s gate path. We simulate by adding to the buffer, flushing,
+    and reading the gate's input — ``_DIAGS_BUFFER.fired_count``."""
+    gen._reset_diags()
+    gen._diag_add("[diagnostic] test", source="unit_test")
+    gen._diag_flush()
+    fired = gen._DIAGS_BUFFER.fired_count
+    assert fired == 1
+    # The gate in main() is: `if args.strict and fired: return 2`.
+    # That predicate is true here -> exit code would be 2.
+    assert fired > 0
+
+
+def test_strict_exit_0_when_no_diagnostics():
+    gen._reset_diags()
+    gen._diag_flush()
+    assert gen._DIAGS_BUFFER.fired_count == 0
+
+
+def test_reset_diags_clears_both_pending_and_fired_count():
+    gen._diag_add("[diagnostic] alpha", source="unit_test")
+    gen._diag_flush()
+    assert gen._DIAGS_BUFFER.fired_count >= 1
+    gen._reset_diags()
+    assert gen._DIAGS_BUFFER.fired_count == 0
+    assert gen._DIAGS_BUFFER.pending == []
diff --git a/Scripts/codegen/tests/test_subclass_emission.py b/Scripts/codegen/tests/test_subclass_emission.py
index 01cd157..959bbed 100644
--- a/Scripts/codegen/tests/test_subclass_emission.py
+++ b/Scripts/codegen/tests/test_subclass_emission.py
@@ -16,13 +16,6 @@
 _PLUGIN_ROOT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
 
 
-@pytest.fixture(scope="session")
-def real_mjspec():
-    """The mjspec snapshot (structs + setto_functions) shipped with the plugin."""
-    with open(os.path.join(_PLUGIN_ROOT, "Scripts", "mjspec_snapshot.json"), "r") as f:
-        return json.load(f)
-
-
 def _find_writes(writes, suffix):
     return [w for w in writes if w.path.endswith(suffix)]
 
diff --git a/Scripts/codegen/tests/test_sync_vendored.py b/Scripts/codegen/tests/test_sync_vendored.py
new file mode 100644
index 0000000..95ea959
--- /dev/null
+++ b/Scripts/codegen/tests/test_sync_vendored.py
@@ -0,0 +1,71 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""
+Unit tests for the manifest parser + formatter in sync_vendored.py.
+
+Network fetches are NOT exercised here — they need the real GitHub
+endpoints. The tests cover only the deterministic-rewrite property
+(re-parsing + re-formatting the same content produces the same bytes)
+and the column-ordering / sort-by-dest invariants.
+"""
+
+from __future__ import annotations
+
+import os
+import sys
+
+_HERE = os.path.dirname(os.path.abspath(__file__))
+_CODEGEN_DIR = os.path.dirname(_HERE)
+if _CODEGEN_DIR not in sys.path:
+    sys.path.insert(0, _CODEGEN_DIR)
+
+from sync_vendored import (  # noqa: E402
+    _parse_manifest,
+    _format_manifest,
+    _extract_preamble,
+)
+
+
+_SAMPLE = """\
+# Vendored sources
+
+Preamble paragraph here.
+
+| file | upstream_url | upstream_sha | retrieved_date | license | local_dest |
+|------|--------------|--------------|-----------------|---------|------------|
+| foo.h | https://example.com/{sha}/foo.h | abc123 | 2026-01-01 | MIT | Scripts/codegen/_vendored/foo.h |
+| bar.h | https://example.com/{sha}/bar.h | def456 | 2026-01-02 | Apache-2.0 | Scripts/codegen/_vendored/bar.h |
+"""
+
+
+def test_parse_skips_separator_and_preamble():
+    rows = _parse_manifest(_SAMPLE)
+    assert len(rows) == 2
+    assert rows[0]["file"] == "foo.h"
+    assert rows[0]["upstream_sha"] == "abc123"
+    assert rows[1]["file"] == "bar.h"
+
+
+def test_extract_preamble_stops_at_first_row():
+    pre = _extract_preamble(_SAMPLE)
+    assert "# Vendored sources" in pre
+    assert "abc123" not in pre
+
+
+def test_format_sorts_by_local_dest():
+    rows = _parse_manifest(_SAMPLE)
+    out = _format_manifest(rows, "# header\n")
+    # bar.h comes first because Scripts/.../bar.h < Scripts/.../foo.h
+    bar_idx = out.index("| bar.h |")
+    foo_idx = out.index("| foo.h |")
+    assert bar_idx < foo_idx
+
+
+def test_format_is_deterministic_round_trip():
+    """Parse + format + parse + format must produce identical bytes —
+    that's the merge-conflict-resistance property the manifest design
+    promises."""
+    pre = _extract_preamble(_SAMPLE)
+    rows = _parse_manifest(_SAMPLE)
+    once = _format_manifest(rows, pre)
+    twice = _format_manifest(_parse_manifest(once), _extract_preamble(once))
+    assert once == twice
diff --git a/Scripts/codegen/tests/test_synthetic_categories.py b/Scripts/codegen/tests/test_synthetic_categories.py
new file mode 100644
index 0000000..6ed0d6e
--- /dev/null
+++ b/Scripts/codegen/tests/test_synthetic_categories.py
@@ -0,0 +1,219 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""
+Tests for the synthetic_categories emitter.
+
+Emits one banner-mode .h file per entry from an mjxmacro X-macro field
+list. Covers MjStatistic, MjOption, the 6 MjVisual_* structs, plus
+MjsCompilerOptions and MjsSpec.
+"""
+
+from __future__ import annotations
+
+import os
+import sys
+
+_HERE = os.path.dirname(os.path.abspath(__file__))
+_CODEGEN_DIR = os.path.dirname(_HERE)
+if _CODEGEN_DIR not in sys.path:
+    sys.path.insert(0, _CODEGEN_DIR)
+
+from generate_ue_components import (  # noqa: E402
+    _emit_synthetic_struct_files,
+    _ue_field_name,
+)
+
+
+def test_ue_field_name_pascal_cases_snake_input():
+    assert _ue_field_name("timestep") == "Timestep"
+    assert _ue_field_name("ls_iterations") == "LsIterations"
+    assert _ue_field_name("sleep_tolerance") == "SleepTolerance"
+    # Single component stays capitalised.
+    assert _ue_field_name("extent") == "Extent"
+
+
+def test_synthetic_struct_emits_uproperty_per_field():
+    mjxmacro = {
+        "struct_fields": {
+            "TEST_FIELDS": [
+                {"type": "mjtNum", "name": "timestep", "count": "1", "is_vec": False},
+                {"type": "int",    "name": "iterations", "count": "1", "is_vec": False},
+                {"type": "float",  "name": "gravity", "count": "3", "is_vec": True},
+            ],
+        },
+    }
+    def_ = {
+        "ue_struct_name": "FTestStruct",
+        "mjxmacro_block": "TEST_FIELDS",
+        "c_struct_type":  "mjTest",
+        "public_header":  "MuJoCo/Generated/MjTest.h",
+        "private_source": "MuJoCo/Generated/MjTest.cpp",
+        "category_label": "Test",
+        "exclude_fields": [],
+        "per_field_meta": {},
+        "urlab_extra_fields": [],
+    }
+    writes = _emit_synthetic_struct_files(
+        "MjTest", def_, mjxmacro, public_root="/tmp/pub", private_root="/tmp/priv",
+    )
+    assert len(writes) == 1
+    h = writes[0].content
+    # USTRUCT outline.
+    assert "USTRUCT(BlueprintType)" in h
+    assert "struct URLAB_API FTestStruct" in h
+    # Each field gets its toggle + value pair.
+    assert "bOverride_Timestep" in h
+    assert "double Timestep" in h
+    assert "bOverride_Iterations" in h
+    assert "int32 Iterations" in h
+    # is_vec + count==3 -> FVector.
+    assert "FVector Gravity" in h
+    # Category labels use the rule's category_label.
+    assert 'Category = "MuJoCo|Test"' in h
+    # ApplyTo body assigns each field with a static_cast<decltype(...)>.
+    assert "Dst.timestep = static_cast<decltype(Dst.timestep)>(Timestep);" in h
+    assert "Dst.gravity[0] = static_cast<decltype(Dst.gravity[0])>(Gravity.X);" in h
+
+
+def test_synthetic_struct_skips_excluded_fields():
+    mjxmacro = {
+        "struct_fields": {
+            "TEST_FIELDS": [
+                {"type": "int", "name": "keep_me", "count": "1", "is_vec": False},
+                {"type": "int", "name": "drop_me", "count": "1", "is_vec": False},
+            ],
+        },
+    }
+    def_ = {
+        "ue_struct_name": "F",
+        "mjxmacro_block": "TEST_FIELDS",
+        "public_header":  "P/F.h",
+        "private_source": "P/F.cpp",
+        "category_label": "C",
+        "exclude_fields": ["drop_me"],
+        "per_field_meta": {},
+    }
+    writes = _emit_synthetic_struct_files("X", def_, mjxmacro, "/tmp/pub", "/tmp/priv")
+    h = writes[0].content
+    assert "KeepMe" in h
+    assert "DropMe" not in h
+
+
+def test_synthetic_struct_field_rename_overrides_default():
+    """``field_renames`` lets rules pin a non-default UE name (e.g. URLab's
+    historical CCD_Iterations vs default CcdIterations)."""
+    mjxmacro = {
+        "struct_fields": {
+            "T_FIELDS": [
+                {"type": "int", "name": "ccd_iterations", "count": "1", "is_vec": False},
+            ],
+        },
+    }
+    def_ = {
+        "ue_struct_name": "FT",
+        "mjxmacro_block": "T_FIELDS",
+        "public_header":  "P/T.h",
+        "private_source": "P/T.cpp",
+        "category_label": "T",
+        "field_renames":  {"ccd_iterations": "CCD_Iterations"},
+    }
+    writes = _emit_synthetic_struct_files("MjT", def_, mjxmacro, "/tmp/pub", "/tmp/priv")
+    h = writes[0].content
+    assert "CCD_Iterations" in h
+    assert "CcdIterations" not in h
+
+
+def test_synthetic_struct_skips_when_mjxmacro_block_missing():
+    """Defensive: a typo in mjxmacro_block produces a diagnostic, not a
+    crash, and emits no file."""
+    from generate_ue_components import _DIAGS_BUFFER, _reset_diags
+    _reset_diags()
+    mjxmacro = {"struct_fields": {}}
+    def_ = {
+        "ue_struct_name": "F", "mjxmacro_block": "NONEXISTENT",
+        "public_header":  "P/F.h", "private_source": "P/F.cpp",
+        "category_label": "C",
+    }
+    writes = _emit_synthetic_struct_files("M", def_, mjxmacro, "/tmp/pub", "/tmp/priv")
+    assert writes == []
+    assert any("NONEXISTENT" in d.message for d in _DIAGS_BUFFER.pending)
+    _reset_diags()
+
+
+def test_generated_enum_emits_uenum_per_entry():
+    from generate_ue_components import _emit_generated_enum_file
+    mjspec = {"enums": {
+        "mjtIntegrator": {"mjINT_EULER": 0, "mjINT_RK4": 1, "mjINT_IMPLICIT": 2},
+    }}
+    def_ = {
+        "public_header": "Generated/MjOptionEnums.h",
+        "enums": [{
+            "ue_name": "EMjIntegrator",
+            "from_mj_enum": "mjtIntegrator",
+            "ue_member_from_mj": {
+                "mjINT_EULER":    "Euler",
+                "mjINT_RK4":      "RK4",
+                "mjINT_IMPLICIT": "Implicit",
+            },
+        }],
+    }
+    writes = _emit_generated_enum_file("MjOptionEnums", def_, mjspec, "/tmp/pub")
+    assert len(writes) == 1
+    h = writes[0].content
+    assert "UENUM(BlueprintType)" in h
+    assert "enum class EMjIntegrator : uint8" in h
+    assert "Euler = 0" in h
+    assert "RK4 = 1" in h
+    assert "Implicit = 2" in h
+
+
+def test_generated_enum_skips_when_mj_enum_missing():
+    from generate_ue_components import _emit_generated_enum_file, _DIAGS_BUFFER, _reset_diags
+    _reset_diags()
+    mjspec = {"enums": {}}
+    def_ = {
+        "public_header": "P/H.h",
+        "enums": [{"ue_name": "E", "from_mj_enum": "mjtMissing", "ue_member_from_mj": {}}],
+    }
+    writes = _emit_generated_enum_file("X", def_, mjspec, "/tmp/pub")
+    assert writes == []
+    assert any("mjtMissing" in d.message for d in _DIAGS_BUFFER.pending)
+    _reset_diags()
+
+
+def test_generated_enum_member_without_mapping_silently_skipped():
+    """C enums often have sentinel constants (mjNGEOMTYPES) that don't
+    map to UE. Rule author omits them from ue_member_from_mj; emitter
+    skips them without diagnostics."""
+    from generate_ue_components import _emit_generated_enum_file
+    mjspec = {"enums": {"mjtX": {"mjX_A": 0, "mjX_SENTINEL": 1, "mjX_B": 2}}}
+    def_ = {
+        "public_header": "P/H.h",
+        "enums": [{
+            "ue_name": "EMjX", "from_mj_enum": "mjtX",
+            "ue_member_from_mj": {"mjX_A": "A", "mjX_B": "B"},
+        }],
+    }
+    h = _emit_generated_enum_file("X", def_, mjspec, "/tmp/pub")[0].content
+    assert "A = 0" in h
+    # B retains its original value 2 (skipped sentinel doesn't renumber).
+    assert "B = 2" in h
+    assert "Sentinel" not in h
+
+
+def test_real_snapshot_mjstatistic_pilot():
+    """End-to-end: the shipped rule produces a syntactically plausible
+    .h file. UE compile separately verifies it actually builds."""
+    import json
+    plugin_root = os.path.normpath(os.path.join(_HERE, "..", "..", ".."))
+    h_path = os.path.join(plugin_root, "Source", "URLab", "Public",
+                          "MuJoCo", "Generated", "MjStatistic.h")
+    assert os.path.exists(h_path), "MjStatistic.h was not generated"
+    with open(h_path, "r", encoding="utf-8") as f:
+        h = f.read()
+    # Pilot fields from MJSTATISTIC_FIELDS.
+    for ue_name in ("Meaninertia", "Meanmass", "Meansize", "Extent", "Center"):
+        assert ue_name in h, f"missing field {ue_name}"
+    # ApplyTo is templated so it works against both mjStatistic and any
+    # alias type. Compile correctness is verified by UE's build step.
+    assert "template <typename TDst>" in h
+    assert "void ApplyTo(TDst& Dst) const" in h
diff --git a/Scripts/codegen/tests/test_type_shape_drift.py b/Scripts/codegen/tests/test_type_shape_drift.py
new file mode 100644
index 0000000..99c4fd5
--- /dev/null
+++ b/Scripts/codegen/tests/test_type_shape_drift.py
@@ -0,0 +1,168 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""Tests for the type-shape reconcile + new-attr fail-loud checks.
+
+Covers ``_classify_c_type`` (C-type kind classifier), ``_shapes_compatible``
+(UE-vs-C shape gate), ``_check_type_shape_drift`` (rule-vs-introspect
+cross-check), and ``_check_new_attr_typing`` (every schema attr must
+be typed or explicitly default-acknowledged)."""
+
+from __future__ import annotations
+
+import generate_ue_components as gen
+import _codegen_core as _core
+import _codegen_checks as _checks
+
+# Stitch test-only re-exports onto `gen` so the assertions below keep
+# their compact ``gen._classify_c_type`` form. The orchestrator's import
+# block intentionally no longer carries these — they're private to
+# _codegen_core / _codegen_checks.
+gen._classify_c_type = _core._classify_c_type
+gen._shapes_compatible = _core._shapes_compatible
+gen._check_type_shape_drift = _checks._check_type_shape_drift
+gen._check_new_attr_typing = _checks._check_new_attr_typing
+
+
+# ---------- _classify_c_type ------------------------------------------------
+
+def test_classify_c_type_scalar_num():
+    assert gen._classify_c_type("double", None) == ("scalar", "num")
+    assert gen._classify_c_type("mjtNum", None) == ("scalar", "num")
+    assert gen._classify_c_type("float", 1) == ("scalar", "num")
+
+
+def test_classify_c_type_vec_dim():
+    assert gen._classify_c_type("double [3]", 3) == ("vec", 3)
+    assert gen._classify_c_type("mjtNum [4]", 4) == ("vec", 4)
+
+
+def test_classify_c_type_dynamic_vec_pointer():
+    """mjFloatVec* / mjDoubleVec* are dynamic-length vectors, NOT
+    opaque pointers — must classify as ('array', None) so TArray<X>
+    rules match cleanly."""
+    assert gen._classify_c_type("mjDoubleVec *", None) == ("array", None)
+    assert gen._classify_c_type("mjFloatVec *", None) == ("array", None)
+
+
+def test_classify_c_type_opaque_element_pointer():
+    """mjsElement* is the parent-spec back-pointer URLab never models;
+    classify as opaque so the shape check skips it."""
+    assert gen._classify_c_type("mjsElement *", None) == ("opaque", None)
+
+
+def test_classify_c_type_string_pointer():
+    assert gen._classify_c_type("mjString", None) == ("scalar", "string")
+    assert gen._classify_c_type("char *", None) == ("scalar", "string")
+
+
+# ---------- _shapes_compatible ----------------------------------------------
+
+def test_shapes_compatible_array_matches_vec():
+    """TArray<float> at the UE layer is the universal carrier for
+    fixed-size C arrays — codegen writes prop.Num() elements and the
+    common case is len(prop) <= C cap."""
+    assert gen._shapes_compatible(("array", None), ("vec", 3))
+    assert gen._shapes_compatible(("array", None), ("array", None))
+
+
+def test_shapes_compatible_scalar_vs_array_is_mismatch():
+    assert not gen._shapes_compatible(("scalar", "num"), ("vec", 3))
+    assert not gen._shapes_compatible(("array", None), ("scalar", "num"))
+    assert not gen._shapes_compatible(("vec", 3), ("vec", 4))
+
+
+# ---------- _check_type_shape_drift ----------------------------------------
+
+def test_type_shape_drift_flags_scalar_vs_array_mismatch():
+    """Rule says TArray<float> but the introspect struct field is a
+    scalar mjtNum — diagnostic fires; emitted code would not compile
+    correctly."""
+    rules = {
+        "type_mappings": {"stiffness": "TArray<float>"},
+        "categories": {
+            "joint": {"mjs_struct": "mjsJoint"},
+        },
+    }
+    mjspec = {
+        "introspect": {
+            "structs": {
+                "mjsJoint": {
+                    "fields": [
+                        {"name": "stiffness", "c_type": "double",
+                         "array_dim": None, "is_pointer": False},
+                    ],
+                },
+            },
+        },
+    }
+    gen._check_type_shape_drift({}, rules, mjspec)
+    assert any("stiffness" in d.message and "mismatch" in d.message
+               for d in gen._DIAGS_BUFFER.pending)
+
+
+def test_type_shape_drift_quiet_on_unmodeled_field():
+    """A field listed in intentionally_unmodeled_mjs_fields is not
+    emitted by codegen — drift check must NOT fire on it."""
+    rules = {
+        "type_mappings": {"damping": "TArray<float>"},
+        "categories": {
+            "flexcomp": {"mjs_struct": "mjsFlex"},
+        },
+        "intentionally_unmodeled_mjs_fields": {
+            "mjsFlex": ["damping"],
+        },
+    }
+    mjspec = {
+        "introspect": {
+            "structs": {
+                "mjsFlex": {
+                    "fields": [
+                        {"name": "damping", "c_type": "double",
+                         "array_dim": None, "is_pointer": False},
+                    ],
+                },
+            },
+        },
+    }
+    gen._check_type_shape_drift({}, rules, mjspec)
+    assert len(gen._DIAGS_BUFFER.pending) == 0
+
+
+# ---------- _check_new_attr_typing -----------------------------------------
+
+def test_new_attr_typing_flags_unmapped_attr():
+    schema = {
+        "joint": {"attrs": ["stiffness", "novel_attr"]},
+    }
+    rules = {
+        "type_mappings": {"stiffness": "TArray<float>"},
+        "default_type": "float",
+        "global_exclusions": [],
+        "intentionally_default_typed_attrs": [],
+        "element_rules": {"joint": {}},
+        "canonicalizations": {},
+        "categories": {
+            "joint": {"schema_common_block": "joint.attrs"},
+        },
+    }
+    gen._check_new_attr_typing(schema, rules)
+    msgs = [d.message for d in gen._DIAGS_BUFFER.pending]
+    assert any("novel_attr" in m and "joint" in m for m in msgs), msgs
+    # stiffness is typed -> no diag.
+    assert not any("'stiffness'" in m for m in msgs)
+
+
+def test_new_attr_typing_respects_intentional_default():
+    schema = {"sensor": {"attrs": ["cutoff"]}}
+    rules = {
+        "type_mappings": {},
+        "default_type": "float",
+        "global_exclusions": [],
+        "intentionally_default_typed_attrs": ["cutoff"],
+        "element_rules": {"sensor": {}},
+        "canonicalizations": {},
+        "categories": {
+            "sensor": {"schema_common_block": "sensor.attrs"},
+        },
+    }
+    gen._check_new_attr_typing(schema, rules)
+    assert len(gen._DIAGS_BUFFER.pending) == 0
diff --git a/Scripts/codegen/tests/test_ue_type_registry.py b/Scripts/codegen/tests/test_ue_type_registry.py
new file mode 100644
index 0000000..2b938e5
--- /dev/null
+++ b/Scripts/codegen/tests/test_ue_type_registry.py
@@ -0,0 +1,44 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""Locks the ``_UE_TYPE_INFO`` registry as the single source of truth
+for every UE type codegen emits. Prevents the previous failure mode
+where ``_attr_default_value`` / ``_UE_TYPE_FAMILY`` / ``Units``-meta
+gates each declared their own type list and drifted apart."""
+
+from __future__ import annotations
+
+from _codegen_core import (
+    _UE_TYPE_INFO, _UE_TYPE_FAMILY, _UE_TYPES_ACCEPTING_UNITS_META,
+    _attr_default_value, _ue_type_accepts_units_meta,
+)
+
+
+def test_ue_type_family_is_view_over_registry():
+    """``_UE_TYPE_FAMILY`` (drift-check input) must be a faithful view
+    over ``_UE_TYPE_INFO`` — same keys, same shape values."""
+    assert set(_UE_TYPE_FAMILY.keys()) == set(_UE_TYPE_INFO.keys())
+    for k, info in _UE_TYPE_INFO.items():
+        assert _UE_TYPE_FAMILY[k] == info.shape, k
+
+
+def test_units_meta_set_is_view_over_registry():
+    expected = {k for k, v in _UE_TYPE_INFO.items() if v.accepts_units_meta}
+    assert _UE_TYPES_ACCEPTING_UNITS_META == expected
+
+
+def test_attr_default_value_uses_registry():
+    for ue_type, info in _UE_TYPE_INFO.items():
+        assert _attr_default_value(ue_type) == info.default_init, ue_type
+
+
+def test_attr_default_value_falls_back_for_novel_tarray():
+    """Novel TArray<...> shapes (e.g. TArray<UObject*>) the registry
+    doesn't enumerate still default-construct empty — the catch-all
+    stays load-bearing for forward compatibility."""
+    assert _attr_default_value("TArray<UMjFoo*>") == "{}"
+
+
+def test_ue_type_accepts_units_meta_matches_registry():
+    for ue_type, info in _UE_TYPE_INFO.items():
+        assert _ue_type_accepts_units_meta(ue_type) == info.accepts_units_meta, ue_type
+    # Unknown types must default to "no Units meta" — UHT would reject.
+    assert _ue_type_accepts_units_meta("UMjFoo*") is False
diff --git a/Scripts/codegen/tests/test_view_struct.py b/Scripts/codegen/tests/test_view_struct.py
index 8ba7449..52abd65 100644
--- a/Scripts/codegen/tests/test_view_struct.py
+++ b/Scripts/codegen/tests/test_view_struct.py
@@ -63,3 +63,58 @@ def test_view_has_one_field_per_mjxmacro_entry(real_rules, real_mjxmacro):
         name = entry["name"]
         view_name = field_renames.get(name, name)
         assert f" {view_name};" in fields, f"missing view field for {name} (-> {view_name})"
+
+
+def test_stride_one_int_entries_emit_as_dereferenced_scalars():
+    """Single-int entries (stride=="1", type=="int") emit as ``int X``
+    with ``X = m->src[id]`` in the bind body — value semantics, not pointer."""
+    rules = {"views": {"FooView": {
+        "obj_type": "mjOBJ_BODY",
+        "include_blocks": ["FOO_BLOCK"],
+    }}}
+    mjxmacro = {"mjmodel_pointers": {"FOO_BLOCK": [
+        {"type": "int",    "name": "foo_kind",     "outer_dim": "nfoo", "stride": "1"},
+        {"type": "int",    "name": "foo_size_arr", "outer_dim": "nfoo", "stride": "3"},
+        {"type": "mjtNum", "name": "foo_pos",      "outer_dim": "nfoo", "stride": "3"},
+    ]}}
+    views = emit_view_structs(rules, mjxmacro)
+    fields, bind = views["FooView"]
+    # Scalar int — dereferenced, not pointer.
+    assert "    int foo_kind;" in fields
+    assert "    foo_kind = m->foo_kind[id];" in bind
+    # Int but stride>1 — stays a pointer.
+    assert "    int* foo_size_arr;" in fields
+    assert "    foo_size_arr = m->foo_size_arr + id * 3;" in bind
+    # mjtNum stride>1 — pointer.
+    assert "    mjtNum* foo_pos;" in fields
+    assert "    foo_pos = m->foo_pos + id * 3;" in bind
+
+
+def test_data_fields_bind_override():
+    """``data_fields[name].bind_override`` short-circuits the standard
+    ``base + index_var * stride`` emit and drops the override verbatim
+    inside the BIND marker. Used by SensorView::sensordata (bounds-checked
+    slice) and ActuatorView::act (nullable lookup)."""
+    rules = {"views": {"BarView": {
+        "obj_type": "mjOBJ_GEOM",
+        "include_blocks": [],
+        "data_fields": {
+            "_note_skip_me": "comment-only; codegen must skip _note_* keys",
+            "plain":        {"index_var": "id", "stride": "1"},
+            "bounds_check": {"bind_override":
+                "bounds_check = (id < m->nfoo) ? d->bounds_check + id : nullptr;"},
+        },
+    }}}
+    mjxmacro = {"mjmodel_pointers": {}}
+    views = emit_view_structs(rules, mjxmacro)
+    fields, bind = views["BarView"]
+    # Standard data_field bind emitted.
+    assert "    mjtNum* plain;" in fields
+    assert "    plain = d->plain + id * 1;" in bind
+    # bind_override field still declared at the standard cpp_type.
+    assert "    mjtNum* bounds_check;" in fields
+    # bind_override body dropped verbatim — no base+id*stride.
+    assert "    bounds_check = (id < m->nfoo) ? d->bounds_check + id : nullptr;" in bind
+    # _note_* keys are documentation, not data fields.
+    assert "_note_skip_me" not in fields
+    assert "_note_skip_me" not in bind
diff --git a/Scripts/codegen/tests/test_xml_passthrough_emit.py b/Scripts/codegen/tests/test_xml_passthrough_emit.py
new file mode 100644
index 0000000..d01d5b3
--- /dev/null
+++ b/Scripts/codegen/tests/test_xml_passthrough_emit.py
@@ -0,0 +1,75 @@
+# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+"""Tests for ``emit_xml_passthrough_body``. Categories with
+``xml_passthrough_emission: true`` (flexcomp) build an MJCF XML string
+instead of writing through the mjs spec API — the codegen owns the
+per-attr formatter so a new schema attr flows end-to-end. The body
+emits ``name="value"`` substrings for every override-tracked
+UPROPERTY."""
+
+from __future__ import annotations
+
+import generate_ue_components as gen
+
+
+def _rules(attr: str, ue_type: str) -> dict:
+    return {
+        "default_type": "float",
+        "type_mappings": {attr: ue_type},
+        "element_rules": {"thing": {}},
+    }
+
+
+def test_float_attr_emits_printf_with_toggle_guard():
+    body = gen.emit_xml_passthrough_body(["stiffness"], _rules("stiffness", "float"), element_name="thing")
+    assert "FString Out;" in body
+    assert "if (bOverride_stiffness)" in body
+    assert '" stiffness=\\"%f\\""' in body
+    assert "return Out;" in body
+
+
+def test_int32_attr_emits_printf_with_d_format():
+    body = gen.emit_xml_passthrough_body(["group"], _rules("group", "int32"), element_name="thing")
+    assert "if (bOverride_group)" in body
+    assert '" group=\\"%d\\""' in body
+
+
+def test_bool_attr_emits_ternary_string():
+    body = gen.emit_xml_passthrough_body(["limited"], _rules("limited", "bool"), element_name="thing")
+    assert 'TEXT("true") : TEXT("false")' in body
+    assert '" limited=\\"%s\\""' in body
+
+
+def test_tarray_float_emits_space_separated_loop():
+    body = gen.emit_xml_passthrough_body(["range"], _rules("range", "TArray<float>"), element_name="thing")
+    assert "if (bOverride_range && range.Num() > 0)" in body
+    assert "for (int32 i = 0; i < range.Num(); ++i)" in body
+    assert 'if (i > 0) Out += TEXT(" ");' in body
+
+
+def test_flinearcolor_emits_four_component_format():
+    body = gen.emit_xml_passthrough_body(["rgba"], _rules("rgba", "FLinearColor"), element_name="thing")
+    assert "rgba.R" in body and "rgba.G" in body and "rgba.B" in body and "rgba.A" in body
+    assert '" rgba=\\"%f %f %f %f\\""' in body
+
+
+def test_name_attr_is_skipped_by_caller_owned_logic():
+    """The caller handles ``name`` (prefix / MjName logic); the body must
+    not duplicate it."""
+    body = gen.emit_xml_passthrough_body(["name", "stiffness"], _rules("stiffness", "float"), element_name="thing")
+    assert '" name=' not in body
+    assert "stiffness" in body
+
+
+def test_global_exclusion_skips_attr():
+    rules = {**_rules("stiffness", "float"), "global_exclusions": ["stiffness"]}
+    body = gen.emit_xml_passthrough_body(["stiffness", "group"], rules, element_name="thing")
+    assert "stiffness" not in body
+    # 'group' still passes if it's typed (it isn't here -> falls back to default_type 'float').
+
+
+def test_unsupported_type_emits_todo_marker():
+    """Novel UE types produce a TODO comment so a reviewer sees the gap
+    instead of a silent missing attr."""
+    body = gen.emit_xml_passthrough_body(["mystery"], _rules("mystery", "FQuat"), element_name="thing")
+    assert "TODO(xml_passthrough)" in body
+    assert '"mystery"' in body and "FQuat" in body
diff --git a/Scripts/mjspec_snapshot.json b/Scripts/mjspec_snapshot.json
deleted file mode 100644
index 64531d0..0000000
--- a/Scripts/mjspec_snapshot.json
+++ /dev/null
@@ -1,693 +0,0 @@
-{
-  "structs": {
-    "mjsElement": [
-      "elemtype",
-      "signature"
-    ],
-    "mjsCompiler": [
-      "autolimits",
-      "boundmass",
-      "boundinertia",
-      "settotalmass",
-      "balanceinertia",
-      "fitaabb",
-      "degree",
-      "eulerseq",
-      "discardvisual",
-      "usethread",
-      "fusestatic",
-      "inertiafromgeom",
-      "inertiagrouprange",
-      "saveinertial",
-      "alignfree",
-      "LRopt",
-      "meshdir",
-      "texturedir"
-    ],
-    "mjsOrientation": [
-      "type",
-      "axisangle",
-      "xyaxes",
-      "zaxis",
-      "euler"
-    ],
-    "mjsPlugin": [
-      "element",
-      "name",
-      "plugin_name",
-      "active",
-      "info"
-    ],
-    "mjsBody": [
-      "element",
-      "childclass",
-      "pos",
-      "quat",
-      "alt",
-      "mass",
-      "ipos",
-      "iquat",
-      "inertia",
-      "ialt",
-      "fullinertia",
-      "mocap",
-      "gravcomp",
-      "sleep",
-      "userdata",
-      "explicitinertial",
-      "plugin",
-      "info"
-    ],
-    "mjsFrame": [
-      "element",
-      "childclass",
-      "pos",
-      "quat",
-      "alt",
-      "info"
-    ],
-    "mjsJoint": [
-      "element",
-      "type",
-      "pos",
-      "axis",
-      "ref",
-      "align",
-      "stiffness",
-      "springref",
-      "springdamper",
-      "limited",
-      "range",
-      "margin",
-      "solref_limit",
-      "solimp_limit",
-      "actfrclimited",
-      "actfrcrange",
-      "armature",
-      "damping",
-      "frictionloss",
-      "solref_friction",
-      "solimp_friction",
-      "group",
-      "actgravcomp",
-      "userdata",
-      "info"
-    ],
-    "mjsGeom": [
-      "element",
-      "type",
-      "pos",
-      "quat",
-      "alt",
-      "fromto",
-      "size",
-      "contype",
-      "conaffinity",
-      "condim",
-      "priority",
-      "friction",
-      "solmix",
-      "solref",
-      "solimp",
-      "margin",
-      "gap",
-      "mass",
-      "density",
-      "typeinertia",
-      "fluid_ellipsoid",
-      "fluid_coefs",
-      "material",
-      "rgba",
-      "group",
-      "hfieldname",
-      "meshname",
-      "fitscale",
-      "userdata",
-      "plugin",
-      "info"
-    ],
-    "mjsSite": [
-      "element",
-      "pos",
-      "quat",
-      "alt",
-      "fromto",
-      "size",
-      "type",
-      "material",
-      "group",
-      "rgba",
-      "userdata",
-      "info"
-    ],
-    "mjsCamera": [
-      "element",
-      "pos",
-      "quat",
-      "alt",
-      "mode",
-      "targetbody",
-      "proj",
-      "resolution",
-      "output",
-      "fovy",
-      "ipd",
-      "intrinsic",
-      "sensor_size",
-      "focal_length",
-      "focal_pixel",
-      "principal_length",
-      "principal_pixel",
-      "userdata",
-      "info"
-    ],
-    "mjsLight": [
-      "element",
-      "pos",
-      "dir",
-      "mode",
-      "targetbody",
-      "active",
-      "type",
-      "texture",
-      "castshadow",
-      "bulbradius",
-      "intensity",
-      "range",
-      "attenuation",
-      "cutoff",
-      "exponent",
-      "ambient",
-      "diffuse",
-      "specular",
-      "info"
-    ],
-    "mjsFlex": [
-      "element",
-      "contype",
-      "conaffinity",
-      "condim",
-      "priority",
-      "friction",
-      "solmix",
-      "solref",
-      "solimp",
-      "margin",
-      "gap",
-      "dim",
-      "radius",
-      "size",
-      "internal",
-      "flatskin",
-      "selfcollide",
-      "passive",
-      "activelayers",
-      "group",
-      "edgestiffness",
-      "edgedamping",
-      "rgba",
-      "material",
-      "young",
-      "poisson",
-      "damping",
-      "thickness",
-      "elastic2d",
-      "cellcount",
-      "order",
-      "nodebody",
-      "vertbody",
-      "node",
-      "vert",
-      "elem",
-      "texcoord",
-      "elemtexcoord",
-      "info"
-    ],
-    "mjsMesh": [
-      "element",
-      "content_type",
-      "file",
-      "refpos",
-      "refquat",
-      "scale",
-      "inertia",
-      "smoothnormal",
-      "needsdf",
-      "maxhullvert",
-      "uservert",
-      "usernormal",
-      "usertexcoord",
-      "userface",
-      "userfacenormal",
-      "userfacetexcoord",
-      "plugin",
-      "material",
-      "octree_maxdepth",
-      "info"
-    ],
-    "mjsHField": [
-      "element",
-      "content_type",
-      "file",
-      "size",
-      "nrow",
-      "ncol",
-      "userdata",
-      "info"
-    ],
-    "mjsSkin": [
-      "element",
-      "file",
-      "material",
-      "rgba",
-      "inflate",
-      "group",
-      "vert",
-      "texcoord",
-      "face",
-      "bodyname",
-      "bindpos",
-      "bindquat",
-      "vertid",
-      "vertweight",
-      "info"
-    ],
-    "mjsTexture": [
-      "element",
-      "type",
-      "colorspace",
-      "builtin",
-      "mark",
-      "rgb1",
-      "rgb2",
-      "markrgb",
-      "random",
-      "height",
-      "width",
-      "nchannel",
-      "content_type",
-      "file",
-      "gridsize",
-      "gridlayout",
-      "cubefiles",
-      "data",
-      "hflip",
-      "vflip",
-      "info"
-    ],
-    "mjsMaterial": [
-      "element",
-      "textures",
-      "texuniform",
-      "texrepeat",
-      "emission",
-      "specular",
-      "shininess",
-      "reflectance",
-      "metallic",
-      "roughness",
-      "rgba",
-      "info"
-    ],
-    "mjsPair": [
-      "element",
-      "geomname1",
-      "geomname2",
-      "condim",
-      "solref",
-      "solreffriction",
-      "solimp",
-      "margin",
-      "gap",
-      "friction",
-      "info"
-    ],
-    "mjsExclude": [
-      "element",
-      "bodyname1",
-      "bodyname2",
-      "info"
-    ],
-    "mjsEquality": [
-      "element",
-      "type",
-      "data",
-      "active",
-      "name1",
-      "name2",
-      "objtype",
-      "solref",
-      "solimp",
-      "info"
-    ],
-    "mjsTendon": [
-      "element",
-      "stiffness",
-      "springlength",
-      "damping",
-      "frictionloss",
-      "solref_friction",
-      "solimp_friction",
-      "armature",
-      "limited",
-      "actfrclimited",
-      "range",
-      "actfrcrange",
-      "margin",
-      "solref_limit",
-      "solimp_limit",
-      "material",
-      "width",
-      "rgba",
-      "group",
-      "userdata",
-      "info"
-    ],
-    "mjsWrap": [
-      "element",
-      "type",
-      "info"
-    ],
-    "mjsActuator": [
-      "element",
-      "gaintype",
-      "gainprm",
-      "biastype",
-      "biasprm",
-      "dyntype",
-      "dynprm",
-      "actdim",
-      "actearly",
-      "trntype",
-      "gear",
-      "target",
-      "refsite",
-      "slidersite",
-      "cranklength",
-      "lengthrange",
-      "inheritrange",
-      "damping",
-      "armature",
-      "ctrllimited",
-      "ctrlrange",
-      "forcelimited",
-      "forcerange",
-      "actlimited",
-      "actrange",
-      "group",
-      "nsample",
-      "interp",
-      "delay",
-      "userdata",
-      "plugin",
-      "info"
-    ],
-    "mjsSensor": [
-      "element",
-      "type",
-      "objtype",
-      "objname",
-      "reftype",
-      "refname",
-      "intprm",
-      "datatype",
-      "needstage",
-      "dim",
-      "cutoff",
-      "noise",
-      "nsample",
-      "interp",
-      "delay",
-      "interval",
-      "userdata",
-      "plugin",
-      "info"
-    ],
-    "mjsNumeric": [
-      "element",
-      "data",
-      "size",
-      "info"
-    ],
-    "mjsText": [
-      "element",
-      "data",
-      "info"
-    ],
-    "mjsTuple": [
-      "element",
-      "objtype",
-      "objname",
-      "objprm",
-      "info"
-    ],
-    "mjsKey": [
-      "element",
-      "time",
-      "qpos",
-      "qvel",
-      "act",
-      "mpos",
-      "mquat",
-      "ctrl",
-      "info"
-    ],
-    "mjsDefault": [
-      "element",
-      "joint",
-      "geom",
-      "site",
-      "camera",
-      "light",
-      "flex",
-      "mesh",
-      "material",
-      "pair",
-      "equality",
-      "tendon",
-      "actuator"
-    ]
-  },
-  "setto_functions": {
-    "mjs_setToMotor": {
-      "params": []
-    },
-    "mjs_setToPosition": {
-      "params": [
-        {
-          "name": "kp",
-          "c_type": "double",
-          "array_dim": null
-        },
-        {
-          "name": "kv",
-          "c_type": "double",
-          "array_dim": 1
-        },
-        {
-          "name": "dampratio",
-          "c_type": "double",
-          "array_dim": 1
-        },
-        {
-          "name": "timeconst",
-          "c_type": "double",
-          "array_dim": 1
-        },
-        {
-          "name": "inheritrange",
-          "c_type": "double",
-          "array_dim": null
-        }
-      ]
-    },
-    "mjs_setToIntVelocity": {
-      "params": [
-        {
-          "name": "kp",
-          "c_type": "double",
-          "array_dim": null
-        },
-        {
-          "name": "kv",
-          "c_type": "double",
-          "array_dim": 1
-        },
-        {
-          "name": "dampratio",
-          "c_type": "double",
-          "array_dim": 1
-        },
-        {
-          "name": "timeconst",
-          "c_type": "double",
-          "array_dim": 1
-        },
-        {
-          "name": "inheritrange",
-          "c_type": "double",
-          "array_dim": null
-        }
-      ]
-    },
-    "mjs_setToVelocity": {
-      "params": [
-        {
-          "name": "kv",
-          "c_type": "double",
-          "array_dim": null
-        }
-      ]
-    },
-    "mjs_setToDamper": {
-      "params": [
-        {
-          "name": "kv",
-          "c_type": "double",
-          "array_dim": null
-        }
-      ]
-    },
-    "mjs_setToCylinder": {
-      "params": [
-        {
-          "name": "timeconst",
-          "c_type": "double",
-          "array_dim": null
-        },
-        {
-          "name": "bias",
-          "c_type": "double",
-          "array_dim": null
-        },
-        {
-          "name": "area",
-          "c_type": "double",
-          "array_dim": null
-        },
-        {
-          "name": "diameter",
-          "c_type": "double",
-          "array_dim": null
-        }
-      ]
-    },
-    "mjs_setToMuscle": {
-      "params": [
-        {
-          "name": "timeconst",
-          "c_type": "double",
-          "array_dim": 2
-        },
-        {
-          "name": "tausmooth",
-          "c_type": "double",
-          "array_dim": null
-        },
-        {
-          "name": "range",
-          "c_type": "double",
-          "array_dim": 2
-        },
-        {
-          "name": "force",
-          "c_type": "double",
-          "array_dim": null
-        },
-        {
-          "name": "scale",
-          "c_type": "double",
-          "array_dim": null
-        },
-        {
-          "name": "lmin",
-          "c_type": "double",
-          "array_dim": null
-        },
-        {
-          "name": "lmax",
-          "c_type": "double",
-          "array_dim": null
-        },
-        {
-          "name": "vmax",
-          "c_type": "double",
-          "array_dim": null
-        },
-        {
-          "name": "fpmax",
-          "c_type": "double",
-          "array_dim": null
-        },
-        {
-          "name": "fvmax",
-          "c_type": "double",
-          "array_dim": null
-        }
-      ]
-    },
-    "mjs_setToAdhesion": {
-      "params": [
-        {
-          "name": "gain",
-          "c_type": "double",
-          "array_dim": null
-        }
-      ]
-    },
-    "mjs_setToDCMotor": {
-      "params": [
-        {
-          "name": "motorconst",
-          "c_type": "double",
-          "array_dim": 2
-        },
-        {
-          "name": "resistance",
-          "c_type": "double",
-          "array_dim": null
-        },
-        {
-          "name": "nominal",
-          "c_type": "double",
-          "array_dim": 3
-        },
-        {
-          "name": "saturation",
-          "c_type": "double",
-          "array_dim": 3
-        },
-        {
-          "name": "inductance",
-          "c_type": "double",
-          "array_dim": 2
-        },
-        {
-          "name": "cogging",
-          "c_type": "double",
-          "array_dim": 3
-        },
-        {
-          "name": "controller",
-          "c_type": "double",
-          "array_dim": 6
-        },
-        {
-          "name": "thermal",
-          "c_type": "double",
-          "array_dim": 6
-        },
-        {
-          "name": "lugre",
-          "c_type": "double",
-          "array_dim": 5
-        },
-        {
-          "name": "input_mode",
-          "c_type": "int",
-          "array_dim": null
-        }
-      ]
-    }
-  }
-}
\ No newline at end of file
diff --git a/Source/URLab/Private/MuJoCo/Components/Actuators/MjActuator.cpp b/Source/URLab/Private/MuJoCo/Components/Actuators/MjActuator.cpp
index eb41488..2205fb2 100644
--- a/Source/URLab/Private/MuJoCo/Components/Actuators/MjActuator.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Actuators/MjActuator.cpp
@@ -21,6 +21,7 @@
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
 
 #include "MuJoCo/Components/Actuators/MjActuator.h"
+#include "MuJoCo/Utils/MjUtils.h"
 #include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
 #include "MuJoCo/Core/AMjManager.h"
@@ -53,7 +54,7 @@ void UMjActuator::ExportTo(mjsActuator* Element, mjsDefault* Default)
     if (!Element) return;
 
     // --- CODEGEN_EXPORT_START ---
-    if (!TargetName.IsEmpty()) mjs_setString(Element->target, TCHAR_TO_UTF8(*TargetName));
+    MjSetString(Element->target, TargetName);
     switch (TransmissionType)
     {
         case EMjActuatorTrnType::Joint:          Element->trntype = mjTRN_JOINT;         break;
@@ -66,11 +67,11 @@ void UMjActuator::ExportTo(mjsActuator* Element, mjsDefault* Default)
     }
     if (TransmissionType == EMjActuatorTrnType::SliderCrank && !SliderSite.IsEmpty())
     {
-        mjs_setString(Element->slidersite, TCHAR_TO_UTF8(*SliderSite));
+        MjSetStringRaw(Element->slidersite, SliderSite);
     }
     if (TransmissionType == EMjActuatorTrnType::Site && !RefSite.IsEmpty())
     {
-        mjs_setString(Element->refsite, TCHAR_TO_UTF8(*RefSite));
+        MjSetStringRaw(Element->refsite, RefSite);
     }
     if (bOverride_GainType)
     {
@@ -114,17 +115,17 @@ void UMjActuator::ExportTo(mjsActuator* Element, mjsDefault* Default)
     if (bOverride_ctrllimited) Element->ctrllimited = ctrllimited ? 1 : 0;
     if (bOverride_forcelimited) Element->forcelimited = forcelimited ? 1 : 0;
     if (bOverride_actlimited) Element->actlimited = actlimited ? 1 : 0;
-    if (bOverride_ctrlrange) { for (int32 i = 0; i < ctrlrange.Num(); ++i) Element->ctrlrange[i] = ctrlrange[i]; }
-    if (bOverride_forcerange) { for (int32 i = 0; i < forcerange.Num(); ++i) Element->forcerange[i] = forcerange[i]; }
-    if (bOverride_actrange) { for (int32 i = 0; i < actrange.Num(); ++i) Element->actrange[i] = actrange[i]; }
-    if (bOverride_lengthrange) { for (int32 i = 0; i < lengthrange.Num(); ++i) Element->lengthrange[i] = lengthrange[i]; }
-    if (bOverride_gear) { for (int32 i = 0; i < gear.Num(); ++i) Element->gear[i] = gear[i]; }
-    if (bOverride_damping) { for (int32 i = 0; i < damping.Num(); ++i) Element->damping[i] = damping[i]; }
+    if (bOverride_ctrlrange) { for (int32 i = 0; i < FMath::Min(ctrlrange.Num(), 2); ++i) Element->ctrlrange[i] = ctrlrange[i]; }
+    if (bOverride_forcerange) { for (int32 i = 0; i < FMath::Min(forcerange.Num(), 2); ++i) Element->forcerange[i] = forcerange[i]; }
+    if (bOverride_actrange) { for (int32 i = 0; i < FMath::Min(actrange.Num(), 2); ++i) Element->actrange[i] = actrange[i]; }
+    if (bOverride_lengthrange) { for (int32 i = 0; i < FMath::Min(lengthrange.Num(), 2); ++i) Element->lengthrange[i] = lengthrange[i]; }
+    if (bOverride_gear) { for (int32 i = 0; i < FMath::Min(gear.Num(), 6); ++i) Element->gear[i] = gear[i]; }
+    if (bOverride_damping) { for (int32 i = 0; i < FMath::Min(damping.Num(), 3); ++i) Element->damping[i] = damping[i]; }
     if (bOverride_armature) Element->armature = armature;
     if (bOverride_cranklength) Element->cranklength = cranklength;
-    if (bOverride_gainprm) { for (int32 i = 0; i < gainprm.Num(); ++i) Element->gainprm[i] = gainprm[i]; }
-    if (bOverride_biasprm) { for (int32 i = 0; i < biasprm.Num(); ++i) Element->biasprm[i] = biasprm[i]; }
-    if (bOverride_dynprm) { for (int32 i = 0; i < dynprm.Num(); ++i) Element->dynprm[i] = dynprm[i]; }
+    if (bOverride_gainprm) { for (int32 i = 0; i < FMath::Min(gainprm.Num(), 10); ++i) Element->gainprm[i] = gainprm[i]; }
+    if (bOverride_biasprm) { for (int32 i = 0; i < FMath::Min(biasprm.Num(), 10); ++i) Element->biasprm[i] = biasprm[i]; }
+    if (bOverride_dynprm) { for (int32 i = 0; i < FMath::Min(dynprm.Num(), 10); ++i) Element->dynprm[i] = dynprm[i]; }
     if (bOverride_actdim) Element->actdim = actdim;
     // --- CODEGEN_EXPORT_END ---
 }
@@ -135,34 +136,38 @@ void UMjActuator::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings&
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
+    MjXmlUtils::ReadAttrString(Node, TEXT("class"), MjClassName);
     { // xml_enum: gaintype -> EMjGainType
         FString S = Node->GetAttribute(TEXT("gaintype"));
         S = S.ToLower();
-        if      (S == TEXT("fixed")) GainType = EMjGainType::Fixed;
-        else if (S == TEXT("affine")) GainType = EMjGainType::Affine;
-        else if (S == TEXT("muscle")) GainType = EMjGainType::Muscle;
-        else if (S == TEXT("user")) GainType = EMjGainType::User;
-        if (!S.IsEmpty()) bOverride_GainType = true;
+        bool bMatched = false;
+        if      (S == TEXT("fixed")) { GainType = EMjGainType::Fixed; bMatched = true; }
+        else if (S == TEXT("affine")) { GainType = EMjGainType::Affine; bMatched = true; }
+        else if (S == TEXT("muscle")) { GainType = EMjGainType::Muscle; bMatched = true; }
+        else if (S == TEXT("user")) { GainType = EMjGainType::User; bMatched = true; }
+        if (bMatched) bOverride_GainType = true;
     }
     { // xml_enum: biastype -> EMjBiasType
         FString S = Node->GetAttribute(TEXT("biastype"));
         S = S.ToLower();
-        if      (S == TEXT("none")) BiasType = EMjBiasType::None;
-        else if (S == TEXT("affine")) BiasType = EMjBiasType::Affine;
-        else if (S == TEXT("muscle")) BiasType = EMjBiasType::Muscle;
-        else if (S == TEXT("user")) BiasType = EMjBiasType::User;
-        if (!S.IsEmpty()) bOverride_BiasType = true;
+        bool bMatched = false;
+        if      (S == TEXT("none")) { BiasType = EMjBiasType::None; bMatched = true; }
+        else if (S == TEXT("affine")) { BiasType = EMjBiasType::Affine; bMatched = true; }
+        else if (S == TEXT("muscle")) { BiasType = EMjBiasType::Muscle; bMatched = true; }
+        else if (S == TEXT("user")) { BiasType = EMjBiasType::User; bMatched = true; }
+        if (bMatched) bOverride_BiasType = true;
     }
     { // xml_enum: dyntype -> EMjDynType
         FString S = Node->GetAttribute(TEXT("dyntype"));
         S = S.ToLower();
-        if      (S == TEXT("none")) DynType = EMjDynType::None;
-        else if (S == TEXT("integrator")) DynType = EMjDynType::Integrator;
-        else if (S == TEXT("filter")) DynType = EMjDynType::Filter;
-        else if (S == TEXT("filterexact")) DynType = EMjDynType::FilterExact;
-        else if (S == TEXT("muscle")) DynType = EMjDynType::Muscle;
-        else if (S == TEXT("user")) DynType = EMjDynType::User;
-        if (!S.IsEmpty()) bOverride_DynType = true;
+        bool bMatched = false;
+        if      (S == TEXT("none")) { DynType = EMjDynType::None; bMatched = true; }
+        else if (S == TEXT("integrator")) { DynType = EMjDynType::Integrator; bMatched = true; }
+        else if (S == TEXT("filter")) { DynType = EMjDynType::Filter; bMatched = true; }
+        else if (S == TEXT("filterexact")) { DynType = EMjDynType::FilterExact; bMatched = true; }
+        else if (S == TEXT("muscle")) { DynType = EMjDynType::Muscle; bMatched = true; }
+        else if (S == TEXT("user")) { DynType = EMjDynType::User; bMatched = true; }
+        if (bMatched) bOverride_DynType = true;
     }
     MjXmlUtils::ReadAttrInt(Node, TEXT("group"), group, bOverride_group);
     MjXmlUtils::ReadAttrInt(Node, TEXT("nsample"), nsample, bOverride_nsample);
@@ -200,40 +205,33 @@ void UMjActuator::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings&
             TransmissionType = EMjActuatorTrnType::Tendon;
             TargetName = TrnTarget;
         }
-        if (MjXmlUtils::ReadAttrString(Node, TEXT("slidersite"), SliderSite))
-        {
-            TransmissionType = EMjActuatorTrnType::SliderCrank;
-        }
         if (MjXmlUtils::ReadAttrString(Node, TEXT("site"), TrnTarget))
         {
             TransmissionType = EMjActuatorTrnType::Site;
             TargetName = TrnTarget;
         }
-        MjXmlUtils::ReadAttrString(Node, TEXT("refsite"), RefSite);
         if (MjXmlUtils::ReadAttrString(Node, TEXT("body"), TrnTarget))
         {
             TransmissionType = EMjActuatorTrnType::Body;
             TargetName = TrnTarget;
         }
+        MjXmlUtils::ReadAttrString(Node, TEXT("refsite"), RefSite);
+        // SliderSite must run AFTER the main-target reads so it
+        // overrides the transmission type while preserving the
+        // TargetName captured above (per MJCF: slider-crank carries
+        // a main target attr PLUS slidersite).
+        if (MjXmlUtils::ReadAttrString(Node, TEXT("slidersite"), SliderSite))
+        {
+            TransmissionType = EMjActuatorTrnType::SliderCrank;
+        }
     }
     // --- CODEGEN_IMPORT_END ---
-
-    MjXmlUtils::ReadAttrString(Node, TEXT("class"), MjClassName);
 }
 
 void UMjActuator::Bind(mjModel* Model, mjData* Data, const FString& Prefix)
 {
     Super::Bind(Model, Data, Prefix);
-    m_ActuatorView = BindToView<ActuatorView>(Prefix);
-
-    if (m_ActuatorView.id != -1)
-    {
-        m_ID = m_ActuatorView.id;
-    }
-    else
-    {
-        UE_LOG(LogURLabBind, Warning, TEXT("[MjActuator] Actuator '%s' could not bind! Prefix: %s"), *GetName(), *Prefix);
-    }
+    BindAndCacheView(m_ActuatorView, Prefix);
 }
 
 // ----------------------------------------------------------------------------------
@@ -288,38 +286,38 @@ float UMjActuator::ResolveDesiredControl(uint8 Source) const
 
 float UMjActuator::GetForce() const
 {
-    if (m_ActuatorView.id != -1 && m_ActuatorView.force)
+    if (m_ActuatorView.id != -1 && m_ActuatorView.actuator_force)
     {
-        return (float)m_ActuatorView.force[0];
+        return (float)m_ActuatorView.actuator_force[0];
     }
     return 0.0f;
 }
 
 float UMjActuator::GetLength() const
 {
-    if (m_ActuatorView.id != -1 && m_ActuatorView.length)
+    if (m_ActuatorView.id != -1 && m_ActuatorView.actuator_length)
     {
-        return (float)m_ActuatorView.length[0];
+        return (float)m_ActuatorView.actuator_length[0];
     }
     return 0.0f;
 }
 
 float UMjActuator::GetVelocity() const
 {
-    if (m_ActuatorView.id != -1 && m_ActuatorView.velocity)
+    if (m_ActuatorView.id != -1 && m_ActuatorView.actuator_velocity)
     {
-        return (float)m_ActuatorView.velocity[0];
+        return (float)m_ActuatorView.actuator_velocity[0];
     }
     return 0.0f;
 }
 
 void UMjActuator::SetGear(const TArray<float>& NewGear)
 {
-    if (m_ActuatorView.id != -1 && m_ActuatorView.gear)
+    if (m_ActuatorView.id != -1 && m_ActuatorView.actuator_gear)
     {
         for(int i=0; i<NewGear.Num() && i<6; ++i)
         {
-             m_ActuatorView.gear[i] = (mjtNum)NewGear[i];
+             m_ActuatorView.actuator_gear[i] = (mjtNum)NewGear[i];
         }
     }
 }
@@ -327,17 +325,17 @@ void UMjActuator::SetGear(const TArray<float>& NewGear)
 TArray<float> UMjActuator::GetGear() const
 {
     TArray<float> Res;
-    if (m_ActuatorView.id != -1 && m_ActuatorView.gear)
+    if (m_ActuatorView.id != -1 && m_ActuatorView.actuator_gear)
     {
-        for(int i=0; i<6; ++i) Res.Add((float)m_ActuatorView.gear[i]);
+        for(int i=0; i<6; ++i) Res.Add((float)m_ActuatorView.actuator_gear[i]);
     }
     return Res;
 }
 
 FVector2D UMjActuator::GetControlRange() const
 {
-    if (m_ActuatorView.id < 0 || !m_ActuatorView.ctrlrange) return FVector2D::ZeroVector;
-    return FVector2D((float)m_ActuatorView.ctrlrange[0], (float)m_ActuatorView.ctrlrange[1]);
+    if (m_ActuatorView.id < 0 || !m_ActuatorView.actuator_ctrlrange) return FVector2D::ZeroVector;
+    return FVector2D((float)m_ActuatorView.actuator_ctrlrange[0], (float)m_ActuatorView.actuator_ctrlrange[1]);
 }
 
 float UMjActuator::GetActivation() const
@@ -403,19 +401,10 @@ TArray<FString> UMjActuator::GetTargetNameOptions() const
     return GetSiblingComponentOptions(this, FilterClass);
 }
 
-TArray<FString> UMjActuator::GetSliderSiteOptions() const
-{
-    return GetSiblingComponentOptions(this, UMjSite::StaticClass());
-}
-
-TArray<FString> UMjActuator::GetRefSiteOptions() const
-{
-    return GetSiblingComponentOptions(this, UMjSite::StaticClass());
-}
-
-TArray<FString> UMjActuator::GetDefaultClassOptions() const
-{
-    return GetSiblingComponentOptions(this, UMjDefault::StaticClass(), true);
-}
+// --- CODEGEN_EDITOR_OPTIONS_START ---
+TArray<FString> UMjActuator::GetSliderSiteOptions() const { return UMjComponent::GetSiblingComponentOptions(this, UMjSite::StaticClass()); }
+TArray<FString> UMjActuator::GetRefSiteOptions() const { return UMjComponent::GetSiblingComponentOptions(this, UMjSite::StaticClass()); }
+TArray<FString> UMjActuator::GetDefaultClassOptions() const { return UMjComponent::GetSiblingComponentOptions(this, UMjDefault::StaticClass(), true); }
+// --- CODEGEN_EDITOR_OPTIONS_END ---
 #endif
 
diff --git a/Source/URLab/Private/MuJoCo/Components/Actuators/MjDcMotorActuator.cpp b/Source/URLab/Private/MuJoCo/Components/Actuators/MjDcMotorActuator.cpp
index ac69092..36bc074 100644
--- a/Source/URLab/Private/MuJoCo/Components/Actuators/MjDcMotorActuator.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Actuators/MjDcMotorActuator.cpp
@@ -79,10 +79,11 @@ void UMjDcMotorActuator::ImportFromXml(const FXmlNode* Node, const FMjCompilerSe
     { // xml_enum: input -> EMjDcMotorInput
         FString S = Node->GetAttribute(TEXT("input"));
         S = S.ToLower();
-        if      (S == TEXT("voltage")) input = EMjDcMotorInput::Voltage;
-        else if (S == TEXT("position")) input = EMjDcMotorInput::Position;
-        else if (S == TEXT("velocity")) input = EMjDcMotorInput::Velocity;
-        if (!S.IsEmpty()) bOverride_input = true;
+        bool bMatched = false;
+        if      (S == TEXT("voltage")) { input = EMjDcMotorInput::Voltage; bMatched = true; }
+        else if (S == TEXT("position")) { input = EMjDcMotorInput::Position; bMatched = true; }
+        else if (S == TEXT("velocity")) { input = EMjDcMotorInput::Velocity; bMatched = true; }
+        if (bMatched) bOverride_input = true;
     }
     MjXmlUtils::ReadAttrFloatArray(Node, TEXT("motorconst"), motorconst, bOverride_motorconst);
     MjXmlUtils::ReadAttrFloat(Node, TEXT("resistance"), resistance, bOverride_resistance);
diff --git a/Source/URLab/Private/MuJoCo/Components/Actuators/MjGeneralActuator.cpp b/Source/URLab/Private/MuJoCo/Components/Actuators/MjGeneralActuator.cpp
index 1d664ce..23c5315 100644
--- a/Source/URLab/Private/MuJoCo/Components/Actuators/MjGeneralActuator.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Actuators/MjGeneralActuator.cpp
@@ -41,7 +41,7 @@ void UMjGeneralActuator::ExportTo(mjsActuator* Element, mjsDefault* def)
 
         // --- CODEGEN_EXPORT_START ---
     if (bOverride_actearly) Element->actearly = actearly ? 1 : 0;
-    // --- CODEGEN_EXPORT_END ---
+        // --- CODEGEN_EXPORT_END ---
 }
 
 void UMjGeneralActuator::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
diff --git a/Source/URLab/Private/MuJoCo/Components/Actuators/MjMotorActuator.cpp b/Source/URLab/Private/MuJoCo/Components/Actuators/MjMotorActuator.cpp
index 81d5efb..ec4fe36 100644
--- a/Source/URLab/Private/MuJoCo/Components/Actuators/MjMotorActuator.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Actuators/MjMotorActuator.cpp
@@ -44,7 +44,6 @@ void UMjMotorActuator::ImportFromXml(const FXmlNode* Node, const FMjCompilerSett
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
     // --- CODEGEN_IMPORT_END ---
 }
 
diff --git a/Source/URLab/Private/MuJoCo/Components/Bodies/MjBody.cpp b/Source/URLab/Private/MuJoCo/Components/Bodies/MjBody.cpp
index 457b5e5..29bcfaf 100644
--- a/Source/URLab/Private/MuJoCo/Components/Bodies/MjBody.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Bodies/MjBody.cpp
@@ -97,11 +97,11 @@ void UMjBody::ExportTo(mjsBody* Element, mjsDefault* Default)
     if (!Element) return;
 
     // --- CODEGEN_EXPORT_START ---
-    if (SleepPolicy != EMjBodySleepPolicy::Default)
+    if (bOverride_SleepPolicy)
     {
         Element->sleep = static_cast<mjtSleepPolicy>(static_cast<uint8>(SleepPolicy));
     }
-    if (bOverride_childclass && !childclass.IsEmpty()) mjs_setString(Element->childclass, TCHAR_TO_UTF8(*childclass));
+    if (bOverride_childclass) MjSetString(Element->childclass, childclass);
     if (bOverride_mocap) Element->mocap = mocap ? 1 : 0;
     if (bOverride_gravcomp) Element->gravcomp = gravcomp;
     // --- CODEGEN_EXPORT_END ---
@@ -228,6 +228,7 @@ void UMjBody::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& Com
     if (!Node) return;
 
         // --- CODEGEN_IMPORT_START ---
+    MjXmlUtils::ReadAttrString(Node, TEXT("name"), MjName);
     if (MjXmlUtils::ReadAttrString(Node, TEXT("childclass"), childclass)) bOverride_childclass = true;
     MjXmlUtils::ReadAttrBool(Node, TEXT("mocap"), mocap, bOverride_mocap);
     MjXmlUtils::ReadAttrFloat(Node, TEXT("gravcomp"), gravcomp, bOverride_gravcomp);
@@ -240,19 +241,16 @@ void UMjBody::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& Com
             bOverride_Quat = true;
         }
     }
-    { // canonicalize body.sleep -> EMjBodySleepPolicy
+    { // canonicalize body.sleep -> EMjBodySleepPolicy + bOverride_SleepPolicy
         FString S = Node->GetAttribute(TEXT("sleep"));
         S = S.ToLower();
-        if      (S == TEXT("never"))   SleepPolicy = EMjBodySleepPolicy::Never;
-        else if (S == TEXT("allowed")) SleepPolicy = EMjBodySleepPolicy::Allowed;
-        else if (S == TEXT("init"))    SleepPolicy = EMjBodySleepPolicy::InitAsleep;
+        if      (S == TEXT("never"))   { SleepPolicy = EMjBodySleepPolicy::Never;      bOverride_SleepPolicy = true; }
+        else if (S == TEXT("allowed")) { SleepPolicy = EMjBodySleepPolicy::Allowed;    bOverride_SleepPolicy = true; }
+        else if (S == TEXT("init"))    { SleepPolicy = EMjBodySleepPolicy::InitAsleep; bOverride_SleepPolicy = true; }
     }
     if (bOverride_Pos)  SetRelativeLocation(Pos);
     if (bOverride_Quat) SetRelativeRotation(Quat);
-    // --- CODEGEN_IMPORT_END ---
-
-    // name attribute → store in MjName for explicit override
-    MjXmlUtils::ReadAttrString(Node, TEXT("name"), MjName);
+        // --- CODEGEN_IMPORT_END ---
 }
 
 void UMjBody::Bind(mjModel* Model, mjData* Data, const FString& Prefix)
@@ -261,22 +259,13 @@ void UMjBody::Bind(mjModel* Model, mjData* Data, const FString& Prefix)
 
 	if (Model && Data)
     {
-        m_BodyView = BindToView<BodyView>(Prefix);
+        BindAndCacheView(m_BodyView, Prefix);
 
-        if (m_BodyView.id != -1)
-        {
-            m_ID = m_BodyView.id;
-            m_IsSetup = true;
-            SetComponentTickEnabled(true);
-        }
-        else
-        {
-            UE_LOG(LogURLabBind, Warning, TEXT("MjBody::Bind() - FAILED to find body '%s'"), *GetName());
-            m_IsSetup = false;
-            SetComponentTickEnabled(false);
-        }
+        const bool bBound = m_BodyView.id != -1;
+        m_IsSetup = bBound;
+        SetComponentTickEnabled(bBound);
 
-        if (mocap && m_ID >= 0)
+        if (bBound && mocap)
         {
             int mocapid = Model->body_mocapid[m_ID];
             if (mocapid >= 0)
@@ -432,8 +421,7 @@ void UMjBody::RegisterToSpec(FMujocoSpecWrapper& Wrapper, mjsBody* ParentBody)
 }
 
 #if WITH_EDITOR
-TArray<FString> UMjBody::GetChildClassOptions() const
-{
-    return UMjComponent::GetSiblingComponentOptions(this, UMjDefault::StaticClass(), true);
-}
+// --- CODEGEN_EDITOR_OPTIONS_START ---
+TArray<FString> UMjBody::GetChildClassOptions() const { return UMjComponent::GetSiblingComponentOptions(this, UMjDefault::StaticClass(), true); }
+// --- CODEGEN_EDITOR_OPTIONS_END ---
 #endif
diff --git a/Source/URLab/Private/MuJoCo/Components/Bodies/MjFrame.cpp b/Source/URLab/Private/MuJoCo/Components/Bodies/MjFrame.cpp
index 5ca3fb0..7fa6093 100644
--- a/Source/URLab/Private/MuJoCo/Components/Bodies/MjFrame.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Bodies/MjFrame.cpp
@@ -64,7 +64,7 @@ void UMjFrame::ExportTo(mjsFrame* Element, mjsDefault* Default)
     if (!Element) return;
 
     // --- CODEGEN_EXPORT_START ---
-    if (bOverride_childclass && !childclass.IsEmpty()) mjs_setString(Element->childclass, TCHAR_TO_UTF8(*childclass));
+    if (bOverride_childclass) MjSetString(Element->childclass, childclass);
     // --- CODEGEN_EXPORT_END ---
 }
 
diff --git a/Source/URLab/Private/MuJoCo/Components/Constraints/MjEquality.cpp b/Source/URLab/Private/MuJoCo/Components/Constraints/MjEquality.cpp
index 547d11a..8023709 100644
--- a/Source/URLab/Private/MuJoCo/Components/Constraints/MjEquality.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Constraints/MjEquality.cpp
@@ -103,6 +103,7 @@ void UMjEquality::ExportTo(mjsEquality* Element)
     // anchor) or site-to-site. URLab's parser collates site1/site2 into
     // Obj1/Obj2 alongside body1/body2; we use the non-empty site1
     // UPROPERTY as the discriminator for "this was a site-mode equality".
+    // --- CODEGEN_OBJTYPE_DISPATCH_START ---
     switch (EqualityType)
     {
         case EMjEqualityType::Connect:
@@ -124,11 +125,12 @@ void UMjEquality::ExportTo(mjsEquality* Element)
             Element->objtype = mjOBJ_UNKNOWN;
             break;
     }
+    // --- CODEGEN_OBJTYPE_DISPATCH_END ---
 
     // --- CODEGEN_EXPORT_START ---
     if (bOverride_active) Element->active = active;
-    if (bOverride_solref) { for (int32 i = 0; i < solref.Num(); ++i) Element->solref[i] = solref[i]; }
-    if (bOverride_solimp) { for (int32 i = 0; i < solimp.Num(); ++i) Element->solimp[i] = solimp[i]; }
+    if (bOverride_solref) { for (int32 i = 0; i < FMath::Min(solref.Num(), 2); ++i) Element->solref[i] = solref[i]; }
+    if (bOverride_solimp) { for (int32 i = 0; i < FMath::Min(solimp.Num(), 5); ++i) Element->solimp[i] = solimp[i]; }
     if (((EqualityType == EMjEqualityType::Connect) || (EqualityType == EMjEqualityType::Weld)) && bOverride_anchor)
     {
         for (int32 i = 0; i < anchor.Num() && i < 3; ++i)
@@ -152,8 +154,8 @@ void UMjEquality::ExportTo(mjsEquality* Element)
     {
         Element->data[10] = (mjtNum)torquescale;
     }
-    if (!Obj1.IsEmpty()) mjs_setString(Element->name1, TCHAR_TO_UTF8(*Obj1));
-    if (!Obj2.IsEmpty()) mjs_setString(Element->name2, TCHAR_TO_UTF8(*Obj2));
+    MjSetString(Element->name1, Obj1);
+    MjSetString(Element->name2, Obj2);
     // --- CODEGEN_EXPORT_END ---
 }
 
diff --git a/Source/URLab/Private/MuJoCo/Components/Deformable/MjFlexcomp.cpp b/Source/URLab/Private/MuJoCo/Components/Deformable/MjFlexcomp.cpp
index 6367d5b..b9e6e2c 100644
--- a/Source/URLab/Private/MuJoCo/Components/Deformable/MjFlexcomp.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Deformable/MjFlexcomp.cpp
@@ -62,27 +62,29 @@ void UMjFlexcomp::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings&
     { // xml_enum: type -> EMjFlexcompType
         FString S = Node->GetAttribute(TEXT("type"));
         S = S.ToLower();
-        if      (S == TEXT("grid")) FlexcompType = EMjFlexcompType::Grid;
-        else if (S == TEXT("box")) FlexcompType = EMjFlexcompType::Box;
-        else if (S == TEXT("cylinder")) FlexcompType = EMjFlexcompType::Cylinder;
-        else if (S == TEXT("ellipsoid")) FlexcompType = EMjFlexcompType::Ellipsoid;
-        else if (S == TEXT("square")) FlexcompType = EMjFlexcompType::Square;
-        else if (S == TEXT("disc")) FlexcompType = EMjFlexcompType::Disc;
-        else if (S == TEXT("circle")) FlexcompType = EMjFlexcompType::Circle;
-        else if (S == TEXT("mesh")) FlexcompType = EMjFlexcompType::Mesh;
-        else if (S == TEXT("gmsh")) FlexcompType = EMjFlexcompType::Gmsh;
-        else if (S == TEXT("direct")) FlexcompType = EMjFlexcompType::Direct;
-        if (!S.IsEmpty()) bOverride_FlexcompType = true;
+        bool bMatched = false;
+        if      (S == TEXT("grid")) { FlexcompType = EMjFlexcompType::Grid; bMatched = true; }
+        else if (S == TEXT("box")) { FlexcompType = EMjFlexcompType::Box; bMatched = true; }
+        else if (S == TEXT("cylinder")) { FlexcompType = EMjFlexcompType::Cylinder; bMatched = true; }
+        else if (S == TEXT("ellipsoid")) { FlexcompType = EMjFlexcompType::Ellipsoid; bMatched = true; }
+        else if (S == TEXT("square")) { FlexcompType = EMjFlexcompType::Square; bMatched = true; }
+        else if (S == TEXT("disc")) { FlexcompType = EMjFlexcompType::Disc; bMatched = true; }
+        else if (S == TEXT("circle")) { FlexcompType = EMjFlexcompType::Circle; bMatched = true; }
+        else if (S == TEXT("mesh")) { FlexcompType = EMjFlexcompType::Mesh; bMatched = true; }
+        else if (S == TEXT("gmsh")) { FlexcompType = EMjFlexcompType::Gmsh; bMatched = true; }
+        else if (S == TEXT("direct")) { FlexcompType = EMjFlexcompType::Direct; bMatched = true; }
+        if (bMatched) bOverride_FlexcompType = true;
     }
     { // xml_enum: dof -> EMjFlexcompDof
         FString S = Node->GetAttribute(TEXT("dof"));
         S = S.ToLower();
-        if      (S == TEXT("full")) FlexcompDof = EMjFlexcompDof::Full;
-        else if (S == TEXT("radial")) FlexcompDof = EMjFlexcompDof::Radial;
-        else if (S == TEXT("trilinear")) FlexcompDof = EMjFlexcompDof::Trilinear;
-        else if (S == TEXT("quadratic")) FlexcompDof = EMjFlexcompDof::Quadratic;
-        else if (S == TEXT("2d")) FlexcompDof = EMjFlexcompDof::TwoD;
-        if (!S.IsEmpty()) bOverride_FlexcompDof = true;
+        bool bMatched = false;
+        if      (S == TEXT("full")) { FlexcompDof = EMjFlexcompDof::Full; bMatched = true; }
+        else if (S == TEXT("radial")) { FlexcompDof = EMjFlexcompDof::Radial; bMatched = true; }
+        else if (S == TEXT("trilinear")) { FlexcompDof = EMjFlexcompDof::Trilinear; bMatched = true; }
+        else if (S == TEXT("quadratic")) { FlexcompDof = EMjFlexcompDof::Quadratic; bMatched = true; }
+        else if (S == TEXT("2d")) { FlexcompDof = EMjFlexcompDof::TwoD; bMatched = true; }
+        if (bMatched) bOverride_FlexcompDof = true;
     }
     MjXmlUtils::ReadAttrInt(Node, TEXT("group"), group, bOverride_group);
     MjXmlUtils::ReadAttrInt(Node, TEXT("dim"), dim, bOverride_dim);
diff --git a/Source/URLab/Private/MuJoCo/Components/Geometry/MjGeom.cpp b/Source/URLab/Private/MuJoCo/Components/Geometry/MjGeom.cpp
index 6eb989c..67fec3d 100644
--- a/Source/URLab/Private/MuJoCo/Components/Geometry/MjGeom.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Geometry/MjGeom.cpp
@@ -76,33 +76,37 @@ void UMjGeom::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& Com
     // on Type to pick size[1] vs size[2] for the half-length write.
 
         // --- CODEGEN_IMPORT_START ---
+    MjXmlUtils::ReadAttrString(Node, TEXT("class"), MjClassName);
     { // xml_enum: type -> EMjGeomType
         FString S = Node->GetAttribute(TEXT("type"));
         S = S.ToLower();
-        if      (S == TEXT("plane")) Type = EMjGeomType::Plane;
-        else if (S == TEXT("hfield")) Type = EMjGeomType::Hfield;
-        else if (S == TEXT("sphere")) Type = EMjGeomType::Sphere;
-        else if (S == TEXT("capsule")) Type = EMjGeomType::Capsule;
-        else if (S == TEXT("ellipsoid")) Type = EMjGeomType::Ellipsoid;
-        else if (S == TEXT("cylinder")) Type = EMjGeomType::Cylinder;
-        else if (S == TEXT("box")) Type = EMjGeomType::Box;
-        else if (S == TEXT("mesh")) Type = EMjGeomType::Mesh;
-        else if (S == TEXT("sdf")) Type = EMjGeomType::SDF;
-        if (!S.IsEmpty()) bOverride_Type = true;
+        bool bMatched = false;
+        if      (S == TEXT("plane")) { Type = EMjGeomType::Plane; bMatched = true; }
+        else if (S == TEXT("hfield")) { Type = EMjGeomType::Hfield; bMatched = true; }
+        else if (S == TEXT("sphere")) { Type = EMjGeomType::Sphere; bMatched = true; }
+        else if (S == TEXT("capsule")) { Type = EMjGeomType::Capsule; bMatched = true; }
+        else if (S == TEXT("ellipsoid")) { Type = EMjGeomType::Ellipsoid; bMatched = true; }
+        else if (S == TEXT("cylinder")) { Type = EMjGeomType::Cylinder; bMatched = true; }
+        else if (S == TEXT("box")) { Type = EMjGeomType::Box; bMatched = true; }
+        else if (S == TEXT("mesh")) { Type = EMjGeomType::Mesh; bMatched = true; }
+        else if (S == TEXT("sdf")) { Type = EMjGeomType::SDF; bMatched = true; }
+        if (bMatched) bOverride_Type = true;
     }
     { // xml_enum: shellinertia -> EMjGeomInertia
         FString S = Node->GetAttribute(TEXT("shellinertia"));
         S = S.ToLower();
-        if      (S == TEXT("false")) ShellInertia = EMjGeomInertia::Volume;
-        else if (S == TEXT("true")) ShellInertia = EMjGeomInertia::Shell;
-        if (!S.IsEmpty()) bOverride_ShellInertia = true;
+        bool bMatched = false;
+        if      (S == TEXT("false")) { ShellInertia = EMjGeomInertia::Volume; bMatched = true; }
+        else if (S == TEXT("true")) { ShellInertia = EMjGeomInertia::Shell; bMatched = true; }
+        if (bMatched) bOverride_ShellInertia = true;
     }
     { // xml_enum: fluidshape -> EMjFluidShape
         FString S = Node->GetAttribute(TEXT("fluidshape"));
         S = S.ToLower();
-        if      (S == TEXT("none")) FluidShape = EMjFluidShape::None;
-        else if (S == TEXT("ellipsoid")) FluidShape = EMjFluidShape::Ellipsoid;
-        if (!S.IsEmpty()) bOverride_FluidShape = true;
+        bool bMatched = false;
+        if      (S == TEXT("none")) { FluidShape = EMjFluidShape::None; bMatched = true; }
+        else if (S == TEXT("ellipsoid")) { FluidShape = EMjFluidShape::Ellipsoid; bMatched = true; }
+        if (bMatched) bOverride_FluidShape = true;
     }
     MjXmlUtils::ReadAttrInt(Node, TEXT("contype"), contype, bOverride_contype);
     MjXmlUtils::ReadAttrInt(Node, TEXT("conaffinity"), conaffinity, bOverride_conaffinity);
@@ -121,7 +125,7 @@ void UMjGeom::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& Com
     MjXmlUtils::ReadAttrFloat(Node, TEXT("gap"), gap, bOverride_gap);
     if (MjXmlUtils::ReadAttrString(Node, TEXT("hfield"), hfield)) bOverride_hfield = true;
     if (MjXmlUtils::ReadAttrString(Node, TEXT("mesh"), mesh)) bOverride_mesh = true;
-    MjXmlUtils::ReadAttrBool(Node, TEXT("fitscale"), fitscale, bOverride_fitscale);
+    MjXmlUtils::ReadAttrDouble(Node, TEXT("fitscale"), fitscale, bOverride_fitscale);
     MjXmlUtils::ReadAttrColor(Node, TEXT("rgba"), rgba, bOverride_rgba);
     MjXmlUtils::ReadAttrFloatArray(Node, TEXT("fluidcoef"), fluidcoef, bOverride_fluidcoef);
     MjUtils::ReadVec3InMeters(Node, TEXT("pos"), Pos, bOverride_Pos);
@@ -148,10 +152,7 @@ void UMjGeom::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& Com
     }
     if (bOverride_Pos)  SetRelativeLocation(Pos);
     if (bOverride_Quat) SetRelativeRotation(Quat);
-    // --- CODEGEN_IMPORT_END ---
-
-    // MuJoCo class inheritance
-    MjXmlUtils::ReadAttrString(Node, TEXT("class"), MjClassName);
+        // --- CODEGEN_IMPORT_END ---
 
     // Implicit mesh-type detection (mesh attr present but type wasn't set)
     if (!bOverride_Type && !mesh.IsEmpty())
@@ -179,29 +180,30 @@ void UMjGeom::ExportTo(mjsGeom* Element, mjsDefault* Default)
     // Type isn't overridden locally, fall back to the default's geom type.
     // (The codegen-owned xml_enum export below writes Element->type only when
     // bOverride_Type is true — exactly the desired behavior.)
+    // --- CODEGEN_GEOM_FINAL_TYPE_START ---
     int FinalType = bOverride_Type
-        ? static_cast<int>(Type) /* placeholder; remapped below */
+        ? mjGEOM_PLANE  /* placeholder; remapped below */
         : (Default ? Default->geom->type : mjGEOM_MESH);
     if (bOverride_Type)
     {
-        switch(Type)
+        switch (Type)
         {
-            case EMjGeomType::Plane:    FinalType = mjGEOM_PLANE;    break;
-            case EMjGeomType::Hfield:   FinalType = mjGEOM_HFIELD;   break;
-            case EMjGeomType::Sphere:   FinalType = mjGEOM_SPHERE;   break;
-            case EMjGeomType::Capsule:  FinalType = mjGEOM_CAPSULE;  break;
-            case EMjGeomType::Ellipsoid:FinalType = mjGEOM_ELLIPSOID;break;
+            case EMjGeomType::Plane: FinalType = mjGEOM_PLANE; break;
+            case EMjGeomType::Hfield: FinalType = mjGEOM_HFIELD; break;
+            case EMjGeomType::Sphere: FinalType = mjGEOM_SPHERE; break;
+            case EMjGeomType::Capsule: FinalType = mjGEOM_CAPSULE; break;
+            case EMjGeomType::Ellipsoid: FinalType = mjGEOM_ELLIPSOID; break;
             case EMjGeomType::Cylinder: FinalType = mjGEOM_CYLINDER; break;
-            case EMjGeomType::Box:      FinalType = mjGEOM_BOX;      break;
-            case EMjGeomType::Mesh:     FinalType = mjGEOM_MESH;     break;
-            case EMjGeomType::SDF:      FinalType = mjGEOM_SDF;      break;
+            case EMjGeomType::Box: FinalType = mjGEOM_BOX; break;
+            case EMjGeomType::Mesh: FinalType = mjGEOM_MESH; break;
+            case EMjGeomType::SDF: FinalType = mjGEOM_SDF; break;
         }
     }
-
     if (FinalType == mjGEOM_MESH && !MeshName.IsEmpty())
     {
-        mjs_setString(Element->meshname, TCHAR_TO_UTF8(*MeshName));
+        MjSetStringRaw(Element->meshname, MeshName);
     }
+    // --- CODEGEN_GEOM_FINAL_TYPE_END ---
 
     // size: codegen-owned TArray<float>; the default per-attr export writes
     // Element->size[i] = size[i] up to size.Num(). Clamped to 3 by mjsGeom's
@@ -260,37 +262,29 @@ void UMjGeom::ExportTo(mjsGeom* Element, mjsDefault* Default)
     if (bOverride_condim) Element->condim = condim;
     if (bOverride_group) Element->group = group;
     if (bOverride_priority) Element->priority = priority;
-    if (bOverride_size) { for (int32 i = 0; i < size.Num(); ++i) { if (size[i] != -1.0f) Element->size[i] = size[i]; } }
-    if (bOverride_friction) { for (int32 i = 0; i < friction.Num(); ++i) Element->friction[i] = friction[i]; }
+    if (bOverride_size) { for (int32 i = 0; i < FMath::Min(size.Num(), 3); ++i) { if (size[i] != -1.0f) Element->size[i] = size[i]; } }
+    if (bOverride_friction) { for (int32 i = 0; i < FMath::Min(friction.Num(), 3); ++i) Element->friction[i] = friction[i]; }
     if (bOverride_mass) Element->mass = mass;
     if (bOverride_density) Element->density = density;
     if (bOverride_solmix) Element->solmix = solmix;
-    if (bOverride_solref) { for (int32 i = 0; i < solref.Num(); ++i) Element->solref[i] = solref[i]; }
-    if (bOverride_solimp) { for (int32 i = 0; i < solimp.Num(); ++i) Element->solimp[i] = solimp[i]; }
+    if (bOverride_solref) { for (int32 i = 0; i < FMath::Min(solref.Num(), 2); ++i) Element->solref[i] = solref[i]; }
+    if (bOverride_solimp) { for (int32 i = 0; i < FMath::Min(solimp.Num(), 5); ++i) Element->solimp[i] = solimp[i]; }
     if (bOverride_margin) Element->margin = margin;
     if (bOverride_gap) Element->gap = gap;
-    if (bOverride_mesh && !mesh.IsEmpty()) mjs_setString(Element->meshname, TCHAR_TO_UTF8(*mesh));
-    if (bOverride_fitscale) Element->fitscale = fitscale ? 1 : 0;
+    if (bOverride_mesh) MjSetString(Element->meshname, mesh);
+    if (bOverride_fitscale) Element->fitscale = fitscale;
     if (bOverride_rgba) { Element->rgba[0] = rgba.R; Element->rgba[1] = rgba.G; Element->rgba[2] = rgba.B; Element->rgba[3] = rgba.A; }
-    if (bOverride_fluidcoef) { for (int32 i = 0; i < fluidcoef.Num(); ++i) Element->fluid_coefs[i] = fluidcoef[i]; }
-    // --- CODEGEN_EXPORT_END ---
+    if (bOverride_fluidcoef) { for (int32 i = 0; i < FMath::Min(fluidcoef.Num(), 5); ++i) Element->fluid_coefs[i] = fluidcoef[i]; }
+        // --- CODEGEN_EXPORT_END ---
 }
 
 void UMjGeom::Bind(mjModel* Model, mjData* Data, const FString& Prefix)
 {
     Super::Bind(Model, Data, Prefix);
-    m_GeomView = BindToView<GeomView>(Prefix);
-
+    BindAndCacheView(m_GeomView, Prefix);
     if (m_GeomView.id != -1)
     {
-        m_ID = m_GeomView.id;
         SyncUnrealTransformFromMj();
-        UE_LOG(LogURLabBind, Log, TEXT("[MjGeom] Successfully bound '%s' to ID %d (MjName: %s)"), *GetName(), m_ID, *MjName);
-    }
-    else
-    {
-        UE_LOG(LogURLabBind, Warning, TEXT("[MjGeom] Geom '%s' FAILED bind. Prefix: %s, MjName: %s"), 
-            *GetName(), *Prefix, *MjName);
     }
 }
 
@@ -299,10 +293,10 @@ void UMjGeom::UpdateGlobalTransform()
     if (m_GeomView._m && m_GeomView._d && m_GeomView.id >= 0)
     {
         // Get world position from MuJoCo
-        FVector WorldPos = MjUtils::MjToUEPosition(m_GeomView.xpos);
+        FVector WorldPos = MjUtils::MjToUEPosition(m_GeomView.geom_xpos);
         FQuat WorldRot;
         mjtNum quat[4];
-        mju_mat2Quat(quat, m_GeomView.xmat);
+        mju_mat2Quat(quat, m_GeomView.geom_xmat);
         WorldRot = MjUtils::MjToUERotation(quat);
         
         SetWorldLocation(WorldPos);
@@ -314,7 +308,7 @@ FVector UMjGeom::GetWorldLocation() const
 {
     if (m_GeomView._m && m_GeomView._d && m_GeomView.id >= 0)
     {
-        return MjUtils::MjToUEPosition(m_GeomView.xpos);
+        return MjUtils::MjToUEPosition(m_GeomView.geom_xpos);
     }
     return GetComponentLocation();
 }
@@ -325,9 +319,9 @@ void UMjGeom::SetFriction(float NewFriction)
     bOverride_friction = true;
     
     // Update runtime model if bound
-    if (m_GeomView._m && m_GeomView._d && m_GeomView.id >= 0 && m_GeomView.friction)
+    if (m_GeomView._m && m_GeomView._d && m_GeomView.id >= 0 && m_GeomView.geom_friction)
     {
-        m_GeomView.friction[0] = NewFriction;
+        m_GeomView.geom_friction[0] = NewFriction;
     }
 }
 
@@ -935,8 +929,7 @@ void UMjGeom::RemoveDecomposition() {}
 #endif
 
 #if WITH_EDITOR
-TArray<FString> UMjGeom::GetDefaultClassOptions() const
-{
-    return GetSiblingComponentOptions(this, UMjDefault::StaticClass(), true);
-}
+// --- CODEGEN_EDITOR_OPTIONS_START ---
+TArray<FString> UMjGeom::GetDefaultClassOptions() const { return UMjComponent::GetSiblingComponentOptions(this, UMjDefault::StaticClass(), true); }
+// --- CODEGEN_EDITOR_OPTIONS_END ---
 #endif
diff --git a/Source/URLab/Private/MuJoCo/Components/Geometry/MjSite.cpp b/Source/URLab/Private/MuJoCo/Components/Geometry/MjSite.cpp
index 5b27f4f..0896a87 100644
--- a/Source/URLab/Private/MuJoCo/Components/Geometry/MjSite.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Geometry/MjSite.cpp
@@ -67,10 +67,12 @@ void UMjSite::ExportTo(mjsSite* Element, mjsDefault* def)
         case EMjSiteType::Ellipsoid: Element->type = (mjtGeom)mjGEOM_ELLIPSOID; break;
         case EMjSiteType::Cylinder: Element->type = (mjtGeom)mjGEOM_CYLINDER; break;
         case EMjSiteType::Box: Element->type = (mjtGeom)mjGEOM_BOX; break;
+        case EMjSiteType::Mesh: Element->type = (mjtGeom)mjGEOM_MESH; break;
+        case EMjSiteType::Hfield: Element->type = (mjtGeom)mjGEOM_HFIELD; break;
         default: break;
     }
     if (bOverride_group) Element->group = group;
-    if (bOverride_size) { for (int32 i = 0; i < size.Num(); ++i) { if (size[i] != -1.0f) Element->size[i] = size[i]; } }
+    if (bOverride_size) { for (int32 i = 0; i < FMath::Min(size.Num(), 3); ++i) { if (size[i] != -1.0f) Element->size[i] = size[i]; } }
     if (bOverride_rgba) { Element->rgba[0] = rgba.R; Element->rgba[1] = rgba.G; Element->rgba[2] = rgba.B; Element->rgba[3] = rgba.A; }
     // --- CODEGEN_EXPORT_END ---
 }
@@ -89,11 +91,13 @@ void UMjSite::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& Com
     { // xml_enum: type -> EMjSiteType
         FString S = Node->GetAttribute(TEXT("type"));
         S = S.ToLower();
-        if      (S == TEXT("sphere")) Type = EMjSiteType::Sphere;
-        else if (S == TEXT("capsule")) Type = EMjSiteType::Capsule;
-        else if (S == TEXT("ellipsoid")) Type = EMjSiteType::Ellipsoid;
-        else if (S == TEXT("cylinder")) Type = EMjSiteType::Cylinder;
-        else if (S == TEXT("box")) Type = EMjSiteType::Box;
+        if      (S == TEXT("sphere")) { Type = EMjSiteType::Sphere; }
+        else if (S == TEXT("capsule")) { Type = EMjSiteType::Capsule; }
+        else if (S == TEXT("ellipsoid")) { Type = EMjSiteType::Ellipsoid; }
+        else if (S == TEXT("cylinder")) { Type = EMjSiteType::Cylinder; }
+        else if (S == TEXT("box")) { Type = EMjSiteType::Box; }
+        else if (S == TEXT("mesh")) { Type = EMjSiteType::Mesh; }
+        else if (S == TEXT("hfield")) { Type = EMjSiteType::Hfield; }
     }
     MjXmlUtils::ReadAttrInt(Node, TEXT("group"), group, bOverride_group);
     if (MjXmlUtils::ReadAttrString(Node, TEXT("material"), material)) bOverride_material = true;
@@ -123,7 +127,7 @@ void UMjSite::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& Com
     }
     if (bOverride_Pos)  SetRelativeLocation(Pos);
     if (bOverride_Quat) SetRelativeRotation(Quat);
-    // --- CODEGEN_IMPORT_END ---
+        // --- CODEGEN_IMPORT_END ---
 }
 
 void UMjSite::RegisterToSpec(FMujocoSpecWrapper& Wrapper, mjsBody* ParentBody)
@@ -142,24 +146,13 @@ void UMjSite::RegisterToSpec(FMujocoSpecWrapper& Wrapper, mjsBody* ParentBody)
 void UMjSite::Bind(mjModel* model, mjData* data, const FString& Prefix)
 {
     Super::Bind(model, data, Prefix);
-    m_SiteView = BindToView<SiteView>(Prefix);
-
-    if (m_SiteView.id != -1)
-    {
-        m_ID = m_SiteView.id;
-        UE_LOG(LogURLabBind, Log, TEXT("[MjSite] Successfully bound '%s' to ID %d (MjName: %s)"), *GetName(), m_ID, *MjName);
-    }
-    else
-    {
-        UE_LOG(LogURLabBind, Warning, TEXT("[MjSite] Site '%s' FAILED bind. Prefix: %s, MjName: %s"), *GetName(), *Prefix, *MjName);
-    }
+    BindAndCacheView(m_SiteView, Prefix);
 }
 
 #if WITH_EDITOR
-TArray<FString> UMjSite::GetDefaultClassOptions() const
-{
-    return GetSiblingComponentOptions(this, UMjDefault::StaticClass(), true);
-}
+// --- CODEGEN_EDITOR_OPTIONS_START ---
+TArray<FString> UMjSite::GetDefaultClassOptions() const { return UMjComponent::GetSiblingComponentOptions(this, UMjDefault::StaticClass(), true); }
+// --- CODEGEN_EDITOR_OPTIONS_END ---
 #endif
 
 // --- Multi-UCLASS subclass constructors --------------------------------------
diff --git a/Source/URLab/Private/MuJoCo/Components/Geometry/Primitives/MjBox.cpp b/Source/URLab/Private/MuJoCo/Components/Geometry/Primitives/MjBox.cpp
index f240359..fca024a 100644
--- a/Source/URLab/Private/MuJoCo/Components/Geometry/Primitives/MjBox.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Geometry/Primitives/MjBox.cpp
@@ -94,8 +94,7 @@ void UMjBox::SetGeomVisibility(bool bNewVisibility)
 void UMjBox::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
         // --- CODEGEN_IMPORT_START ---
-
-    // --- CODEGEN_IMPORT_END ---
+        // --- CODEGEN_IMPORT_END ---
 
 	Super::ImportFromXml(Node, CompilerSettings);
 	// MuJoCo box size is 3 half-extents in metres.
@@ -126,8 +125,7 @@ void UMjBox::ExportTo(mjsGeom* Element, mjsDefault* def)
 	Super::ExportTo(Element, def);
 
         // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
+        // --- CODEGEN_EXPORT_END ---
 }
 
 void UMjBox::ApplyOverrideMaterial(UMaterialInterface* Material)
@@ -149,9 +147,9 @@ void UMjBox::SyncUnrealTransformFromMj()
         // MuJoCo box size is half-extents. Unreal Cube is 100 units.
         // Size 0.5 -> scale 1.0
         float r[3];
-        r[0] = m_GeomView.size[0];
-        r[1] = m_GeomView.size[1];
-        r[2] = m_GeomView.size[2];
+        r[0] = m_GeomView.geom_size[0];
+        r[1] = m_GeomView.geom_size[1];
+        r[2] = m_GeomView.geom_size[2];
 
         FVector NewScale = FVector(r[0], r[1], r[2]) * 2.0f;
         SetRelativeScale3D(NewScale);
diff --git a/Source/URLab/Private/MuJoCo/Components/Geometry/Primitives/MjCapsule.cpp b/Source/URLab/Private/MuJoCo/Components/Geometry/Primitives/MjCapsule.cpp
index fb508c4..3848ae6 100644
--- a/Source/URLab/Private/MuJoCo/Components/Geometry/Primitives/MjCapsule.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Geometry/Primitives/MjCapsule.cpp
@@ -135,8 +135,7 @@ void UMjCapsule::UpdateCapTransforms()
 void UMjCapsule::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
         // --- CODEGEN_IMPORT_START ---
-
-    // --- CODEGEN_IMPORT_END ---
+        // --- CODEGEN_IMPORT_END ---
 
     Super::ImportFromXml(Node, CompilerSettings);
 
@@ -174,8 +173,7 @@ void UMjCapsule::ExportTo(mjsGeom* Element, mjsDefault* def)
     Super::ExportTo(Element, def);
 
         // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
+        // --- CODEGEN_EXPORT_END ---
 }
 
 void UMjCapsule::SyncUnrealTransformFromMj()
@@ -184,8 +182,8 @@ void UMjCapsule::SyncUnrealTransformFromMj()
 
     if (!bOverride_size)
     {
-        const float RadiusVal     = m_GeomView.size[0];
-        const float HalfLengthVal = m_GeomView.size[1];
+        const float RadiusVal     = m_GeomView.geom_size[0];
+        const float HalfLengthVal = m_GeomView.geom_size[1];
 
         const FVector NewScale = FVector(RadiusVal * 2.0f, RadiusVal * 2.0f, HalfLengthVal * 2.0f);
         SetRelativeScale3D(NewScale);
diff --git a/Source/URLab/Private/MuJoCo/Components/Geometry/Primitives/MjCylinder.cpp b/Source/URLab/Private/MuJoCo/Components/Geometry/Primitives/MjCylinder.cpp
index 4035d2b..ed2fbf9 100644
--- a/Source/URLab/Private/MuJoCo/Components/Geometry/Primitives/MjCylinder.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Geometry/Primitives/MjCylinder.cpp
@@ -78,8 +78,7 @@ void UMjCylinder::OnRegister()
 void UMjCylinder::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
         // --- CODEGEN_IMPORT_START ---
-
-    // --- CODEGEN_IMPORT_END ---
+        // --- CODEGEN_IMPORT_END ---
 
 	Super::ImportFromXml(Node, CompilerSettings);
 	// Clamp -1.0f sentinels (set by the fromto canon when slot is unset).
@@ -113,8 +112,7 @@ void UMjCylinder::ExportTo(mjsGeom* Element, mjsDefault* def)
 	Super::ExportTo(Element, def);
 
         // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
+        // --- CODEGEN_EXPORT_END ---
 }
 
 void UMjCylinder::ApplyOverrideMaterial(UMaterialInterface* Material)
@@ -131,8 +129,8 @@ void UMjCylinder::SyncUnrealTransformFromMj()
     {
         // MuJoCo cylinder size: [radius, half-length]
         // Unreal Cylinder: diameter=100, Height=100
-        float RadiusVal = m_GeomView.size[0];
-        float HalfLengthVal = m_GeomView.size[1];
+        float RadiusVal = m_GeomView.geom_size[0];
+        float HalfLengthVal = m_GeomView.geom_size[1];
 
         FVector NewScale = FVector(RadiusVal * 2.0f, RadiusVal * 2.0f, HalfLengthVal * 2.0f);
         SetRelativeScale3D(NewScale);
diff --git a/Source/URLab/Private/MuJoCo/Components/Geometry/Primitives/MjEllipsoid.cpp b/Source/URLab/Private/MuJoCo/Components/Geometry/Primitives/MjEllipsoid.cpp
index 674f5d0..8549126 100644
--- a/Source/URLab/Private/MuJoCo/Components/Geometry/Primitives/MjEllipsoid.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Geometry/Primitives/MjEllipsoid.cpp
@@ -41,7 +41,7 @@ void UMjEllipsoid::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
@@ -52,6 +52,6 @@ void UMjEllipsoid::ExportTo(mjsGeom* Element, mjsDefault* Default)
     Super::ExportTo(Element, Default);
 
     // --- CODEGEN_EXPORT_START ---
-
+    // (no type-specific schema exports)
     // --- CODEGEN_EXPORT_END ---
 }
diff --git a/Source/URLab/Private/MuJoCo/Components/Geometry/Primitives/MjPlane.cpp b/Source/URLab/Private/MuJoCo/Components/Geometry/Primitives/MjPlane.cpp
index d836d7a..00fbd43 100644
--- a/Source/URLab/Private/MuJoCo/Components/Geometry/Primitives/MjPlane.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Geometry/Primitives/MjPlane.cpp
@@ -41,7 +41,7 @@ void UMjPlane::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& Co
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
@@ -52,6 +52,6 @@ void UMjPlane::ExportTo(mjsGeom* Element, mjsDefault* Default)
     Super::ExportTo(Element, Default);
 
     // --- CODEGEN_EXPORT_START ---
-
+    // (no type-specific schema exports)
     // --- CODEGEN_EXPORT_END ---
 }
diff --git a/Source/URLab/Private/MuJoCo/Components/Geometry/Primitives/MjSdf.cpp b/Source/URLab/Private/MuJoCo/Components/Geometry/Primitives/MjSdf.cpp
index 90eff86..318c2cd 100644
--- a/Source/URLab/Private/MuJoCo/Components/Geometry/Primitives/MjSdf.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Geometry/Primitives/MjSdf.cpp
@@ -41,7 +41,7 @@ void UMjSdf::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& Comp
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
@@ -52,6 +52,6 @@ void UMjSdf::ExportTo(mjsGeom* Element, mjsDefault* Default)
     Super::ExportTo(Element, Default);
 
     // --- CODEGEN_EXPORT_START ---
-
+    // (no type-specific schema exports)
     // --- CODEGEN_EXPORT_END ---
 }
diff --git a/Source/URLab/Private/MuJoCo/Components/Geometry/Primitives/MjSphere.cpp b/Source/URLab/Private/MuJoCo/Components/Geometry/Primitives/MjSphere.cpp
index 80fa9e6..e143dbe 100644
--- a/Source/URLab/Private/MuJoCo/Components/Geometry/Primitives/MjSphere.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Geometry/Primitives/MjSphere.cpp
@@ -78,8 +78,7 @@ void UMjSphere::OnRegister()
 void UMjSphere::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
         // --- CODEGEN_IMPORT_START ---
-
-    // --- CODEGEN_IMPORT_END ---
+        // --- CODEGEN_IMPORT_END ---
 
 	Super::ImportFromXml(Node, CompilerSettings);
 	Radius = size.Num() > 0 ? size[0] : 0.0f;
@@ -109,8 +108,7 @@ void UMjSphere::ExportTo(mjsGeom* Element, mjsDefault* def)
 	Super::ExportTo(Element, def);
 
         // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
+        // --- CODEGEN_EXPORT_END ---
 }
 
 void UMjSphere::ApplyOverrideMaterial(UMaterialInterface* Material)
@@ -126,7 +124,7 @@ void UMjSphere::SyncUnrealTransformFromMj()
     if (!bOverride_size)
     {
         // MuJoCo sphere size is radius. Unreal Sphere is 100 units diameter.
-        float RadiusVal = m_GeomView.size[0];
+        float RadiusVal = m_GeomView.geom_size[0];
         FVector NewScale = FVector(RadiusVal * 2.0f);
         SetRelativeScale3D(NewScale);
 
diff --git a/Source/URLab/Private/MuJoCo/Components/Joints/MjBallJoint.cpp b/Source/URLab/Private/MuJoCo/Components/Joints/MjBallJoint.cpp
index 884f5a2..47f6fd2 100644
--- a/Source/URLab/Private/MuJoCo/Components/Joints/MjBallJoint.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Joints/MjBallJoint.cpp
@@ -13,31 +13,27 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Joints/MjBallJoint.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjBallJoint::UMjBallJoint()
 {
-    bOverride_Type = true;
     Type = EMjJointType::Ball;
-}
-
-void UMjBallJoint::ExportTo(mjsJoint* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
+    bOverride_Type = true;
 }
 
 void UMjBallJoint::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
@@ -46,7 +42,17 @@ void UMjBallJoint::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjBallJoint::ExportTo(mjsJoint* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Joints/MjFreeJoint.cpp b/Source/URLab/Private/MuJoCo/Components/Joints/MjFreeJoint.cpp
index 49e865a..63d0752 100644
--- a/Source/URLab/Private/MuJoCo/Components/Joints/MjFreeJoint.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Joints/MjFreeJoint.cpp
@@ -79,7 +79,6 @@ void UMjFreeJoint::ExportTo(mjsJoint* Element, mjsDefault* Default)
     Super::ExportTo(Element, Default);
 
     // --- CODEGEN_EXPORT_START ---
-
     // --- CODEGEN_EXPORT_END ---
 }
 
@@ -89,7 +88,6 @@ void UMjFreeJoint::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
     // --- CODEGEN_IMPORT_END ---
 }
 
diff --git a/Source/URLab/Private/MuJoCo/Components/Joints/MjHingeJoint.cpp b/Source/URLab/Private/MuJoCo/Components/Joints/MjHingeJoint.cpp
index f8077a3..3809256 100644
--- a/Source/URLab/Private/MuJoCo/Components/Joints/MjHingeJoint.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Joints/MjHingeJoint.cpp
@@ -13,31 +13,27 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Joints/MjHingeJoint.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjHingeJoint::UMjHingeJoint()
 {
-    bOverride_Type = true;
     Type = EMjJointType::Hinge;
-}
-
-void UMjHingeJoint::ExportTo(mjsJoint* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
+    bOverride_Type = true;
 }
 
 void UMjHingeJoint::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
@@ -46,7 +42,17 @@ void UMjHingeJoint::ImportFromXml(const FXmlNode* Node, const FMjCompilerSetting
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjHingeJoint::ExportTo(mjsJoint* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Joints/MjJoint.cpp b/Source/URLab/Private/MuJoCo/Components/Joints/MjJoint.cpp
index 65b0d8f..5565e71 100644
--- a/Source/URLab/Private/MuJoCo/Components/Joints/MjJoint.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Joints/MjJoint.cpp
@@ -42,26 +42,30 @@ void UMjJoint::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& Co
     if (!Node) return;
 
         // --- CODEGEN_IMPORT_START ---
+    MjXmlUtils::ReadAttrString(Node, TEXT("class"), MjClassName);
     { // xml_enum: type -> EMjJointType
         FString S = Node->GetAttribute(TEXT("type"));
         S = S.ToLower();
-        if      (S == TEXT("hinge")) Type = EMjJointType::Hinge;
-        else if (S == TEXT("slide")) Type = EMjJointType::Slide;
-        else if (S == TEXT("ball")) Type = EMjJointType::Ball;
-        else if (S == TEXT("free")) Type = EMjJointType::Free;
-        if (!S.IsEmpty()) bOverride_Type = true;
+        bool bMatched = false;
+        if      (S == TEXT("hinge")) { Type = EMjJointType::Hinge; bMatched = true; }
+        else if (S == TEXT("slide")) { Type = EMjJointType::Slide; bMatched = true; }
+        else if (S == TEXT("ball")) { Type = EMjJointType::Ball; bMatched = true; }
+        else if (S == TEXT("free")) { Type = EMjJointType::Free; bMatched = true; }
+        if (bMatched) bOverride_Type = true;
     }
     MjXmlUtils::ReadAttrInt(Node, TEXT("group"), group, bOverride_group);
+    MjXmlUtils::ReadAttrVec3(Node, TEXT("axis"), Axis, bOverride_Axis);
+    if (bOverride_Axis) Axis.Y = -Axis.Y;
     MjXmlUtils::ReadAttrFloatArray(Node, TEXT("springdamper"), springdamper, bOverride_springdamper);
     MjXmlUtils::ReadAttrBool(Node, TEXT("limited"), limited, bOverride_limited);
-    MjXmlUtils::ReadAttrBool(Node, TEXT("actuatorfrclimited"), actuatorfrclimited, bOverride_actuatorfrclimited);
+    MjXmlUtils::ReadAttrBool(Node, TEXT("actuatorfrclimited"), ActFrcLimited, bOverride_ActFrcLimited);
     MjXmlUtils::ReadAttrFloatArray(Node, TEXT("solreflimit"), solreflimit, bOverride_solreflimit);
     MjXmlUtils::ReadAttrFloatArray(Node, TEXT("solimplimit"), solimplimit, bOverride_solimplimit);
     MjXmlUtils::ReadAttrFloatArray(Node, TEXT("solreffriction"), solreffriction, bOverride_solreffriction);
     MjXmlUtils::ReadAttrFloatArray(Node, TEXT("solimpfriction"), solimpfriction, bOverride_solimpfriction);
     MjXmlUtils::ReadAttrFloatArray(Node, TEXT("stiffness"), stiffness, bOverride_stiffness);
     MjXmlUtils::ReadAttrFloatArray(Node, TEXT("range"), range, bOverride_range);
-    MjXmlUtils::ReadAttrFloatArray(Node, TEXT("actuatorfrcrange"), actuatorfrcrange, bOverride_actuatorfrcrange);
+    MjXmlUtils::ReadAttrFloatArray(Node, TEXT("actuatorfrcrange"), ActFrcRange, bOverride_ActFrcRange);
     MjXmlUtils::ReadAttrFloat(Node, TEXT("actuatorgravcomp"), actuatorgravcomp, bOverride_actuatorgravcomp);
     MjXmlUtils::ReadAttrFloat(Node, TEXT("margin"), margin, bOverride_margin);
     MjXmlUtils::ReadAttrFloat(Node, TEXT("ref"), ref, bOverride_ref);
@@ -69,6 +73,8 @@ void UMjJoint::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& Co
     MjXmlUtils::ReadAttrFloat(Node, TEXT("armature"), armature, bOverride_armature);
     MjXmlUtils::ReadAttrFloatArray(Node, TEXT("damping"), damping, bOverride_damping);
     MjXmlUtils::ReadAttrFloat(Node, TEXT("frictionloss"), frictionloss, bOverride_frictionloss);
+    MjUtils::ReadVec3InMeters(Node, TEXT("pos"), Pos, bOverride_Pos);
+    if (bOverride_Pos)  SetRelativeLocation(Pos);
     if (bOverride_range)
     {
         if      (Type == EMjJointType::Slide) { for (float& V : range) { V *= 100.0f; } }
@@ -84,28 +90,10 @@ void UMjJoint::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& Co
         if      (Type == EMjJointType::Slide) { springref *= 100.0f; }
         else if ((Type == EMjJointType::Hinge) || (Type == EMjJointType::Ball)) { if (!CompilerSettings.bAngleInDegrees) { springref = FMath::RadiansToDegrees(springref); } }
     }
-    // --- CODEGEN_IMPORT_END ---
-
-    // Class Name
-    MjXmlUtils::ReadAttrString(Node, TEXT("class"), MjClassName);
-
-    // Axis — convert from MuJoCo to UE direction-vector convention (negate Y, no scale)
-    FString AxisStr;
-    if (MjXmlUtils::ReadAttrString(Node, TEXT("axis"), AxisStr))
-    {
-        bOverride_Axis = true;
-        FVector RawAxis = MjXmlUtils::ParseVector(AxisStr);
-        Axis = FVector(RawAxis.X, -RawAxis.Y, RawAxis.Z);
-    }
+        // --- CODEGEN_IMPORT_END ---
 
-
-    FString PosStr;
-    if (MjXmlUtils::ReadAttrString(Node, TEXT("pos"), PosStr))
-    {
-        FVector RawPos = MjXmlUtils::ParseVector(PosStr);
-        double p[3] = { (double)RawPos.X, (double)RawPos.Y, (double)RawPos.Z };
-        SetRelativeLocation(MjUtils::MjToUEPosition(p));
-    }
+    // axis (Y-negate vec3_convert) + pos (spatial_pose canon) are
+    // codegen-owned via the joint rule in codegen_rules.json.
 
     // Note: autolimits (limited inferred from range) is handled by the MuJoCo spec
     // compiler (mjLIMITED_AUTO default). We do not replicate that logic here.
@@ -120,20 +108,19 @@ void UMjJoint::ExportTo(mjsJoint* Element, mjsDefault* Default)
 {
     if (!Element) return;
 
-    FVector _pos = GetRelativeLocation();
-    if (!_pos.IsZero()) MjUtils::UEToMjPosition(_pos, Element->pos);
-
-    if (bOverride_Axis)
-    {
-        // Convert UE direction vector back to MuJoCo convention (negate Y, no scale)
-        Element->axis[0] = Axis.X;
-        Element->axis[1] = -Axis.Y;
-        Element->axis[2] = Axis.Z;
-    }
+    // axis + pos exports are codegen-emitted between the CODEGEN_EXPORT
+    // markers below (spatial_pose canon writes pos, vec3_convert.axis
+    // applies the Y-negate to the codegen-emitted axis write).
 
     // Slide joints: convert ref from cm (UE) back to meters (MuJoCo)
 
         // --- CODEGEN_EXPORT_START ---
+    if (bOverride_Pos)
+    {
+        double TmpPos[3];
+        MjUtils::UEToMjPosition(Pos, TmpPos);
+        Element->pos[0] = TmpPos[0]; Element->pos[1] = TmpPos[1]; Element->pos[2] = TmpPos[2];
+    }
     if (bOverride_Type)
     {
         switch (Type)
@@ -146,19 +133,20 @@ void UMjJoint::ExportTo(mjsJoint* Element, mjsDefault* Default)
         }
     }
     if (bOverride_group) Element->group = group;
-    if (bOverride_springdamper) { for (int32 i = 0; i < springdamper.Num(); ++i) Element->springdamper[i] = springdamper[i]; }
+    if (bOverride_Axis) { Element->axis[0] = Axis.X; Element->axis[1] = -Axis.Y; Element->axis[2] = Axis.Z; }
+    if (bOverride_springdamper) { for (int32 i = 0; i < FMath::Min(springdamper.Num(), 2); ++i) Element->springdamper[i] = springdamper[i]; }
     if (bOverride_limited) Element->limited = limited ? 1 : 0;
-    if (bOverride_actuatorfrclimited) Element->actfrclimited = actuatorfrclimited ? 1 : 0;
-    if (bOverride_solreflimit) { for (int32 i = 0; i < solreflimit.Num(); ++i) Element->solref_limit[i] = solreflimit[i]; }
-    if (bOverride_solimplimit) { for (int32 i = 0; i < solimplimit.Num(); ++i) Element->solimp_limit[i] = solimplimit[i]; }
-    if (bOverride_solreffriction) { for (int32 i = 0; i < solreffriction.Num(); ++i) Element->solref_friction[i] = solreffriction[i]; }
-    if (bOverride_solimpfriction) { for (int32 i = 0; i < solimpfriction.Num(); ++i) Element->solimp_friction[i] = solimpfriction[i]; }
-    if (bOverride_stiffness) { for (int32 i = 0; i < stiffness.Num(); ++i) Element->stiffness[i] = stiffness[i]; }
-    if (bOverride_actuatorfrcrange) { for (int32 i = 0; i < actuatorfrcrange.Num(); ++i) Element->actfrcrange[i] = actuatorfrcrange[i]; }
+    if (bOverride_ActFrcLimited) Element->actfrclimited = ActFrcLimited ? 1 : 0;
+    if (bOverride_solreflimit) { for (int32 i = 0; i < FMath::Min(solreflimit.Num(), 2); ++i) Element->solref_limit[i] = solreflimit[i]; }
+    if (bOverride_solimplimit) { for (int32 i = 0; i < FMath::Min(solimplimit.Num(), 5); ++i) Element->solimp_limit[i] = solimplimit[i]; }
+    if (bOverride_solreffriction) { for (int32 i = 0; i < FMath::Min(solreffriction.Num(), 2); ++i) Element->solref_friction[i] = solreffriction[i]; }
+    if (bOverride_solimpfriction) { for (int32 i = 0; i < FMath::Min(solimpfriction.Num(), 5); ++i) Element->solimp_friction[i] = solimpfriction[i]; }
+    if (bOverride_stiffness) { for (int32 i = 0; i < FMath::Min(stiffness.Num(), 3); ++i) Element->stiffness[i] = stiffness[i]; }
+    if (bOverride_ActFrcRange) { for (int32 i = 0; i < FMath::Min(ActFrcRange.Num(), 2); ++i) Element->actfrcrange[i] = ActFrcRange[i]; }
     if (bOverride_actuatorgravcomp) Element->actgravcomp = actuatorgravcomp;
     if (bOverride_margin) Element->margin = margin;
     if (bOverride_armature) Element->armature = armature;
-    if (bOverride_damping) { for (int32 i = 0; i < damping.Num(); ++i) Element->damping[i] = damping[i]; }
+    if (bOverride_damping) { for (int32 i = 0; i < FMath::Min(damping.Num(), 3); ++i) Element->damping[i] = damping[i]; }
     if (bOverride_frictionloss) Element->frictionloss = frictionloss;
     if (bOverride_range)
     {
@@ -184,15 +172,13 @@ void UMjJoint::ExportTo(mjsJoint* Element, mjsDefault* Default)
         else if ((Type == EMjJointType::Hinge) || (Type == EMjJointType::Ball)) { V = FMath::DegreesToRadians(V); }
         Element->springref = (mjtNum)V;
     }
-    // --- CODEGEN_EXPORT_END ---
+        // --- CODEGEN_EXPORT_END ---
 }
 
 void UMjJoint::Bind(mjModel* Model, mjData* Data, const FString& Prefix)
 {
     Super::Bind(Model, Data, Prefix);
-    m_JointView = BindToView<JointView>(Prefix);
-    if (m_JointView.id != -1) m_ID = m_JointView.id;
-    else UE_LOG(LogURLabBind, Warning, TEXT("Failed to bind Joint %s"), *GetName());
+    BindAndCacheView(m_JointView, Prefix);
 }
 
 float UMjJoint::GetPosition() const
@@ -240,8 +226,8 @@ float UMjJoint::GetAcceleration() const
 
 FVector2D UMjJoint::GetJointRange() const
 {
-    if (m_JointView.id < 0 || !m_JointView.range) return FVector2D::ZeroVector;
-    return FVector2D((float)m_JointView.range[0], (float)m_JointView.range[1]);
+    if (m_JointView.id < 0 || !m_JointView.jnt_range) return FVector2D::ZeroVector;
+    return FVector2D((float)m_JointView.jnt_range[0], (float)m_JointView.jnt_range[1]);
 }
 
 FVector UMjJoint::GetWorldAnchor() const
@@ -290,7 +276,7 @@ EMjJointType UMjJoint::GetResolvedType() const
     // Runtime: use compiled model
     if (m_JointView.id >= 0)
     {
-        switch (m_JointView.type)
+        switch (m_JointView.jnt_type)
         {
             case mjJNT_FREE:  return EMjJointType::Free;
             case mjJNT_BALL:  return EMjJointType::Ball;
@@ -378,10 +364,10 @@ FVector2D UMjJoint::GetResolvedRange() const
     };
 
     // Runtime: use compiled model (rad/m).
-    if (m_JointView.id >= 0 && m_JointView.range)
+    if (m_JointView.id >= 0 && m_JointView.jnt_range)
     {
-        return FVector2D(MjToUE((float)m_JointView.range[0]),
-                         MjToUE((float)m_JointView.range[1]));
+        return FVector2D(MjToUE((float)m_JointView.jnt_range[0]),
+                         MjToUE((float)m_JointView.jnt_range[1]));
     }
 
     // Editor: local override wins. Already in UE units.
@@ -416,10 +402,10 @@ FVector2D UMjJoint::GetResolvedRange() const
 bool UMjJoint::GetResolvedLimited() const
 {
     // Runtime: use compiled model range
-    if (m_JointView.id >= 0 && m_JointView.range)
+    if (m_JointView.id >= 0 && m_JointView.jnt_range)
     {
         // MuJoCo sets range to (0,0) when unlimited
-        return m_JointView.range[0] != 0.0 || m_JointView.range[1] != 0.0;
+        return m_JointView.jnt_range[0] != 0.0 || m_JointView.jnt_range[1] != 0.0;
     }
 
     // Editor: local override wins
@@ -465,13 +451,13 @@ void UMjJoint::BuildBinaryPayload(FBufferArchive& OutBuffer) const
     int32 JointID = m_ID;
     OutBuffer << JointID;
 
-    float Pos = GetPosition();
-    float Vel = GetVelocity();
-    float Acc = GetAcceleration();
+    float JointPos = GetPosition();
+    float JointVel = GetVelocity();
+    float JointAcc = GetAcceleration();
 
-    OutBuffer << Pos;
-    OutBuffer << Vel;
-    OutBuffer << Acc;
+    OutBuffer << JointPos;
+    OutBuffer << JointVel;
+    OutBuffer << JointAcc;
 }
 
 FString UMjJoint::GetTelemetryTopicName() const
@@ -480,8 +466,7 @@ FString UMjJoint::GetTelemetryTopicName() const
 }
 
 #if WITH_EDITOR
-TArray<FString> UMjJoint::GetDefaultClassOptions() const
-{
-    return GetSiblingComponentOptions(this, UMjDefault::StaticClass(), true);
-}
+// --- CODEGEN_EDITOR_OPTIONS_START ---
+TArray<FString> UMjJoint::GetDefaultClassOptions() const { return UMjComponent::GetSiblingComponentOptions(this, UMjDefault::StaticClass(), true); }
+// --- CODEGEN_EDITOR_OPTIONS_END ---
 #endif
diff --git a/Source/URLab/Private/MuJoCo/Components/Joints/MjSlideJoint.cpp b/Source/URLab/Private/MuJoCo/Components/Joints/MjSlideJoint.cpp
index 5c159b2..5946baf 100644
--- a/Source/URLab/Private/MuJoCo/Components/Joints/MjSlideJoint.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Joints/MjSlideJoint.cpp
@@ -13,31 +13,27 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Joints/MjSlideJoint.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjSlideJoint::UMjSlideJoint()
 {
-    bOverride_Type = true;
     Type = EMjJointType::Slide;
-}
-
-void UMjSlideJoint::ExportTo(mjsJoint* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
+    bOverride_Type = true;
 }
 
 void UMjSlideJoint::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
@@ -46,7 +42,17 @@ void UMjSlideJoint::ImportFromXml(const FXmlNode* Node, const FMjCompilerSetting
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjSlideJoint::ExportTo(mjsJoint* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Keyframes/MjKeyframe.cpp b/Source/URLab/Private/MuJoCo/Components/Keyframes/MjKeyframe.cpp
index 7bdd43c..8e2a8d1 100644
--- a/Source/URLab/Private/MuJoCo/Components/Keyframes/MjKeyframe.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Keyframes/MjKeyframe.cpp
@@ -26,6 +26,7 @@
 #include "MuJoCo/Components/Joints/MjFreeJoint.h"
 #include "MuJoCo/Components/Actuators/MjActuator.h"
 #include "MuJoCo/Utils/MjXmlUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
 #include "XmlNode.h"
 #include "Utils/URLabLogging.h"
 
@@ -58,36 +59,12 @@ void UMjKeyframe::ExportTo(mjsKey* Element, mjsDefault* Default)
 
     // --- CODEGEN_EXPORT_START ---
     if (bOverride_Time) Element->time = Time;
-    if (bOverride_Qpos && Element->qpos)
-    {
-        Element->qpos->clear();
-        for (float V : Qpos) Element->qpos->push_back((double)V);
-    }
-    if (bOverride_Qvel && Element->qvel)
-    {
-        Element->qvel->clear();
-        for (float V : Qvel) Element->qvel->push_back((double)V);
-    }
-    if (bOverride_Act && Element->act)
-    {
-        Element->act->clear();
-        for (float V : Act) Element->act->push_back((double)V);
-    }
-    if (bOverride_Ctrl && Element->ctrl)
-    {
-        Element->ctrl->clear();
-        for (float V : Ctrl) Element->ctrl->push_back((double)V);
-    }
-    if (bOverride_Mpos && Element->mpos)
-    {
-        Element->mpos->clear();
-        for (float V : Mpos) Element->mpos->push_back((double)V);
-    }
-    if (bOverride_Mquat && Element->mquat)
-    {
-        Element->mquat->clear();
-        for (float V : Mquat) Element->mquat->push_back((double)V);
-    }
+    if (bOverride_Qpos) MjSetDoubleVec(Element->qpos, Qpos);
+    if (bOverride_Qvel) MjSetDoubleVec(Element->qvel, Qvel);
+    if (bOverride_Act) MjSetDoubleVec(Element->act, Act);
+    if (bOverride_Ctrl) MjSetDoubleVec(Element->ctrl, Ctrl);
+    if (bOverride_Mpos) MjSetDoubleVec(Element->mpos, Mpos);
+    if (bOverride_Mquat) MjSetDoubleVec(Element->mquat, Mquat);
     // --- CODEGEN_EXPORT_END ---
 }
 
@@ -153,16 +130,8 @@ static FKeyframeDimensions CountDimensions(AActor* Owner)
     return D;
 }
 
-namespace
-{
-    // Helper for overwriting an mjDoubleVec* with a TArray<float>.
-    void SetVec(mjDoubleVec* Dest, const TArray<float>& Src)
-    {
-        if (!Dest) return;
-        Dest->clear();
-        for (float V : Src) Dest->push_back((double)V);
-    }
-}
+// SetVec is a thin alias for MjUtils::MjSetDoubleVec; kept here so the
+// freejoint padding paths below read cleanly.
 
 void UMjKeyframe::RegisterToSpec(FMujocoSpecWrapper& Wrapper, mjsBody* ParentBody)
 {
@@ -199,7 +168,7 @@ void UMjKeyframe::RegisterToSpec(FMujocoSpecWrapper& Wrapper, mjsBody* ParentBod
             Padded.Append({0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f});
         }
         Padded.Append(Qpos);
-        SetVec(Key->qpos, Padded);
+        MjSetDoubleVec(Key->qpos, Padded);
         UE_LOG(LogURLabImport, Log, TEXT("[MjKeyframe] '%s': padded qpos %d -> %d (%d free joints)"),
             *TName, Qpos.Num(), Padded.Num(), Dims.NumFreeJoints);
     }
@@ -218,7 +187,7 @@ void UMjKeyframe::RegisterToSpec(FMujocoSpecWrapper& Wrapper, mjsBody* ParentBod
             Padded.Append({0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f});
         }
         Padded.Append(Qvel);
-        SetVec(Key->qvel, Padded);
+        MjSetDoubleVec(Key->qvel, Padded);
         UE_LOG(LogURLabImport, Log, TEXT("[MjKeyframe] '%s': padded qvel %d -> %d"),
             *TName, Qvel.Num(), Padded.Num());
     }
diff --git a/Source/URLab/Private/MuJoCo/Components/Physics/MjContactExclude.cpp b/Source/URLab/Private/MuJoCo/Components/Physics/MjContactExclude.cpp
index a2da014..0907f90 100644
--- a/Source/URLab/Private/MuJoCo/Components/Physics/MjContactExclude.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Physics/MjContactExclude.cpp
@@ -21,6 +21,7 @@
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
 
 #include "MuJoCo/Components/Physics/MjContactExclude.h"
+#include "MuJoCo/Utils/MjUtils.h"
 #include "XmlFile.h"
 #include "mujoco/mujoco.h"
 #include "MuJoCo/Core/Spec/MjSpecWrapper.h"
@@ -42,21 +43,15 @@ void UMjContactExclude::BeginPlay()
 void UMjContactExclude::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
         // --- CODEGEN_IMPORT_START ---
+    MjXmlUtils::ReadAttrString(Node, TEXT("name"), Name);
     if (MjXmlUtils::ReadAttrString(Node, TEXT("body1"), body1)) bOverride_body1 = true;
     if (MjXmlUtils::ReadAttrString(Node, TEXT("body2"), body2)) bOverride_body2 = true;
-    // --- CODEGEN_IMPORT_END ---
+        // --- CODEGEN_IMPORT_END ---
 
     if (!Node)
     {
         return;
     }
-
-    // Required attributes
-    body1 = Node->GetAttribute(TEXT("body1"));
-    body2 = Node->GetAttribute(TEXT("body2"));
-
-    // Optional attributes
-    Name = Node->GetAttribute(TEXT("name"));
 }
 
 void UMjContactExclude::ExportTo(mjsExclude* Element)
@@ -64,8 +59,8 @@ void UMjContactExclude::ExportTo(mjsExclude* Element)
     if (!Element) return;
 
     // --- CODEGEN_EXPORT_START ---
-    if (bOverride_body1 && !body1.IsEmpty()) mjs_setString(Element->bodyname1, TCHAR_TO_UTF8(*body1));
-    if (bOverride_body2 && !body2.IsEmpty()) mjs_setString(Element->bodyname2, TCHAR_TO_UTF8(*body2));
+    if (bOverride_body1) MjSetString(Element->bodyname1, body1);
+    if (bOverride_body2) MjSetString(Element->bodyname2, body2);
     // --- CODEGEN_EXPORT_END ---
 }
 
@@ -76,14 +71,8 @@ void UMjContactExclude::RegisterToSpec(FMujocoSpecWrapper& Wrapper, mjsBody* Par
     UE_LOG(LogURLabWrapper, Log, TEXT("Added contact exclude: %s<->%s"), *body1, *body2);
 }
 
-void UMjContactExclude::Bind(mjModel* model, mjData* data, const FString& Prefix)
-{
-    // Contact excludes are global static data.
-}
-
 #if WITH_EDITOR
-TArray<FString> UMjContactExclude::GetBodyOptions() const
-{
-    return UMjComponent::GetSiblingComponentOptions(this, UMjBody::StaticClass());
-}
+// --- CODEGEN_EDITOR_OPTIONS_START ---
+TArray<FString> UMjContactExclude::GetBodyOptions() const { return UMjComponent::GetSiblingComponentOptions(this, UMjBody::StaticClass()); }
+// --- CODEGEN_EDITOR_OPTIONS_END ---
 #endif
diff --git a/Source/URLab/Private/MuJoCo/Components/Physics/MjContactPair.cpp b/Source/URLab/Private/MuJoCo/Components/Physics/MjContactPair.cpp
index 9035544..14439cb 100644
--- a/Source/URLab/Private/MuJoCo/Components/Physics/MjContactPair.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Physics/MjContactPair.cpp
@@ -21,6 +21,7 @@
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
 
 #include "MuJoCo/Components/Physics/MjContactPair.h"
+#include "MuJoCo/Utils/MjUtils.h"
 #include "XmlFile.h"
 #include "mujoco/mujoco.h"
 #include "MuJoCo/Core/Spec/MjSpecWrapper.h"
@@ -43,6 +44,7 @@ void UMjContactPair::BeginPlay()
 void UMjContactPair::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
         // --- CODEGEN_IMPORT_START ---
+    MjXmlUtils::ReadAttrString(Node, TEXT("name"), Name);
     if (MjXmlUtils::ReadAttrString(Node, TEXT("geom1"), geom1)) bOverride_geom1 = true;
     if (MjXmlUtils::ReadAttrString(Node, TEXT("geom2"), geom2)) bOverride_geom2 = true;
     MjXmlUtils::ReadAttrInt(Node, TEXT("condim"), condim, bOverride_condim);
@@ -52,30 +54,13 @@ void UMjContactPair::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettin
     MjXmlUtils::ReadAttrFloatArray(Node, TEXT("solimp"), solimp, bOverride_solimp);
     MjXmlUtils::ReadAttrFloat(Node, TEXT("gap"), gap, bOverride_gap);
     MjXmlUtils::ReadAttrFloat(Node, TEXT("margin"), margin, bOverride_margin);
-    // --- CODEGEN_IMPORT_END ---
+        // --- CODEGEN_IMPORT_END ---
 
     if (!Node)
     {
         return;
     }
 
-    // Required attributes
-
-    // Optional attributes
-    MjXmlUtils::ReadAttrString(Node, TEXT("name"), Name);
-
-    {
-        bool bCondimOverride = false;
-    }
-
-    // Parse friction/solref/solimp arrays
-    {
-        bool bFrictionOverride = false, bSolrefOverride = false, bSolimpOverride = false;
-    }
-
-    {
-        bool bGapOverride = false, bMarginOverride = false;
-    }
 }
 
 void UMjContactPair::ExportTo(mjsPair* Element)
@@ -83,13 +68,13 @@ void UMjContactPair::ExportTo(mjsPair* Element)
     if (!Element) return;
 
     // --- CODEGEN_EXPORT_START ---
-    if (bOverride_geom1 && !geom1.IsEmpty()) mjs_setString(Element->geomname1, TCHAR_TO_UTF8(*geom1));
-    if (bOverride_geom2 && !geom2.IsEmpty()) mjs_setString(Element->geomname2, TCHAR_TO_UTF8(*geom2));
+    if (bOverride_geom1) MjSetString(Element->geomname1, geom1);
+    if (bOverride_geom2) MjSetString(Element->geomname2, geom2);
     if (bOverride_condim) Element->condim = condim;
-    if (bOverride_friction) { for (int32 i = 0; i < friction.Num(); ++i) Element->friction[i] = friction[i]; }
-    if (bOverride_solref) { for (int32 i = 0; i < solref.Num(); ++i) Element->solref[i] = solref[i]; }
-    if (bOverride_solreffriction) { for (int32 i = 0; i < solreffriction.Num(); ++i) Element->solreffriction[i] = solreffriction[i]; }
-    if (bOverride_solimp) { for (int32 i = 0; i < solimp.Num(); ++i) Element->solimp[i] = solimp[i]; }
+    if (bOverride_friction) { for (int32 i = 0; i < FMath::Min(friction.Num(), 5); ++i) Element->friction[i] = friction[i]; }
+    if (bOverride_solref) { for (int32 i = 0; i < FMath::Min(solref.Num(), 2); ++i) Element->solref[i] = solref[i]; }
+    if (bOverride_solreffriction) { for (int32 i = 0; i < FMath::Min(solreffriction.Num(), 2); ++i) Element->solreffriction[i] = solreffriction[i]; }
+    if (bOverride_solimp) { for (int32 i = 0; i < FMath::Min(solimp.Num(), 5); ++i) Element->solimp[i] = solimp[i]; }
     if (bOverride_gap) Element->gap = gap;
     if (bOverride_margin) Element->margin = margin;
     // --- CODEGEN_EXPORT_END ---
@@ -106,14 +91,8 @@ void UMjContactPair::RegisterToSpec(FMujocoSpecWrapper& Wrapper, mjsBody* Parent
     UE_LOG(LogURLabWrapper, Log, TEXT("Added contact pair: %s->%s"), *geom1, *geom2);
 }
 
-void UMjContactPair::Bind(mjModel* model, mjData* data, const FString& Prefix)
-{
-    // Contact pairs are global static data in MuJoCo, usually not bound to runtime indices easily or needed for runtime update.
-}
-
 #if WITH_EDITOR
-TArray<FString> UMjContactPair::GetGeomOptions() const
-{
-    return UMjComponent::GetSiblingComponentOptions(this, UMjGeom::StaticClass());
-}
+// --- CODEGEN_EDITOR_OPTIONS_START ---
+TArray<FString> UMjContactPair::GetGeomOptions() const { return UMjComponent::GetSiblingComponentOptions(this, UMjGeom::StaticClass()); }
+// --- CODEGEN_EDITOR_OPTIONS_END ---
 #endif
diff --git a/Source/URLab/Private/MuJoCo/Components/Physics/MjInertial.cpp b/Source/URLab/Private/MuJoCo/Components/Physics/MjInertial.cpp
index 306ad7d..1c0813a 100644
--- a/Source/URLab/Private/MuJoCo/Components/Physics/MjInertial.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Physics/MjInertial.cpp
@@ -54,7 +54,7 @@ void UMjInertial::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings&
     }
     if (bOverride_Pos)  SetRelativeLocation(Pos);
     if (bOverride_Quat) SetRelativeRotation(Quat);
-    // --- CODEGEN_IMPORT_END ---
+        // --- CODEGEN_IMPORT_END ---
 }
 
 void UMjInertial::RegisterToSpec(FMujocoSpecWrapper& Wrapper, mjsBody* ParentBody)
@@ -86,6 +86,3 @@ void UMjInertial::RegisterToSpec(FMujocoSpecWrapper& Wrapper, mjsBody* ParentBod
     MjUtils::UEToMjRotation(RelTrans.GetRotation(), ParentBody->iquat);
 }
 
-void UMjInertial::Bind(mjModel* model, mjData* data, const FString& Prefix)
-{
-}
diff --git a/Source/URLab/Private/MuJoCo/Components/QuickConvert/AMjHeightfieldActor.cpp b/Source/URLab/Private/MuJoCo/Components/QuickConvert/AMjHeightfieldActor.cpp
index 6a4d0d1..d1e8c75 100644
--- a/Source/URLab/Private/MuJoCo/Components/QuickConvert/AMjHeightfieldActor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/QuickConvert/AMjHeightfieldActor.cpp
@@ -21,6 +21,7 @@
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
 
 #include "MuJoCo/Components/QuickConvert/AMjHeightfieldActor.h"
+#include "MuJoCo/Utils/MjUtils.h"
 
 #include "DrawDebugHelpers.h"
 #include "MuJoCo/Components/QuickConvert/MjQuickConvertComponent.h"
@@ -340,7 +341,7 @@ void AMjHeightfieldActor::Setup(mjSpec* Spec, mjVFS* /*VFS*/)
     mjs_setName(Geom->element, TCHAR_TO_UTF8(*GeomName));
 
     Geom->type = mjGEOM_HFIELD;
-    mjs_setString(Geom->hfieldname, TCHAR_TO_UTF8(*HFieldName));
+    MjSetStringRaw(Geom->hfieldname, HFieldName);
 
     Geom->pos[0] =  (Bounds.Min.X + Bounds.Max.X) / 200.0;
     Geom->pos[1] = -(Bounds.Min.Y + Bounds.Max.Y) / 200.0;
diff --git a/Source/URLab/Private/MuJoCo/Components/QuickConvert/MjQuickConvertComponent.cpp b/Source/URLab/Private/MuJoCo/Components/QuickConvert/MjQuickConvertComponent.cpp
index 8bd095d..495cbd1 100644
--- a/Source/URLab/Private/MuJoCo/Components/QuickConvert/MjQuickConvertComponent.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/QuickConvert/MjQuickConvertComponent.cpp
@@ -76,10 +76,10 @@ void UMjQuickConvertComponent::DrawDebugCollision() {
     if (!World) return;
 
     for (auto geom_view : m_CreatedBody->GetMj().Geoms()) {
-        int meshId = geom_view.dataid;
+        int meshId = geom_view.geom_dataid;
 
-        mjtNum* pos = geom_view.xpos; 
-        mjtNum* mat = geom_view.xmat;
+        mjtNum* pos = geom_view.geom_xpos; 
+        mjtNum* mat = geom_view.geom_xmat;
 
         FVector Position = FVector(pos[0], -pos[1], pos[2]);
         Position *= Multiplier;
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjAccelerometer.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjAccelerometer.cpp
index 6e61b1b..15aeed8 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjAccelerometer.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjAccelerometer.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjAccelerometer.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjAccelerometer::UMjAccelerometer()
 {
     Type = EMjSensorType::Accelerometer;
 }
 
-void UMjAccelerometer::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjAccelerometer::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjAccelerometer::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjActuatorFrcSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjActuatorFrcSensor.cpp
index 6da948f..8c1c1e1 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjActuatorFrcSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjActuatorFrcSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjActuatorFrcSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjActuatorFrcSensor::UMjActuatorFrcSensor()
 {
     Type = EMjSensorType::ActuatorFrc;
 }
 
-void UMjActuatorFrcSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjActuatorFrcSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjActuatorFrcSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjActuatorPosSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjActuatorPosSensor.cpp
index 7264c0b..2667c95 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjActuatorPosSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjActuatorPosSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjActuatorPosSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjActuatorPosSensor::UMjActuatorPosSensor()
 {
     Type = EMjSensorType::ActuatorPos;
 }
 
-void UMjActuatorPosSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjActuatorPosSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjActuatorPosSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjActuatorVelSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjActuatorVelSensor.cpp
index 856142f..943ec91 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjActuatorVelSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjActuatorVelSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjActuatorVelSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjActuatorVelSensor::UMjActuatorVelSensor()
 {
     Type = EMjSensorType::ActuatorVel;
 }
 
-void UMjActuatorVelSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjActuatorVelSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjActuatorVelSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjBallAngVelSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjBallAngVelSensor.cpp
index 40cb1a2..7b1e8a2 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjBallAngVelSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjBallAngVelSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjBallAngVelSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjBallAngVelSensor::UMjBallAngVelSensor()
 {
     Type = EMjSensorType::BallAngVel;
 }
 
-void UMjBallAngVelSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjBallAngVelSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjBallAngVelSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjBallQuatSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjBallQuatSensor.cpp
index 0da6eb5..b81903f 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjBallQuatSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjBallQuatSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjBallQuatSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjBallQuatSensor::UMjBallQuatSensor()
 {
     Type = EMjSensorType::BallQuat;
 }
 
-void UMjBallQuatSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjBallQuatSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjBallQuatSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjCamProjectionSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjCamProjectionSensor.cpp
index ce59afb..d7deabf 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjCamProjectionSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjCamProjectionSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjCamProjectionSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjCamProjectionSensor::UMjCamProjectionSensor()
 {
     Type = EMjSensorType::CamProjection;
 }
 
-void UMjCamProjectionSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjCamProjectionSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjCamProjectionSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjCamera.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjCamera.cpp
index b41cbf9..bb979c1 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjCamera.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjCamera.cpp
@@ -690,14 +690,14 @@ void UMjCamera::ExportTo(mjsCamera* Element, mjsDefault* /*def*/)
     }
     if (bOverride_fovy) Element->fovy = fovy;
     if (bOverride_ipd) Element->ipd = ipd;
-    if (bOverride_resolution) { for (int32 i = 0; i < resolution.Num(); ++i) Element->resolution[i] = resolution[i]; }
+    if (bOverride_resolution) { for (int32 i = 0; i < FMath::Min(resolution.Num(), 2); ++i) Element->resolution[i] = resolution[i]; }
     if (bOverride_output) Element->output = output;
-    if (bOverride_target && !target.IsEmpty()) mjs_setString(Element->targetbody, TCHAR_TO_UTF8(*target));
-    if (bOverride_focal) { for (int32 i = 0; i < focal.Num(); ++i) Element->focal_length[i] = focal[i]; }
-    if (bOverride_focalpixel) { for (int32 i = 0; i < focalpixel.Num(); ++i) Element->focal_pixel[i] = focalpixel[i]; }
-    if (bOverride_principal) { for (int32 i = 0; i < principal.Num(); ++i) Element->principal_length[i] = principal[i]; }
-    if (bOverride_principalpixel) { for (int32 i = 0; i < principalpixel.Num(); ++i) Element->principal_pixel[i] = principalpixel[i]; }
-    if (bOverride_sensorsize) { for (int32 i = 0; i < sensorsize.Num(); ++i) Element->sensor_size[i] = sensorsize[i]; }
+    if (bOverride_target) MjSetString(Element->targetbody, target);
+    if (bOverride_focal) { for (int32 i = 0; i < FMath::Min(focal.Num(), 2); ++i) Element->focal_length[i] = focal[i]; }
+    if (bOverride_focalpixel) { for (int32 i = 0; i < FMath::Min(focalpixel.Num(), 2); ++i) Element->focal_pixel[i] = focalpixel[i]; }
+    if (bOverride_principal) { for (int32 i = 0; i < FMath::Min(principal.Num(), 2); ++i) Element->principal_length[i] = principal[i]; }
+    if (bOverride_principalpixel) { for (int32 i = 0; i < FMath::Min(principalpixel.Num(), 2); ++i) Element->principal_pixel[i] = principalpixel[i]; }
+    if (bOverride_sensorsize) { for (int32 i = 0; i < FMath::Min(sensorsize.Num(), 2); ++i) Element->sensor_size[i] = sensorsize[i]; }
     // --- CODEGEN_EXPORT_END ---
 }
 
@@ -712,22 +712,25 @@ void UMjCamera::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& C
     if (!Node) return;
 
         // --- CODEGEN_IMPORT_START ---
+    MjXmlUtils::ReadAttrString(Node, TEXT("name"), MjName);
     { // xml_enum: mode -> EMjCameraTrackingMode
         FString S = Node->GetAttribute(TEXT("mode"));
         S = S.ToLower();
-        if      (S == TEXT("fixed")) TrackingMode = EMjCameraTrackingMode::Fixed;
-        else if (S == TEXT("track")) TrackingMode = EMjCameraTrackingMode::Track;
-        else if (S == TEXT("trackcom")) TrackingMode = EMjCameraTrackingMode::TrackCom;
-        else if (S == TEXT("targetbody")) TrackingMode = EMjCameraTrackingMode::TargetBody;
-        else if (S == TEXT("targetbodycom")) TrackingMode = EMjCameraTrackingMode::TargetBodyCom;
-        if (!S.IsEmpty()) bOverride_TrackingMode = true;
+        bool bMatched = false;
+        if      (S == TEXT("fixed")) { TrackingMode = EMjCameraTrackingMode::Fixed; bMatched = true; }
+        else if (S == TEXT("track")) { TrackingMode = EMjCameraTrackingMode::Track; bMatched = true; }
+        else if (S == TEXT("trackcom")) { TrackingMode = EMjCameraTrackingMode::TrackCom; bMatched = true; }
+        else if (S == TEXT("targetbody")) { TrackingMode = EMjCameraTrackingMode::TargetBody; bMatched = true; }
+        else if (S == TEXT("targetbodycom")) { TrackingMode = EMjCameraTrackingMode::TargetBodyCom; bMatched = true; }
+        if (bMatched) bOverride_TrackingMode = true;
     }
     { // xml_enum: projection -> EMjCameraProjection
         FString S = Node->GetAttribute(TEXT("projection"));
         S = S.ToLower();
-        if      (S == TEXT("orthographic")) Projection = EMjCameraProjection::Orthographic;
-        else if (S == TEXT("perspective")) Projection = EMjCameraProjection::Perspective;
-        if (!S.IsEmpty()) bOverride_Projection = true;
+        bool bMatched = false;
+        if      (S == TEXT("orthographic")) { Projection = EMjCameraProjection::Orthographic; bMatched = true; }
+        else if (S == TEXT("perspective")) { Projection = EMjCameraProjection::Perspective; bMatched = true; }
+        if (bMatched) bOverride_Projection = true;
     }
     MjXmlUtils::ReadAttrFloat(Node, TEXT("fovy"), fovy, bOverride_fovy);
     MjXmlUtils::ReadAttrFloat(Node, TEXT("ipd"), ipd, bOverride_ipd);
@@ -750,11 +753,11 @@ void UMjCamera::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& C
     }
     if (bOverride_Pos)  SetRelativeLocation(Pos);
     if (bOverride_Quat) SetRelativeRotation(Quat);
-    // --- CODEGEN_IMPORT_END ---
+        // --- CODEGEN_IMPORT_END ---
 
-    // Name
-    if (!MjXmlUtils::ReadAttrString(Node, TEXT("name"), MjName))
-        MjName = TEXT("Camera");
+    // Name fallback (codegen above reads MjName from the "name" attribute;
+    // here we provide a sensible default if the user omitted it).
+    if (MjName.IsEmpty()) MjName = TEXT("Camera");
 
     // fovy
     FString FovyStr = Node->GetAttribute(TEXT("fovy"));
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjClockSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjClockSensor.cpp
index 68fffe2..c57125b 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjClockSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjClockSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjClockSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjClockSensor::UMjClockSensor()
 {
     Type = EMjSensorType::Clock;
 }
 
-void UMjClockSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjClockSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjClockSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjContactSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjContactSensor.cpp
index c1f3011..cde9646 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjContactSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjContactSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjContactSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjContactSensor::UMjContactSensor()
 {
     Type = EMjSensorType::Contact;
 }
 
-void UMjContactSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjContactSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjContactSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjEKineticSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjEKineticSensor.cpp
index 2542a32..01f7275 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjEKineticSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjEKineticSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjEKineticSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjEKineticSensor::UMjEKineticSensor()
 {
     Type = EMjSensorType::EKinetic;
 }
 
-void UMjEKineticSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjEKineticSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjEKineticSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjEPotentialSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjEPotentialSensor.cpp
index a8bd394..202cf62 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjEPotentialSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjEPotentialSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjEPotentialSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjEPotentialSensor::UMjEPotentialSensor()
 {
     Type = EMjSensorType::EPotential;
 }
 
-void UMjEPotentialSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjEPotentialSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjEPotentialSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjForceSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjForceSensor.cpp
index 3e140fc..3de8e3a 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjForceSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjForceSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjForceSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjForceSensor::UMjForceSensor()
 {
     Type = EMjSensorType::Force;
 }
 
-void UMjForceSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjForceSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjForceSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjFrameAngAccSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjFrameAngAccSensor.cpp
index b597799..9608dcc 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjFrameAngAccSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjFrameAngAccSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjFrameAngAccSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjFrameAngAccSensor::UMjFrameAngAccSensor()
 {
     Type = EMjSensorType::FrameAngAcc;
 }
 
-void UMjFrameAngAccSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjFrameAngAccSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjFrameAngAccSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjFrameAngVelSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjFrameAngVelSensor.cpp
index bc614eb..b5952f0 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjFrameAngVelSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjFrameAngVelSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjFrameAngVelSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjFrameAngVelSensor::UMjFrameAngVelSensor()
 {
     Type = EMjSensorType::FrameAngVel;
 }
 
-void UMjFrameAngVelSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjFrameAngVelSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjFrameAngVelSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjFrameLinAccSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjFrameLinAccSensor.cpp
index 5421170..925f269 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjFrameLinAccSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjFrameLinAccSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjFrameLinAccSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjFrameLinAccSensor::UMjFrameLinAccSensor()
 {
     Type = EMjSensorType::FrameLinAcc;
 }
 
-void UMjFrameLinAccSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjFrameLinAccSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjFrameLinAccSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjFrameLinVelSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjFrameLinVelSensor.cpp
index 29a6c1a..3064df3 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjFrameLinVelSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjFrameLinVelSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjFrameLinVelSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjFrameLinVelSensor::UMjFrameLinVelSensor()
 {
     Type = EMjSensorType::FrameLinVel;
 }
 
-void UMjFrameLinVelSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjFrameLinVelSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjFrameLinVelSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjFramePosSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjFramePosSensor.cpp
index 768fb3e..716a940 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjFramePosSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjFramePosSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjFramePosSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjFramePosSensor::UMjFramePosSensor()
 {
     Type = EMjSensorType::FramePos;
 }
 
-void UMjFramePosSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjFramePosSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjFramePosSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjFrameQuatSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjFrameQuatSensor.cpp
index 1d1b016..c16132b 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjFrameQuatSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjFrameQuatSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjFrameQuatSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjFrameQuatSensor::UMjFrameQuatSensor()
 {
     Type = EMjSensorType::FrameQuat;
 }
 
-void UMjFrameQuatSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjFrameQuatSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjFrameQuatSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjFrameXAxisSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjFrameXAxisSensor.cpp
index b5671a7..a9d2b56 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjFrameXAxisSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjFrameXAxisSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjFrameXAxisSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjFrameXAxisSensor::UMjFrameXAxisSensor()
 {
     Type = EMjSensorType::FrameXAxis;
 }
 
-void UMjFrameXAxisSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjFrameXAxisSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjFrameXAxisSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjFrameYAxisSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjFrameYAxisSensor.cpp
index 7de49a0..e9d237a 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjFrameYAxisSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjFrameYAxisSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjFrameYAxisSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjFrameYAxisSensor::UMjFrameYAxisSensor()
 {
     Type = EMjSensorType::FrameYAxis;
 }
 
-void UMjFrameYAxisSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjFrameYAxisSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjFrameYAxisSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjFrameZAxisSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjFrameZAxisSensor.cpp
index f12fa5d..79f8c0e 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjFrameZAxisSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjFrameZAxisSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjFrameZAxisSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjFrameZAxisSensor::UMjFrameZAxisSensor()
 {
     Type = EMjSensorType::FrameZAxis;
 }
 
-void UMjFrameZAxisSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjFrameZAxisSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjFrameZAxisSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjGeomDistSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjGeomDistSensor.cpp
index f61fb61..72d8040 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjGeomDistSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjGeomDistSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjGeomDistSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjGeomDistSensor::UMjGeomDistSensor()
 {
     Type = EMjSensorType::GeomDist;
 }
 
-void UMjGeomDistSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjGeomDistSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjGeomDistSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjGeomFromToSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjGeomFromToSensor.cpp
index 822c0bf..220e01c 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjGeomFromToSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjGeomFromToSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjGeomFromToSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjGeomFromToSensor::UMjGeomFromToSensor()
 {
     Type = EMjSensorType::GeomFromTo;
 }
 
-void UMjGeomFromToSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjGeomFromToSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjGeomFromToSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjGeomNormalSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjGeomNormalSensor.cpp
index d30bf53..e7877aa 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjGeomNormalSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjGeomNormalSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjGeomNormalSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjGeomNormalSensor::UMjGeomNormalSensor()
 {
     Type = EMjSensorType::GeomNormal;
 }
 
-void UMjGeomNormalSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjGeomNormalSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjGeomNormalSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjGyro.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjGyro.cpp
index 020722e..1aa11ed 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjGyro.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjGyro.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjGyro.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjGyro::UMjGyro()
 {
     Type = EMjSensorType::Gyro;
 }
 
-void UMjGyro::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjGyro::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjGyro::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjInsideSiteSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjInsideSiteSensor.cpp
index d7323c9..a31eb6c 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjInsideSiteSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjInsideSiteSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjInsideSiteSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjInsideSiteSensor::UMjInsideSiteSensor()
 {
     Type = EMjSensorType::InsideSite;
 }
 
-void UMjInsideSiteSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjInsideSiteSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjInsideSiteSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjJointActFrcSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjJointActFrcSensor.cpp
index b8e2cba..1c4fc29 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjJointActFrcSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjJointActFrcSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjJointActFrcSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjJointActFrcSensor::UMjJointActFrcSensor()
 {
     Type = EMjSensorType::JointActFrc;
 }
 
-void UMjJointActFrcSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjJointActFrcSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjJointActFrcSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjJointLimitFrcSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjJointLimitFrcSensor.cpp
index 62becfd..eb80d81 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjJointLimitFrcSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjJointLimitFrcSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjJointLimitFrcSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjJointLimitFrcSensor::UMjJointLimitFrcSensor()
 {
     Type = EMjSensorType::JointLimitFrc;
 }
 
-void UMjJointLimitFrcSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjJointLimitFrcSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjJointLimitFrcSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjJointLimitPosSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjJointLimitPosSensor.cpp
index 4217fc9..0276d0e 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjJointLimitPosSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjJointLimitPosSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjJointLimitPosSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjJointLimitPosSensor::UMjJointLimitPosSensor()
 {
     Type = EMjSensorType::JointLimitPos;
 }
 
-void UMjJointLimitPosSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjJointLimitPosSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjJointLimitPosSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjJointLimitVelSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjJointLimitVelSensor.cpp
index c16ebbd..4916352 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjJointLimitVelSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjJointLimitVelSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjJointLimitVelSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjJointLimitVelSensor::UMjJointLimitVelSensor()
 {
     Type = EMjSensorType::JointLimitVel;
 }
 
-void UMjJointLimitVelSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjJointLimitVelSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjJointLimitVelSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjJointPosSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjJointPosSensor.cpp
index 2d14341..5f9b2e4 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjJointPosSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjJointPosSensor.cpp
@@ -36,7 +36,6 @@ void UMjJointPosSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
     Super::ExportTo(Element, Default);
 
     // --- CODEGEN_EXPORT_START ---
-
     // --- CODEGEN_EXPORT_END ---
 }
 
@@ -46,7 +45,6 @@ void UMjJointPosSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSet
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
     // --- CODEGEN_IMPORT_END ---
 }
 
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjJointVelSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjJointVelSensor.cpp
index e0830f4..6e3c534 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjJointVelSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjJointVelSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjJointVelSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjJointVelSensor::UMjJointVelSensor()
 {
     Type = EMjSensorType::JointVel;
 }
 
-void UMjJointVelSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjJointVelSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjJointVelSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjMagnetometer.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjMagnetometer.cpp
index 977cf39..41afa06 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjMagnetometer.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjMagnetometer.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjMagnetometer.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjMagnetometer::UMjMagnetometer()
 {
     Type = EMjSensorType::Magnetometer;
 }
 
-void UMjMagnetometer::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjMagnetometer::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjMagnetometer::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjPluginSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjPluginSensor.cpp
index 261c0ba..eba640b 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjPluginSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjPluginSensor.cpp
@@ -41,7 +41,7 @@ void UMjPluginSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSetti
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
@@ -52,6 +52,6 @@ void UMjPluginSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
     Super::ExportTo(Element, Default);
 
     // --- CODEGEN_EXPORT_START ---
-
+    // (no type-specific schema exports)
     // --- CODEGEN_EXPORT_END ---
 }
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjRangeFinderSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjRangeFinderSensor.cpp
index 043f546..8fa9413 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjRangeFinderSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjRangeFinderSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjRangeFinderSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjRangeFinderSensor::UMjRangeFinderSensor()
 {
     Type = EMjSensorType::RangeFinder;
 }
 
-void UMjRangeFinderSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjRangeFinderSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjRangeFinderSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjSensor.cpp
index b9e2f52..2c2d73d 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjSensor.cpp
@@ -110,8 +110,8 @@ void UMjSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
 {
     if (!Element) return;
 
-    if (!TargetName.IsEmpty()) mjs_setString(Element->objname, TCHAR_TO_UTF8(*TargetName));
-    if (!ReferenceName.IsEmpty()) mjs_setString(Element->refname, TCHAR_TO_UTF8(*ReferenceName));
+    MjSetString(Element->objname, TargetName);
+    MjSetString(Element->refname, ReferenceName);
 
     // Most built-in sensor types have `dim` derived from the sensor type
     // during mj_compile (e.g. accelerometer dim=3). Writing 0 here zeros
@@ -126,76 +126,66 @@ void UMjSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
     
     switch(Type)
     {
-        case EMjSensorType::Touch:          Element->type = mjSENS_TOUCH;         Element->objtype = mjOBJ_SITE; break;
-        case EMjSensorType::Accelerometer:  Element->type = mjSENS_ACCELEROMETER; Element->objtype = mjOBJ_SITE; break;
-        case EMjSensorType::Velocimeter:    Element->type = mjSENS_VELOCIMETER;   Element->objtype = mjOBJ_SITE; break;
-        case EMjSensorType::Gyro:           Element->type = mjSENS_GYRO;          Element->objtype = mjOBJ_SITE; break;
-        case EMjSensorType::Force:          Element->type = mjSENS_FORCE;         Element->objtype = mjOBJ_SITE; break;
-        case EMjSensorType::Torque:         Element->type = mjSENS_TORQUE;        Element->objtype = mjOBJ_SITE; break;
-        case EMjSensorType::Magnetometer:   Element->type = mjSENS_MAGNETOMETER;  Element->objtype = mjOBJ_SITE; break;
-
-        case EMjSensorType::CamProjection:
-            Element->type = mjSENS_CAMPROJECTION;
-            Element->objtype = mjOBJ_SITE;
-            Element->reftype = mjOBJ_CAMERA;
-            break;
-
+        // --- CODEGEN_SENSOR_TYPE_SWITCH_START ---
+        case EMjSensorType::Touch: Element->type = mjSENS_TOUCH; Element->objtype = mjOBJ_SITE; break;
+        case EMjSensorType::Accelerometer: Element->type = mjSENS_ACCELEROMETER; Element->objtype = mjOBJ_SITE; break;
+        case EMjSensorType::Velocimeter: Element->type = mjSENS_VELOCIMETER; Element->objtype = mjOBJ_SITE; break;
+        case EMjSensorType::Gyro: Element->type = mjSENS_GYRO; Element->objtype = mjOBJ_SITE; break;
+        case EMjSensorType::Force: Element->type = mjSENS_FORCE; Element->objtype = mjOBJ_SITE; break;
+        case EMjSensorType::Torque: Element->type = mjSENS_TORQUE; Element->objtype = mjOBJ_SITE; break;
+        case EMjSensorType::Magnetometer: Element->type = mjSENS_MAGNETOMETER; Element->objtype = mjOBJ_SITE; break;
+        case EMjSensorType::CamProjection: Element->type = mjSENS_CAMPROJECTION; Element->objtype = mjOBJ_SITE; Element->reftype = mjOBJ_CAMERA; break;
         case EMjSensorType::RangeFinder:
-            Element->type = mjSENS_RANGEFINDER;
-            Element->objtype = (ObjType == EMjObjType::Camera) ? mjOBJ_CAMERA : mjOBJ_SITE;
-            break;
-
-        case EMjSensorType::JointPos:        Element->type = mjSENS_JOINTPOS;      Element->objtype = mjOBJ_JOINT; break;
-        case EMjSensorType::JointVel:        Element->type = mjSENS_JOINTVEL;      Element->objtype = mjOBJ_JOINT; break;
-        case EMjSensorType::JointActFrc:     Element->type = mjSENS_JOINTACTFRC;   Element->objtype = mjOBJ_JOINT; break;
-        case EMjSensorType::BallQuat:        Element->type = mjSENS_BALLQUAT;      Element->objtype = mjOBJ_JOINT; break;
-        case EMjSensorType::BallAngVel:      Element->type = mjSENS_BALLANGVEL;    Element->objtype = mjOBJ_JOINT; break;
-        case EMjSensorType::JointLimitPos:   Element->type = mjSENS_JOINTLIMITPOS; Element->objtype = mjOBJ_JOINT; break;
-        case EMjSensorType::JointLimitVel:   Element->type = mjSENS_JOINTLIMITVEL; Element->objtype = mjOBJ_JOINT; break;
-        case EMjSensorType::JointLimitFrc:   Element->type = mjSENS_JOINTLIMITFRC; Element->objtype = mjOBJ_JOINT; break;
-
-        case EMjSensorType::TendonPos:       Element->type = mjSENS_TENDONPOS;      Element->objtype = mjOBJ_TENDON; break;
-        case EMjSensorType::TendonVel:       Element->type = mjSENS_TENDONVEL;      Element->objtype = mjOBJ_TENDON; break;
-        case EMjSensorType::TendonActFrc:    Element->type = mjSENS_TENDONACTFRC;   Element->objtype = mjOBJ_TENDON; break;
-        case EMjSensorType::TendonLimitPos:  Element->type = mjSENS_TENDONLIMITPOS; Element->objtype = mjOBJ_TENDON; break;
-        case EMjSensorType::TendonLimitVel:  Element->type = mjSENS_TENDONLIMITVEL; Element->objtype = mjOBJ_TENDON; break;
-        case EMjSensorType::TendonLimitFrc:  Element->type = mjSENS_TENDONLIMITFRC; Element->objtype = mjOBJ_TENDON; break;
-
-        case EMjSensorType::ActuatorPos:     Element->type = mjSENS_ACTUATORPOS; Element->objtype = mjOBJ_ACTUATOR; break;
-        case EMjSensorType::ActuatorVel:     Element->type = mjSENS_ACTUATORVEL; Element->objtype = mjOBJ_ACTUATOR; break;
-        case EMjSensorType::ActuatorFrc:     Element->type = mjSENS_ACTUATORFRC; Element->objtype = mjOBJ_ACTUATOR; break;
-
-        case EMjSensorType::FramePos:       Element->type = mjSENS_FRAMEPOS;    break;
-        case EMjSensorType::FrameQuat:      Element->type = mjSENS_FRAMEQUAT;   break;
-        case EMjSensorType::FrameXAxis:     Element->type = mjSENS_FRAMEXAXIS;  break;
-        case EMjSensorType::FrameYAxis:     Element->type = mjSENS_FRAMEYAXIS;  break;
-        case EMjSensorType::FrameZAxis:     Element->type = mjSENS_FRAMEZAXIS;  break;
-        case EMjSensorType::FrameLinVel:    Element->type = mjSENS_FRAMELINVEL; break;
-        case EMjSensorType::FrameAngVel:    Element->type = mjSENS_FRAMEANGVEL; break;
-        case EMjSensorType::FrameLinAcc:    Element->type = mjSENS_FRAMELINACC; break;
-        case EMjSensorType::FrameAngAcc:    Element->type = mjSENS_FRAMEANGACC; break;
-
-        case EMjSensorType::InsideSite:     Element->type = mjSENS_INSIDESITE;  Element->objtype = (mjtObj)EnumToMjObj(ObjType); Element->reftype = mjOBJ_SITE; break;
-        case EMjSensorType::SubtreeCom:     Element->type = mjSENS_SUBTREECOM;  Element->objtype = mjOBJ_BODY; break;
-        case EMjSensorType::SubtreeLinVel:  Element->type = mjSENS_SUBTREELINVEL; Element->objtype = mjOBJ_BODY; break;
-        case EMjSensorType::SubtreeAngMom:  Element->type = mjSENS_SUBTREEANGMOM; Element->objtype = mjOBJ_BODY; break;
-
-        case EMjSensorType::GeomDist:       Element->type = mjSENS_GEOMDIST;   break;
-        case EMjSensorType::GeomNormal:     Element->type = mjSENS_GEOMNORMAL; break;
-        case EMjSensorType::GeomFromTo:     Element->type = mjSENS_GEOMFROMTO; break;
-
-        case EMjSensorType::Contact:        Element->type = mjSENS_CONTACT; break;
-        case EMjSensorType::EPotential:     Element->type = mjSENS_E_POTENTIAL; break;
-        case EMjSensorType::EKinetic:      Element->type = mjSENS_E_KINETIC;   break;
-        case EMjSensorType::Clock:         Element->type = mjSENS_CLOCK;       break;
-
-        case EMjSensorType::Tactile:        Element->type = mjSENS_TACTILE; Element->objtype = mjOBJ_MESH; Element->reftype = mjOBJ_GEOM; break;
-        case EMjSensorType::Plugin:         Element->type = mjSENS_PLUGIN; break;
-        case EMjSensorType::User:           Element->type = mjSENS_USER;   break;
-
-        default:                            Element->type = mjSENS_ACCELEROMETER; Element->objtype = mjOBJ_SITE; break;
+            Element->type = mjSENS_RANGEFINDER; Element->objtype = (ObjType == EMjObjType::Camera) ? mjOBJ_CAMERA : mjOBJ_SITE; break;
+        case EMjSensorType::JointPos: Element->type = mjSENS_JOINTPOS; Element->objtype = mjOBJ_JOINT; break;
+        case EMjSensorType::JointVel: Element->type = mjSENS_JOINTVEL; Element->objtype = mjOBJ_JOINT; break;
+        case EMjSensorType::BallQuat: Element->type = mjSENS_BALLQUAT; Element->objtype = mjOBJ_JOINT; break;
+        case EMjSensorType::BallAngVel: Element->type = mjSENS_BALLANGVEL; Element->objtype = mjOBJ_JOINT; break;
+        case EMjSensorType::JointLimitPos: Element->type = mjSENS_JOINTLIMITPOS; Element->objtype = mjOBJ_JOINT; break;
+        case EMjSensorType::JointLimitVel: Element->type = mjSENS_JOINTLIMITVEL; Element->objtype = mjOBJ_JOINT; break;
+        case EMjSensorType::JointLimitFrc: Element->type = mjSENS_JOINTLIMITFRC; Element->objtype = mjOBJ_JOINT; break;
+        case EMjSensorType::TendonPos: Element->type = mjSENS_TENDONPOS; Element->objtype = mjOBJ_TENDON; break;
+        case EMjSensorType::TendonVel: Element->type = mjSENS_TENDONVEL; Element->objtype = mjOBJ_TENDON; break;
+        case EMjSensorType::TendonLimitPos: Element->type = mjSENS_TENDONLIMITPOS; Element->objtype = mjOBJ_TENDON; break;
+        case EMjSensorType::TendonLimitVel: Element->type = mjSENS_TENDONLIMITVEL; Element->objtype = mjOBJ_TENDON; break;
+        case EMjSensorType::TendonLimitFrc: Element->type = mjSENS_TENDONLIMITFRC; Element->objtype = mjOBJ_TENDON; break;
+        case EMjSensorType::ActuatorPos: Element->type = mjSENS_ACTUATORPOS; Element->objtype = mjOBJ_ACTUATOR; break;
+        case EMjSensorType::ActuatorVel: Element->type = mjSENS_ACTUATORVEL; Element->objtype = mjOBJ_ACTUATOR; break;
+        case EMjSensorType::ActuatorFrc: Element->type = mjSENS_ACTUATORFRC; Element->objtype = mjOBJ_ACTUATOR; break;
+        case EMjSensorType::JointActFrc: Element->type = mjSENS_JOINTACTFRC; Element->objtype = mjOBJ_JOINT; break;
+        case EMjSensorType::TendonActFrc: Element->type = mjSENS_TENDONACTFRC; Element->objtype = mjOBJ_TENDON; break;
+        case EMjSensorType::FramePos: Element->type = mjSENS_FRAMEPOS; break;
+        case EMjSensorType::FrameQuat: Element->type = mjSENS_FRAMEQUAT; break;
+        case EMjSensorType::FrameXAxis: Element->type = mjSENS_FRAMEXAXIS; break;
+        case EMjSensorType::FrameYAxis: Element->type = mjSENS_FRAMEYAXIS; break;
+        case EMjSensorType::FrameZAxis: Element->type = mjSENS_FRAMEZAXIS; break;
+        case EMjSensorType::FrameLinVel: Element->type = mjSENS_FRAMELINVEL; break;
+        case EMjSensorType::FrameAngVel: Element->type = mjSENS_FRAMEANGVEL; break;
+        case EMjSensorType::FrameLinAcc: Element->type = mjSENS_FRAMELINACC; break;
+        case EMjSensorType::FrameAngAcc: Element->type = mjSENS_FRAMEANGACC; break;
+        case EMjSensorType::SubtreeCom: Element->type = mjSENS_SUBTREECOM; Element->objtype = mjOBJ_BODY; break;
+        case EMjSensorType::SubtreeLinVel: Element->type = mjSENS_SUBTREELINVEL; Element->objtype = mjOBJ_BODY; break;
+        case EMjSensorType::SubtreeAngMom: Element->type = mjSENS_SUBTREEANGMOM; Element->objtype = mjOBJ_BODY; break;
+        case EMjSensorType::InsideSite: Element->type = mjSENS_INSIDESITE; Element->reftype = mjOBJ_SITE; break;
+        case EMjSensorType::GeomDist: Element->type = mjSENS_GEOMDIST; break;
+        case EMjSensorType::GeomNormal: Element->type = mjSENS_GEOMNORMAL; break;
+        case EMjSensorType::GeomFromTo: Element->type = mjSENS_GEOMFROMTO; break;
+        case EMjSensorType::Contact: Element->type = mjSENS_CONTACT; break;
+        case EMjSensorType::EPotential: Element->type = mjSENS_E_POTENTIAL; Element->objtype = mjOBJ_UNKNOWN; break;
+        case EMjSensorType::EKinetic: Element->type = mjSENS_E_KINETIC; Element->objtype = mjOBJ_UNKNOWN; break;
+        case EMjSensorType::Clock: Element->type = mjSENS_CLOCK; Element->objtype = mjOBJ_UNKNOWN; break;
+        case EMjSensorType::Tactile: Element->type = mjSENS_TACTILE; Element->objtype = mjOBJ_MESH; Element->reftype = mjOBJ_GEOM; break;
+        case EMjSensorType::User: Element->type = mjSENS_USER; break;
+        case EMjSensorType::Plugin: Element->type = mjSENS_PLUGIN; break;
+        default: Element->type = mjSENS_ACCELEROMETER; Element->objtype = mjOBJ_SITE; break;
+        // --- CODEGEN_SENSOR_TYPE_SWITCH_END ---
     }
 
+    // Variable objtype / reftype handling. The codegen switch above sets
+    // STATIC objtype/reftype literals when sensor_per_type carries a
+    // mjOBJ_X value. For sensors whose objtype/reftype is "from_xml" or
+    // "computed" in MuJoCo, the UE side reads ObjType / RefType properties
+    // and translates them here AFTER the switch fires.
     if (Element->type >= mjSENS_FRAMEPOS && Element->type <= mjSENS_FRAMEANGACC)
     {
         Element->objtype = (mjtObj)EnumToMjObj(ObjType);
@@ -206,8 +196,10 @@ void UMjSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
         Element->objtype = (mjtObj)EnumToMjObj(ObjType);
         Element->reftype = (mjtObj)EnumToMjObj(RefType);
     }
-    else if (Type == EMjSensorType::User)
+    else if (Type == EMjSensorType::User || Type == EMjSensorType::InsideSite)
     {
+        // InsideSite's reftype = mjOBJ_SITE is already set by the codegen
+        // switch above; here we add the UE-driven objtype.
         Element->objtype = (mjtObj)EnumToMjObj(ObjType);
     }
 
@@ -215,10 +207,12 @@ void UMjSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
     if (bOverride_nsample) Element->nsample = nsample;
     if (bOverride_interp) Element->interp = interp;
     if (bOverride_delay) Element->delay = delay;
-    if (bOverride_interval) { for (int32 i = 0; i < interval.Num(); ++i) Element->interval[i] = interval[i]; }
+    if (bOverride_interval) { for (int32 i = 0; i < FMath::Min(interval.Num(), 2); ++i) Element->interval[i] = interval[i]; }
     if (bOverride_cutoff) Element->cutoff = cutoff;
     if (bOverride_noise) Element->noise = noise;
-    // --- CODEGEN_EXPORT_END ---
+    MjSetString(Element->objname, TargetName);
+    MjSetString(Element->refname, ReferenceName);
+        // --- CODEGEN_EXPORT_END ---
 }
 
 void UMjSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
@@ -226,72 +220,45 @@ void UMjSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& C
     if (!Node) return;
 
         // --- CODEGEN_IMPORT_START ---
+    MjXmlUtils::ReadAttrString(Node, TEXT("class"), MjClassName);
+    { // xml_enum: objtype -> EMjObjType
+        FString S = Node->GetAttribute(TEXT("objtype"));
+        S = S.ToLower();
+        if      (S == TEXT("body")) { ObjType = EMjObjType::Body; }
+        else if (S == TEXT("xbody")) { ObjType = EMjObjType::XBody; }
+        else if (S == TEXT("joint")) { ObjType = EMjObjType::Joint; }
+        else if (S == TEXT("dof")) { ObjType = EMjObjType::DoF; }
+        else if (S == TEXT("geom")) { ObjType = EMjObjType::Geom; }
+        else if (S == TEXT("site")) { ObjType = EMjObjType::Site; }
+        else if (S == TEXT("camera")) { ObjType = EMjObjType::Camera; }
+        else if (S == TEXT("light")) { ObjType = EMjObjType::Light; }
+        else if (S == TEXT("mesh")) { ObjType = EMjObjType::Mesh; }
+        else if (S == TEXT("hfield")) { ObjType = EMjObjType::HField; }
+        else if (S == TEXT("texture")) { ObjType = EMjObjType::Texture; }
+        else if (S == TEXT("material")) { ObjType = EMjObjType::Material; }
+        else if (S == TEXT("pair")) { ObjType = EMjObjType::Pair; }
+        else if (S == TEXT("exclude")) { ObjType = EMjObjType::Exclude; }
+        else if (S == TEXT("equality")) { ObjType = EMjObjType::Equality; }
+        else if (S == TEXT("tendon")) { ObjType = EMjObjType::Tendon; }
+        else if (S == TEXT("actuator")) { ObjType = EMjObjType::Actuator; }
+    }
+    { // xml_enum: reftype -> EMjObjType
+        FString S = Node->GetAttribute(TEXT("reftype"));
+        S = S.ToLower();
+        if      (S == TEXT("body")) { RefType = EMjObjType::Body; }
+        else if (S == TEXT("xbody")) { RefType = EMjObjType::XBody; }
+        else if (S == TEXT("joint")) { RefType = EMjObjType::Joint; }
+        else if (S == TEXT("geom")) { RefType = EMjObjType::Geom; }
+        else if (S == TEXT("site")) { RefType = EMjObjType::Site; }
+        else if (S == TEXT("camera")) { RefType = EMjObjType::Camera; }
+    }
     MjXmlUtils::ReadAttrInt(Node, TEXT("nsample"), nsample, bOverride_nsample);
     MjXmlUtils::ReadAttrInt(Node, TEXT("interp"), interp, bOverride_interp);
     MjXmlUtils::ReadAttrFloat(Node, TEXT("delay"), delay, bOverride_delay);
     MjXmlUtils::ReadAttrFloatArray(Node, TEXT("interval"), interval, bOverride_interval);
     MjXmlUtils::ReadAttrFloat(Node, TEXT("cutoff"), cutoff, bOverride_cutoff);
     MjXmlUtils::ReadAttrFloat(Node, TEXT("noise"), noise, bOverride_noise);
-    // --- CODEGEN_IMPORT_END ---
-
-    // Determine sensor type from the XML tag name
-    static const TMap<FString, EMjSensorType> TagToType = {
-        {TEXT("touch"),           EMjSensorType::Touch},
-        {TEXT("accelerometer"),   EMjSensorType::Accelerometer},
-        {TEXT("velocimeter"),     EMjSensorType::Velocimeter},
-        {TEXT("gyro"),            EMjSensorType::Gyro},
-        {TEXT("force"),           EMjSensorType::Force},
-        {TEXT("torque"),          EMjSensorType::Torque},
-        {TEXT("magnetometer"),    EMjSensorType::Magnetometer},
-        {TEXT("rangefinder"),     EMjSensorType::RangeFinder},
-        {TEXT("camprojection"),   EMjSensorType::CamProjection},
-        {TEXT("jointpos"),        EMjSensorType::JointPos},
-        {TEXT("jointvel"),        EMjSensorType::JointVel},
-        {TEXT("jointactfrc"),     EMjSensorType::JointActFrc},
-        {TEXT("tendonpos"),       EMjSensorType::TendonPos},
-        {TEXT("tendonvel"),       EMjSensorType::TendonVel},
-        {TEXT("tendonactfrc"),    EMjSensorType::TendonActFrc},
-        {TEXT("actuatorpos"),     EMjSensorType::ActuatorPos},
-        {TEXT("actuatorvel"),     EMjSensorType::ActuatorVel},
-        {TEXT("actuatorfrc"),     EMjSensorType::ActuatorFrc},
-        {TEXT("ballquat"),        EMjSensorType::BallQuat},
-        {TEXT("ballangvel"),      EMjSensorType::BallAngVel},
-        {TEXT("jointlimitpos"),   EMjSensorType::JointLimitPos},
-        {TEXT("jointlimitvel"),   EMjSensorType::JointLimitVel},
-        {TEXT("jointlimitfrc"),   EMjSensorType::JointLimitFrc},
-        {TEXT("tendonlimitpos"),  EMjSensorType::TendonLimitPos},
-        {TEXT("tendonlimitvel"),  EMjSensorType::TendonLimitVel},
-        {TEXT("tendonlimitfrc"),  EMjSensorType::TendonLimitFrc},
-        {TEXT("framepos"),        EMjSensorType::FramePos},
-        {TEXT("framequat"),       EMjSensorType::FrameQuat},
-        {TEXT("framexaxis"),      EMjSensorType::FrameXAxis},
-        {TEXT("frameyaxis"),      EMjSensorType::FrameYAxis},
-        {TEXT("framezaxis"),      EMjSensorType::FrameZAxis},
-        {TEXT("framelinvel"),     EMjSensorType::FrameLinVel},
-        {TEXT("frameangvel"),     EMjSensorType::FrameAngVel},
-        {TEXT("framelinacc"),     EMjSensorType::FrameLinAcc},
-        {TEXT("frameangacc"),     EMjSensorType::FrameAngAcc},
-        {TEXT("subtreecom"),      EMjSensorType::SubtreeCom},
-        {TEXT("subtreelinvel"),   EMjSensorType::SubtreeLinVel},
-        {TEXT("subtreeangmom"),   EMjSensorType::SubtreeAngMom},
-        {TEXT("insidesite"),      EMjSensorType::InsideSite},
-        {TEXT("geomdist"),        EMjSensorType::GeomDist},
-        {TEXT("geomnormal"),      EMjSensorType::GeomNormal},
-        {TEXT("geomfromto"),      EMjSensorType::GeomFromTo},
-        {TEXT("contact"),         EMjSensorType::Contact},
-        {TEXT("e_potential"),     EMjSensorType::EPotential},
-        {TEXT("e_kinetic"),       EMjSensorType::EKinetic},
-        {TEXT("clock"),           EMjSensorType::Clock},
-        {TEXT("tactile"),         EMjSensorType::Tactile},
-        {TEXT("plugin"),          EMjSensorType::Plugin},
-        {TEXT("user"),            EMjSensorType::User},
-    };
-    const FString Tag = Node->GetTag().ToLower();
-    const EMjSensorType* Found = TagToType.Find(Tag);
-    if (Found) Type = *Found;
-
-
-    // Object name: try each attribute in priority order
+    // target_collation: -> TargetName
     TargetName = Node->GetAttribute(TEXT("site"));
     if (TargetName.IsEmpty()) TargetName = Node->GetAttribute(TEXT("joint"));
     if (TargetName.IsEmpty()) TargetName = Node->GetAttribute(TEXT("tendon"));
@@ -299,46 +266,77 @@ void UMjSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& C
     if (TargetName.IsEmpty()) TargetName = Node->GetAttribute(TEXT("body"));
     if (TargetName.IsEmpty()) TargetName = Node->GetAttribute(TEXT("geom"));
     if (TargetName.IsEmpty()) TargetName = Node->GetAttribute(TEXT("objname"));
-
-    // Reference name (for frame sensors, camprojection, etc.)
+    if (TargetName.IsEmpty()) TargetName = Node->GetAttribute(TEXT("camera"));
+    if (TargetName.IsEmpty()) TargetName = Node->GetAttribute(TEXT("mesh"));
+    // target_collation: -> ReferenceName
     ReferenceName = Node->GetAttribute(TEXT("refname"));
+        // --- CODEGEN_IMPORT_END ---
 
-    // objtype / reftype — string attributes specifying object/reference type for frame,
-    // geomfromto, contact, user, and camprojection sensors.
-    // Map from MJCF string → EMjObjType so ExportTo() can write the correct mjtObj enum.
-    static const TMap<FString, EMjObjType> ObjTypeStrMap = {
-        {TEXT("body"),      EMjObjType::Body},
-        {TEXT("xbody"),     EMjObjType::XBody},
-        {TEXT("joint"),     EMjObjType::Joint},
-        {TEXT("dof"),       EMjObjType::DoF},
-        {TEXT("geom"),      EMjObjType::Geom},
-        {TEXT("site"),      EMjObjType::Site},
-        {TEXT("camera"),    EMjObjType::Camera},
-        {TEXT("light"),     EMjObjType::Light},
-        {TEXT("mesh"),      EMjObjType::Mesh},
-        {TEXT("hfield"),    EMjObjType::HField},
-        {TEXT("texture"),   EMjObjType::Texture},
-        {TEXT("material"),  EMjObjType::Material},
-        {TEXT("pair"),      EMjObjType::Pair},
-        {TEXT("exclude"),   EMjObjType::Exclude},
-        {TEXT("equality"),  EMjObjType::Equality},
-        {TEXT("tendon"),    EMjObjType::Tendon},
-        {TEXT("actuator"),  EMjObjType::Actuator},
+    // Determine sensor type from the XML tag name
+    static const TMap<FString, EMjSensorType> TagToType = {
+        // --- CODEGEN_SENSOR_TAG_TO_TYPE_START ---
+        {TEXT("touch"), EMjSensorType::Touch},
+        {TEXT("accelerometer"), EMjSensorType::Accelerometer},
+        {TEXT("velocimeter"), EMjSensorType::Velocimeter},
+        {TEXT("gyro"), EMjSensorType::Gyro},
+        {TEXT("force"), EMjSensorType::Force},
+        {TEXT("torque"), EMjSensorType::Torque},
+        {TEXT("magnetometer"), EMjSensorType::Magnetometer},
+        {TEXT("camprojection"), EMjSensorType::CamProjection},
+        {TEXT("rangefinder"), EMjSensorType::RangeFinder},
+        {TEXT("jointpos"), EMjSensorType::JointPos},
+        {TEXT("jointvel"), EMjSensorType::JointVel},
+        {TEXT("ballquat"), EMjSensorType::BallQuat},
+        {TEXT("ballangvel"), EMjSensorType::BallAngVel},
+        {TEXT("jointlimitpos"), EMjSensorType::JointLimitPos},
+        {TEXT("jointlimitvel"), EMjSensorType::JointLimitVel},
+        {TEXT("jointlimitfrc"), EMjSensorType::JointLimitFrc},
+        {TEXT("tendonpos"), EMjSensorType::TendonPos},
+        {TEXT("tendonvel"), EMjSensorType::TendonVel},
+        {TEXT("tendonlimitpos"), EMjSensorType::TendonLimitPos},
+        {TEXT("tendonlimitvel"), EMjSensorType::TendonLimitVel},
+        {TEXT("tendonlimitfrc"), EMjSensorType::TendonLimitFrc},
+        {TEXT("actuatorpos"), EMjSensorType::ActuatorPos},
+        {TEXT("actuatorvel"), EMjSensorType::ActuatorVel},
+        {TEXT("actuatorfrc"), EMjSensorType::ActuatorFrc},
+        {TEXT("jointactuatorfrc"), EMjSensorType::JointActFrc},
+        {TEXT("tendonactuatorfrc"), EMjSensorType::TendonActFrc},
+        {TEXT("framepos"), EMjSensorType::FramePos},
+        {TEXT("framequat"), EMjSensorType::FrameQuat},
+        {TEXT("framexaxis"), EMjSensorType::FrameXAxis},
+        {TEXT("frameyaxis"), EMjSensorType::FrameYAxis},
+        {TEXT("framezaxis"), EMjSensorType::FrameZAxis},
+        {TEXT("framelinvel"), EMjSensorType::FrameLinVel},
+        {TEXT("frameangvel"), EMjSensorType::FrameAngVel},
+        {TEXT("framelinacc"), EMjSensorType::FrameLinAcc},
+        {TEXT("frameangacc"), EMjSensorType::FrameAngAcc},
+        {TEXT("subtreecom"), EMjSensorType::SubtreeCom},
+        {TEXT("subtreelinvel"), EMjSensorType::SubtreeLinVel},
+        {TEXT("subtreeangmom"), EMjSensorType::SubtreeAngMom},
+        {TEXT("insidesite"), EMjSensorType::InsideSite},
+        {TEXT("distance"), EMjSensorType::GeomDist},
+        {TEXT("normal"), EMjSensorType::GeomNormal},
+        {TEXT("fromto"), EMjSensorType::GeomFromTo},
+        {TEXT("contact"), EMjSensorType::Contact},
+        {TEXT("e_potential"), EMjSensorType::EPotential},
+        {TEXT("e_kinetic"), EMjSensorType::EKinetic},
+        {TEXT("clock"), EMjSensorType::Clock},
+        {TEXT("tactile"), EMjSensorType::Tactile},
+        {TEXT("user"), EMjSensorType::User},
+        {TEXT("plugin"), EMjSensorType::Plugin},
+        // --- CODEGEN_SENSOR_TAG_TO_TYPE_END ---
     };
+    const FString Tag = Node->GetTag().ToLower();
+    const EMjSensorType* Found = TagToType.Find(Tag);
+    if (Found) Type = *Found;
 
-    FString ObjTypeStr = Node->GetAttribute(TEXT("objtype")).ToLower();
-    if (!ObjTypeStr.IsEmpty())
-    {
-        const EMjObjType* FoundObjType = ObjTypeStrMap.Find(ObjTypeStr);
-        if (FoundObjType) ObjType = *FoundObjType;
-    }
 
-    FString RefTypeStr = Node->GetAttribute(TEXT("reftype")).ToLower();
-    if (!RefTypeStr.IsEmpty())
-    {
-        const EMjObjType* FoundRefType = ObjTypeStrMap.Find(RefTypeStr);
-        if (FoundRefType) RefType = *FoundRefType;
-    }
+    // TargetName / ReferenceName (target_collations), ObjType / RefType
+    // (xml_enum_attrs), MjClassName (common_imports) are all codegen-emitted
+    // inside the CODEGEN_IMPORT block above. dim / adr remain hand-written
+    // here because they need bespoke handling: dim is conditionally written
+    // (only if XML provides it, preserving the compile-time default), and
+    // adr is an out-of-band override for user sensors only.
 
     // dim override — used by user sensors where the dimension is not fixed by type
     FString DimStr = Node->GetAttribute(TEXT("dim"));
@@ -353,23 +351,12 @@ void UMjSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& C
         bool bAdrOverride = false;
         MjXmlUtils::ReadAttrInt(Node, TEXT("adr"), UserAdr, bAdrOverride);
     }
-
-    MjXmlUtils::ReadAttrString(Node, TEXT("class"), MjClassName);
 }
 
 void UMjSensor::Bind(mjModel* Model, mjData* Data, const FString& Prefix)
 {
     Super::Bind(Model, Data, Prefix);
-    m_SensorView = BindToView<SensorView>(Prefix);
-    
-    if (m_SensorView.id != -1)
-    {
-        m_ID = m_SensorView.id;
-    }
-    else
-    {
-        UE_LOG(LogURLabBind, Warning, TEXT("[MjSensor] Sensor '%s' could not bind! Prefix: %s"), *GetName(), *Prefix);
-    }
+    BindAndCacheView(m_SensorView, Prefix);
 }
 
 // Apply the MuJoCo → UE coordinate transform appropriate for each sensor type.
@@ -452,10 +439,10 @@ static void TransformSensorReading(TArray<float>& R, EMjSensorType Type)
 TArray<float> UMjSensor::GetReading() const
 {
     TArray<float> Result;
-    if (m_SensorView.id != -1 && m_SensorView.data)
+    if (m_SensorView.id != -1 && m_SensorView.sensordata)
     {
-        for (int i = 0; i < m_SensorView.dim; ++i)
-            Result.Add((float)m_SensorView.data[i]);
+        for (int i = 0; i < m_SensorView.sensor_dim; ++i)
+            Result.Add((float)m_SensorView.sensordata[i]);
         TransformSensorReading(Result, Type);
     }
     return Result;
@@ -463,9 +450,9 @@ TArray<float> UMjSensor::GetReading() const
 
 float UMjSensor::GetScalarReading() const
 {
-    if (m_SensorView.id != -1 && m_SensorView.data && m_SensorView.dim > 0)
+    if (m_SensorView.id != -1 && m_SensorView.sensordata && m_SensorView.sensor_dim > 0)
     {
-        return (float)m_SensorView.data[0];
+        return (float)m_SensorView.sensordata[0];
     }
     return 0.0f;
 }
@@ -473,7 +460,7 @@ float UMjSensor::GetScalarReading() const
 int UMjSensor::GetDimension() const
 {
     if (m_SensorView.id != -1)
-        return m_SensorView.dim;
+        return m_SensorView.sensor_dim;
     return 0;
 }
 
@@ -502,11 +489,11 @@ void UMjSensor::BuildBinaryPayload(FBufferArchive& OutBuffer) const
     int32 NumElements = GetDimension();
     OutBuffer << NumElements;
 
-    if (m_SensorView.id != -1 && m_SensorView.data && NumElements > 0)
+    if (m_SensorView.id != -1 && m_SensorView.sensordata && NumElements > 0)
     {
         for (int i = 0; i < NumElements; ++i)
         {
-            float Val = (float)m_SensorView.data[i];
+            float Val = (float)m_SensorView.sensordata[i];
             OutBuffer << Val;
         }
     }
@@ -547,8 +534,7 @@ TArray<FString> UMjSensor::GetReferenceNameOptions() const
     return UMjComponent::GetSiblingComponentOptions(this, GetClassForObjType(RefType));
 }
 
-TArray<FString> UMjSensor::GetDefaultClassOptions() const
-{
-    return UMjComponent::GetSiblingComponentOptions(this, UMjDefault::StaticClass(), true);
-}
+// --- CODEGEN_EDITOR_OPTIONS_START ---
+TArray<FString> UMjSensor::GetDefaultClassOptions() const { return UMjComponent::GetSiblingComponentOptions(this, UMjDefault::StaticClass(), true); }
+// --- CODEGEN_EDITOR_OPTIONS_END ---
 #endif
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjSubtreeAngMomSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjSubtreeAngMomSensor.cpp
index 4f24533..9a43d3c 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjSubtreeAngMomSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjSubtreeAngMomSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjSubtreeAngMomSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjSubtreeAngMomSensor::UMjSubtreeAngMomSensor()
 {
     Type = EMjSensorType::SubtreeAngMom;
 }
 
-void UMjSubtreeAngMomSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjSubtreeAngMomSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjSubtreeAngMomSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjSubtreeComSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjSubtreeComSensor.cpp
index 5686734..de7e384 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjSubtreeComSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjSubtreeComSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjSubtreeComSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjSubtreeComSensor::UMjSubtreeComSensor()
 {
     Type = EMjSensorType::SubtreeCom;
 }
 
-void UMjSubtreeComSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjSubtreeComSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjSubtreeComSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjSubtreeLinVelSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjSubtreeLinVelSensor.cpp
index 3368496..fdef06c 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjSubtreeLinVelSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjSubtreeLinVelSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjSubtreeLinVelSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjSubtreeLinVelSensor::UMjSubtreeLinVelSensor()
 {
     Type = EMjSensorType::SubtreeLinVel;
 }
 
-void UMjSubtreeLinVelSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjSubtreeLinVelSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjSubtreeLinVelSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjTactileSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjTactileSensor.cpp
index f7249b1..3a95606 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjTactileSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjTactileSensor.cpp
@@ -44,7 +44,6 @@ void UMjTactileSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
     Super::ExportTo(Element, Default);
 
     // --- CODEGEN_EXPORT_START ---
-
     // --- CODEGEN_EXPORT_END ---
 }
 
@@ -54,7 +53,6 @@ void UMjTactileSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSett
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
     // --- CODEGEN_IMPORT_END ---
 }
 
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjTendonActFrcSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjTendonActFrcSensor.cpp
index 35d1e69..7a0929f 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjTendonActFrcSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjTendonActFrcSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjTendonActFrcSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjTendonActFrcSensor::UMjTendonActFrcSensor()
 {
     Type = EMjSensorType::TendonActFrc;
 }
 
-void UMjTendonActFrcSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjTendonActFrcSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjTendonActFrcSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjTendonLimitFrcSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjTendonLimitFrcSensor.cpp
index 6b070c1..c0863bc 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjTendonLimitFrcSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjTendonLimitFrcSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjTendonLimitFrcSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjTendonLimitFrcSensor::UMjTendonLimitFrcSensor()
 {
     Type = EMjSensorType::TendonLimitFrc;
 }
 
-void UMjTendonLimitFrcSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjTendonLimitFrcSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjTendonLimitFrcSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjTendonLimitPosSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjTendonLimitPosSensor.cpp
index 9561b40..f5dfa90 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjTendonLimitPosSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjTendonLimitPosSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjTendonLimitPosSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjTendonLimitPosSensor::UMjTendonLimitPosSensor()
 {
     Type = EMjSensorType::TendonLimitPos;
 }
 
-void UMjTendonLimitPosSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjTendonLimitPosSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjTendonLimitPosSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjTendonLimitVelSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjTendonLimitVelSensor.cpp
index fc06ebc..1b0574f 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjTendonLimitVelSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjTendonLimitVelSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjTendonLimitVelSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjTendonLimitVelSensor::UMjTendonLimitVelSensor()
 {
     Type = EMjSensorType::TendonLimitVel;
 }
 
-void UMjTendonLimitVelSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjTendonLimitVelSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjTendonLimitVelSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjTendonPosSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjTendonPosSensor.cpp
index c7898f8..0ed5e05 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjTendonPosSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjTendonPosSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjTendonPosSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjTendonPosSensor::UMjTendonPosSensor()
 {
     Type = EMjSensorType::TendonPos;
 }
 
-void UMjTendonPosSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjTendonPosSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjTendonPosSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjTendonVelSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjTendonVelSensor.cpp
index c3731b8..5cc0fc5 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjTendonVelSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjTendonVelSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjTendonVelSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjTendonVelSensor::UMjTendonVelSensor()
 {
     Type = EMjSensorType::TendonVel;
 }
 
-void UMjTendonVelSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjTendonVelSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjTendonVelSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjTorqueSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjTorqueSensor.cpp
index 1687d15..6153425 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjTorqueSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjTorqueSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjTorqueSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjTorqueSensor::UMjTorqueSensor()
 {
     Type = EMjSensorType::Torque;
 }
 
-void UMjTorqueSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjTorqueSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjTorqueSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjTouchSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjTouchSensor.cpp
index dea2e54..be4fc56 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjTouchSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjTouchSensor.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjTouchSensor.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjTouchSensor::UMjTouchSensor()
 {
     Type = EMjSensorType::Touch;
 }
 
-void UMjTouchSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjTouchSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjTouchSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjUserSensor.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjUserSensor.cpp
index e9b860b..1d2f578 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjUserSensor.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjUserSensor.cpp
@@ -41,7 +41,7 @@ void UMjUserSensor::ImportFromXml(const FXmlNode* Node, const FMjCompilerSetting
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
@@ -52,6 +52,6 @@ void UMjUserSensor::ExportTo(mjsSensor* Element, mjsDefault* Default)
     Super::ExportTo(Element, Default);
 
     // --- CODEGEN_EXPORT_START ---
-
+    // (no type-specific schema exports)
     // --- CODEGEN_EXPORT_END ---
 }
diff --git a/Source/URLab/Private/MuJoCo/Components/Sensors/MjVelocimeter.cpp b/Source/URLab/Private/MuJoCo/Components/Sensors/MjVelocimeter.cpp
index 7c7c1a4..ba10c86 100644
--- a/Source/URLab/Private/MuJoCo/Components/Sensors/MjVelocimeter.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Sensors/MjVelocimeter.cpp
@@ -13,39 +13,45 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #include "MuJoCo/Components/Sensors/MjVelocimeter.h"
+#include "MuJoCo/Utils/MjXmlUtils.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
+#include "MuJoCo/Utils/MjUtils.h"
+#include "XmlNode.h"
+#include "mujoco/mjspec.h"
 
 UMjVelocimeter::UMjVelocimeter()
 {
     Type = EMjSensorType::Velocimeter;
 }
 
-void UMjVelocimeter::ExportTo(mjsSensor* Element, mjsDefault* Default)
-{
-    if (!Element) return;
-
-    Super::ExportTo(Element, Default);
-
-    // --- CODEGEN_EXPORT_START ---
-
-    // --- CODEGEN_EXPORT_END ---
-}
-
 void UMjVelocimeter::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& CompilerSettings)
 {
     Super::ImportFromXml(Node, CompilerSettings);
     if (!Node) return;
 
     // --- CODEGEN_IMPORT_START ---
-
+    // (no type-specific schema imports)
     // --- CODEGEN_IMPORT_END ---
 }
 
+void UMjVelocimeter::ExportTo(mjsSensor* Element, mjsDefault* Default)
+{
+    if (!Element) return;
+
+    Super::ExportTo(Element, Default);
+
+    // --- CODEGEN_EXPORT_START ---
+    // (no type-specific schema exports)
+    // --- CODEGEN_EXPORT_END ---
+}
diff --git a/Source/URLab/Private/MuJoCo/Components/Tendons/MjTendon.cpp b/Source/URLab/Private/MuJoCo/Components/Tendons/MjTendon.cpp
index 361f907..3f2f8c2 100644
--- a/Source/URLab/Private/MuJoCo/Components/Tendons/MjTendon.cpp
+++ b/Source/URLab/Private/MuJoCo/Components/Tendons/MjTendon.cpp
@@ -46,11 +46,12 @@ void UMjTendon::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& C
     if (!Node) return;
 
         // --- CODEGEN_IMPORT_START ---
+    MjXmlUtils::ReadAttrString(Node, TEXT("class"), MjClassName);
     MjXmlUtils::ReadAttrInt(Node, TEXT("group"), group, bOverride_group);
     MjXmlUtils::ReadAttrBool(Node, TEXT("limited"), limited, bOverride_limited);
-    MjXmlUtils::ReadAttrBool(Node, TEXT("actuatorfrclimited"), actuatorfrclimited, bOverride_actuatorfrclimited);
+    MjXmlUtils::ReadAttrBool(Node, TEXT("actuatorfrclimited"), ActFrcLimited, bOverride_ActFrcLimited);
     MjXmlUtils::ReadAttrFloatArray(Node, TEXT("range"), range, bOverride_range);
-    MjXmlUtils::ReadAttrFloatArray(Node, TEXT("actuatorfrcrange"), actuatorfrcrange, bOverride_actuatorfrcrange);
+    MjXmlUtils::ReadAttrFloatArray(Node, TEXT("actuatorfrcrange"), ActFrcRange, bOverride_ActFrcRange);
     MjXmlUtils::ReadAttrFloatArray(Node, TEXT("solreflimit"), solreflimit, bOverride_solreflimit);
     MjXmlUtils::ReadAttrFloatArray(Node, TEXT("solimplimit"), solimplimit, bOverride_solimplimit);
     MjXmlUtils::ReadAttrFloatArray(Node, TEXT("solreffriction"), solreffriction, bOverride_solreffriction);
@@ -64,16 +65,7 @@ void UMjTendon::ImportFromXml(const FXmlNode* Node, const FMjCompilerSettings& C
     MjXmlUtils::ReadAttrFloatArray(Node, TEXT("damping"), damping, bOverride_damping);
     MjXmlUtils::ReadAttrFloat(Node, TEXT("armature"), armature, bOverride_armature);
     MjXmlUtils::ReadAttrColor(Node, TEXT("rgba"), rgba, bOverride_rgba);
-    // --- CODEGEN_IMPORT_END ---
-
-    // Class name
-    MjXmlUtils::ReadAttrString(Node, TEXT("class"), MjClassName);
-
-    // --- Physics ---
-
-    // --- Limits ---
-    MjXmlUtils::ReadAttrBool(Node, TEXT("actfrclimited"), bActFrcLimited, bOverride_ActFrcLimited);
-    MjXmlUtils::ReadAttrFloatArray(Node, TEXT("actfrcrange"), ActFrcRange, bOverride_ActFrcRange);
+        // --- CODEGEN_IMPORT_END ---
 
     // --- Solver ---
 
@@ -131,35 +123,22 @@ void UMjTendon::ExportTo(mjsTendon* Element, mjsDefault* def)
 
     // --- Physics properties ---
 
-    // --- Limits ---
-
-    if (bOverride_ActFrcLimited) Element->actfrclimited = bActFrcLimited ? mjLIMITED_TRUE : mjLIMITED_FALSE;
-    if (bOverride_ActFrcRange)
-    {
-        for (int i = 0; i < ActFrcRange.Num() && i < 2; ++i)
-            Element->actfrcrange[i] = ActFrcRange[i];
-    }
-
-    // --- Solver ---
-
-    // --- Visuals ---
-
-        // --- CODEGEN_EXPORT_START ---
+    // --- CODEGEN_EXPORT_START ---
     if (bOverride_group) Element->group = group;
     if (bOverride_limited) Element->limited = limited ? 1 : 0;
-    if (bOverride_actuatorfrclimited) Element->actfrclimited = actuatorfrclimited ? 1 : 0;
-    if (bOverride_range) { for (int32 i = 0; i < range.Num(); ++i) Element->range[i] = range[i]; }
-    if (bOverride_actuatorfrcrange) { for (int32 i = 0; i < actuatorfrcrange.Num(); ++i) Element->actfrcrange[i] = actuatorfrcrange[i]; }
-    if (bOverride_solreflimit) { for (int32 i = 0; i < solreflimit.Num(); ++i) Element->solref_limit[i] = solreflimit[i]; }
-    if (bOverride_solimplimit) { for (int32 i = 0; i < solimplimit.Num(); ++i) Element->solimp_limit[i] = solimplimit[i]; }
-    if (bOverride_solreffriction) { for (int32 i = 0; i < solreffriction.Num(); ++i) Element->solref_friction[i] = solreffriction[i]; }
-    if (bOverride_solimpfriction) { for (int32 i = 0; i < solimpfriction.Num(); ++i) Element->solimp_friction[i] = solimpfriction[i]; }
+    if (bOverride_ActFrcLimited) Element->actfrclimited = ActFrcLimited ? 1 : 0;
+    if (bOverride_range) { for (int32 i = 0; i < FMath::Min(range.Num(), 2); ++i) Element->range[i] = range[i]; }
+    if (bOverride_ActFrcRange) { for (int32 i = 0; i < FMath::Min(ActFrcRange.Num(), 2); ++i) Element->actfrcrange[i] = ActFrcRange[i]; }
+    if (bOverride_solreflimit) { for (int32 i = 0; i < FMath::Min(solreflimit.Num(), 2); ++i) Element->solref_limit[i] = solreflimit[i]; }
+    if (bOverride_solimplimit) { for (int32 i = 0; i < FMath::Min(solimplimit.Num(), 5); ++i) Element->solimp_limit[i] = solimplimit[i]; }
+    if (bOverride_solreffriction) { for (int32 i = 0; i < FMath::Min(solreffriction.Num(), 2); ++i) Element->solref_friction[i] = solreffriction[i]; }
+    if (bOverride_solimpfriction) { for (int32 i = 0; i < FMath::Min(solimpfriction.Num(), 5); ++i) Element->solimp_friction[i] = solimpfriction[i]; }
     if (bOverride_frictionloss) Element->frictionloss = frictionloss;
-    if (bOverride_springlength) { for (int32 i = 0; i < springlength.Num(); ++i) Element->springlength[i] = springlength[i]; }
+    if (bOverride_springlength) { for (int32 i = 0; i < FMath::Min(springlength.Num(), 2); ++i) Element->springlength[i] = springlength[i]; }
     if (bOverride_width) Element->width = width;
     if (bOverride_margin) Element->margin = margin;
-    if (bOverride_stiffness) { for (int32 i = 0; i < stiffness.Num(); ++i) Element->stiffness[i] = stiffness[i]; }
-    if (bOverride_damping) { for (int32 i = 0; i < damping.Num(); ++i) Element->damping[i] = damping[i]; }
+    if (bOverride_stiffness) { for (int32 i = 0; i < FMath::Min(stiffness.Num(), 3); ++i) Element->stiffness[i] = stiffness[i]; }
+    if (bOverride_damping) { for (int32 i = 0; i < FMath::Min(damping.Num(), 3); ++i) Element->damping[i] = damping[i]; }
     if (bOverride_armature) Element->armature = armature;
     if (bOverride_rgba) { Element->rgba[0] = rgba.R; Element->rgba[1] = rgba.G; Element->rgba[2] = rgba.B; Element->rgba[3] = rgba.A; }
     // --- CODEGEN_EXPORT_END ---
@@ -248,16 +227,7 @@ void UMjTendon::RegisterToSpec(FMujocoSpecWrapper& Wrapper, mjsBody* ParentBody)
 void UMjTendon::Bind(mjModel* model, mjData* data, const FString& Prefix)
 {
     Super::Bind(model, data, Prefix);
-    m_TendonView = BindToView<TendonView>(Prefix);
-    if (m_TendonView.id != -1)
-    {
-        m_ID = m_TendonView.id;
-    }
-    else
-    {
-        UE_LOG(LogURLabImport, Warning, TEXT("[MjTendon] Failed to bind tendon '%s' (prefix='%s')"),
-            *GetName(), *Prefix);
-    }
+    BindAndCacheView(m_TendonView, Prefix);
 }
 
 float UMjTendon::GetLength() const
@@ -277,10 +247,9 @@ FString UMjTendon::GetMjName() const
 }
 
 #if WITH_EDITOR
-TArray<FString> UMjTendon::GetDefaultClassOptions() const
-{
-    return GetSiblingComponentOptions(this, UMjDefault::StaticClass(), true);
-}
+// --- CODEGEN_EDITOR_OPTIONS_START ---
+TArray<FString> UMjTendon::GetDefaultClassOptions() const { return UMjComponent::GetSiblingComponentOptions(this, UMjDefault::StaticClass(), true); }
+// --- CODEGEN_EDITOR_OPTIONS_END ---
 #endif
 
 // --- Multi-UCLASS subclass constructors --------------------------------------
diff --git a/Source/URLab/Private/MuJoCo/Core/MjArticulation.cpp b/Source/URLab/Private/MuJoCo/Core/MjArticulation.cpp
index ec7f8d9..235fd85 100644
--- a/Source/URLab/Private/MuJoCo/Core/MjArticulation.cpp
+++ b/Source/URLab/Private/MuJoCo/Core/MjArticulation.cpp
@@ -238,11 +238,30 @@ void AMjArticulation::UnPossessed()
 }
 
 
+// Collapses 7 identical "GetComponents + for-loop + RegisterToSpec"
+// category blocks into one call. Flex stays separate — it needs
+// ParentSpecBody resolution.
+template <typename T>
+static void RegisterAllOf(AActor* Owner, FMujocoSpecWrapper& Wrapper,
+                          bool bSkipDefaults = true)
+{
+    if (!Owner) return;
+    TArray<T*> All;
+    Owner->GetComponents<T>(All);
+    for (T* C : All)
+    {
+        if (!C) continue;
+        if (bSkipDefaults && C->bIsDefault) continue;
+        C->RegisterToSpec(Wrapper);
+    }
+}
+
+
 void AMjArticulation::Setup(mjSpec* Spec, mjVFS* VFS)
 {
     m_spec = Spec;
     m_vfs = VFS;
-    
+
     m_ChildSpec = mj_makeSpec();
     m_ChildSpec->compiler.degree = false;
 
@@ -410,60 +429,30 @@ void AMjArticulation::Setup(mjSpec* Spec, mjVFS* VFS)
     }
 
     // 4. Add Tendons (into child spec, after bodies so joint names are set)
-    TArray<UMjTendon*> Tendons;
-    GetComponents<UMjTendon>(Tendons);
-    for (UMjTendon* Tendon : Tendons)
-    {
-        if (Tendon && !Tendon->bIsDefault)
-            Tendon->RegisterToSpec(*m_wrapper);
-    }
+    RegisterAllOf<UMjTendon>(this, *m_wrapper);
 
-    // 5. Add Sensors and Actuators (into child spec, after tendons strings might be referenced)
-    TArray<UMjSensor*> Sensors;
-    GetComponents<UMjSensor>(Sensors);
-    for (UMjSensor* Sensor : Sensors)
-    {
-        if (Sensor && !Sensor->bIsDefault)
-            Sensor->RegisterToSpec(*m_wrapper);
-    }
+    // 5. Add Sensors / Actuators (after tendons so tendon names resolve).
+    RegisterAllOf<UMjSensor>(this, *m_wrapper);
+    RegisterAllOf<UMjActuator>(this, *m_wrapper);
 
-    TArray<UMjActuator*> Actuators;
-    GetComponents<UMjActuator>(Actuators);
-    for (UMjActuator* Actuator : Actuators)
+    // 6. Contact pairs / excludes inherit from IMjSpecElement (not
+    //    UMjComponent) so they have no bIsDefault field — register all.
     {
-        if (Actuator && !Actuator->bIsDefault)
-            Actuator->RegisterToSpec(*m_wrapper);
+        TArray<UMjContactPair*> Pairs;
+        GetComponents<UMjContactPair>(Pairs);
+        for (UMjContactPair* Pair : Pairs)
+            if (Pair) Pair->RegisterToSpec(*m_wrapper);
     }
-
-    TArray<UMjContactPair*> ContactPairs;
-    GetComponents<UMjContactPair>(ContactPairs);
-    for (UMjContactPair* Pair : ContactPairs)
     {
-        Pair->RegisterToSpec(*m_wrapper);
+        TArray<UMjContactExclude*> Excludes;
+        GetComponents<UMjContactExclude>(Excludes);
+        for (UMjContactExclude* Excl : Excludes)
+            if (Excl) Excl->RegisterToSpec(*m_wrapper);
     }
 
-    TArray<UMjContactExclude*> ContactExcludes;
-    GetComponents<UMjContactExclude>(ContactExcludes);
-    for (UMjContactExclude* Exclude : ContactExcludes)
-    {
-        Exclude->RegisterToSpec(*m_wrapper);
-    }
-
-    TArray<UMjEquality*> Equalities;
-    GetComponents<UMjEquality>(Equalities);
-    for (UMjEquality* Equality : Equalities)
-    {
-        if (Equality && !Equality->bIsDefault)
-            Equality->RegisterToSpec(*m_wrapper);
-    }
-
-    TArray<UMjKeyframe*> Keyframes;
-    GetComponents<UMjKeyframe>(Keyframes);
-    for (UMjKeyframe* Keyframe : Keyframes)
-    {
-        if (Keyframe && !Keyframe->bIsDefault)
-            Keyframe->RegisterToSpec(*m_wrapper);
-    }
+    // 7. Equalities + Keyframes.
+    RegisterAllOf<UMjEquality>(this, *m_wrapper);
+    RegisterAllOf<UMjKeyframe>(this, *m_wrapper);
     
     mjsBody* parentWorld = mjs_findBody(Spec, "world");
     mjsFrame* attachmentFrame = mjs_addFrame(parentWorld, 0);
@@ -695,73 +684,16 @@ void AMjArticulation::ApplyControls()
 // Blueprint Runtime API — Discovery
 // =========================================================================
 
-TArray<FString> AMjArticulation::GetActuatorNames() const
-{
-    TArray<FString> Names;
-    for (auto& Elem : ActuatorIdMap)
-    {
-        if (Elem.Value) Names.Add(Elem.Value->GetMjName());
-    }
-    return Names;
-}
-
-TArray<UMjActuator*> AMjArticulation::GetActuators() const
-{
-    TArray<UMjActuator*> Components;
-    ActuatorIdMap.GenerateValueArray(Components);
-    return Components;
-}
-
-TArray<FString> AMjArticulation::GetJointNames() const
-{
-    TArray<FString> Names;
-    for (auto& Elem : JointIdMap)
-    {
-        if (Elem.Value) Names.Add(Elem.Value->GetMjName());
-    }
-    return Names;
-}
-
-TArray<UMjJoint*> AMjArticulation::GetJoints() const
-{
-    TArray<UMjJoint*> Components;
-    JointIdMap.GenerateValueArray(Components);
-    return Components;
-}
-
-TArray<FString> AMjArticulation::GetSensorNames() const
-{
-    TArray<FString> Names;
-    for (auto& Elem : SensorIdMap)
-    {
-        if (Elem.Value) Names.Add(Elem.Value->GetMjName());
-    }
-    return Names;
-}
-
-TArray<UMjSensor*> AMjArticulation::GetSensors() const
-{
-    TArray<UMjSensor*> Components;
-    SensorIdMap.GenerateValueArray(Components);
-    return Components;
-}
+// --- Templated map-lookup wrappers (v8: implementations in MjArticulation.h templates) ---
 
-TArray<FString> AMjArticulation::GetBodyNames() const
-{
-    TArray<FString> Names;
-    for (auto& Elem : BodyIdMap)
-    {
-        if (Elem.Value) Names.Add(Elem.Value->GetMjName());
-    }
-    return Names;
-}
-
-TArray<UMjBody*> AMjArticulation::GetBodies() const
-{
-    TArray<UMjBody*> Components;
-    BodyIdMap.GenerateValueArray(Components);
-    return Components;
-}
+TArray<FString>      AMjArticulation::GetActuatorNames() const { return GetComponentNamesFromMap(ActuatorIdMap); }
+TArray<UMjActuator*> AMjArticulation::GetActuators()    const { return GetComponentsFromMap(ActuatorIdMap); }
+TArray<FString>      AMjArticulation::GetJointNames()   const { return GetComponentNamesFromMap(JointIdMap); }
+TArray<UMjJoint*>    AMjArticulation::GetJoints()       const { return GetComponentsFromMap(JointIdMap); }
+TArray<FString>      AMjArticulation::GetSensorNames()  const { return GetComponentNamesFromMap(SensorIdMap); }
+TArray<UMjSensor*>   AMjArticulation::GetSensors()      const { return GetComponentsFromMap(SensorIdMap); }
+TArray<FString>      AMjArticulation::GetBodyNames()    const { return GetComponentNamesFromMap(BodyIdMap); }
+TArray<UMjBody*>     AMjArticulation::GetBodies()       const { return GetComponentsFromMap(BodyIdMap); }
 
 void AMjArticulation::WakeAll()
 {
@@ -786,60 +718,15 @@ TArray<UMjFrame*> AMjArticulation::GetFrames() const
     return Components;
 }
 
-TArray<UMjGeom*> AMjArticulation::GetGeoms() const
-{
-    TArray<UMjGeom*> Components;
-    GeomIdMap.GenerateValueArray(Components);
-    return Components;
-}
-
+TArray<UMjGeom*>     AMjArticulation::GetGeoms()        const { return GetComponentsFromMap(GeomIdMap); }
+TArray<UMjTendon*>   AMjArticulation::GetTendons()      const { return GetComponentsFromMap(TendonIdMap); }
+TArray<FString>      AMjArticulation::GetTendonNames()  const { return GetComponentNamesFromMap(TendonIdMap); }
 
-UMjActuator* AMjArticulation::GetActuator(const FString& Name) const
-{
-    if (const auto* Ptr = ActuatorComponentMap.Find(Name)) return *Ptr;
-    return nullptr;
-}
-
-UMjJoint* AMjArticulation::GetJoint(const FString& Name) const
-{
-    if (const auto* Ptr = JointComponentMap.Find(Name)) return *Ptr;
-    return nullptr;
-}
-
-UMjSensor* AMjArticulation::GetSensor(const FString& Name) const
-{
-    if (const auto* Ptr = SensorComponentMap.Find(Name)) return *Ptr;
-    return nullptr;
-}
-
-UMjBody* AMjArticulation::GetBody(const FString& Name) const
-{
-    if (const auto* Ptr = BodyComponentMap.Find(Name)) return *Ptr;
-    return nullptr;
-}
-
-TArray<UMjTendon*> AMjArticulation::GetTendons() const
-{
-    TArray<UMjTendon*> Components;
-    TendonIdMap.GenerateValueArray(Components);
-    return Components;
-}
-
-TArray<FString> AMjArticulation::GetTendonNames() const
-{
-    TArray<FString> Names;
-    for (auto& Elem : TendonIdMap)
-    {
-        if (Elem.Value) Names.Add(Elem.Value->GetMjName());
-    }
-    return Names;
-}
-
-UMjTendon* AMjArticulation::GetTendon(const FString& Name) const
-{
-    if (const auto* Ptr = TendonComponentMap.Find(Name)) return *Ptr;
-    return nullptr;
-}
+UMjActuator* AMjArticulation::GetActuator(const FString& Name) const { return GetComponentByNameInMap(Name, ActuatorComponentMap); }
+UMjJoint*    AMjArticulation::GetJoint   (const FString& Name) const { return GetComponentByNameInMap(Name, JointComponentMap); }
+UMjSensor*   AMjArticulation::GetSensor  (const FString& Name) const { return GetComponentByNameInMap(Name, SensorComponentMap); }
+UMjBody*     AMjArticulation::GetBody    (const FString& Name) const { return GetComponentByNameInMap(Name, BodyComponentMap); }
+UMjTendon*   AMjArticulation::GetTendon  (const FString& Name) const { return GetComponentByNameInMap(Name, TendonComponentMap); }
 
 TArray<UMjEquality*> AMjArticulation::GetEqualities() const
 {
@@ -1013,28 +900,22 @@ void AMjArticulation::StopHoldKeyframe()
 
 UMjComponent* AMjArticulation::GetComponentByMjId(mjtObj type, int32 id) const
 {
+    // Single-lookup dispatch via GetComponentByIdInMap<T> (v8). Each branch
+    // is a single Map.Find + nullptr-return, matching the standalone Get*ByMjId
+    // helpers below.
     switch (type)
     {
-        case mjOBJ_BODY:     return GetBodyByMjId(id);
-        case mjOBJ_GEOM:     return GetGeomByMjId(id);
-        case mjOBJ_JOINT:    return JointIdMap.Contains(id) ? JointIdMap[id] : nullptr;
-        case mjOBJ_SENSOR:   return SensorIdMap.Contains(id) ? SensorIdMap[id] : nullptr;
-        case mjOBJ_ACTUATOR: return ActuatorIdMap.Contains(id) ? ActuatorIdMap[id] : nullptr;
+        case mjOBJ_BODY:     return GetComponentByIdInMap(id, BodyIdMap);
+        case mjOBJ_GEOM:     return GetComponentByIdInMap(id, GeomIdMap);
+        case mjOBJ_JOINT:    return GetComponentByIdInMap(id, JointIdMap);
+        case mjOBJ_SENSOR:   return GetComponentByIdInMap(id, SensorIdMap);
+        case mjOBJ_ACTUATOR: return GetComponentByIdInMap(id, ActuatorIdMap);
         default:             return nullptr;
     }
 }
 
-UMjBody* AMjArticulation::GetBodyByMjId(int32 id) const
-{
-    if (const auto* Ptr = BodyIdMap.Find(id)) return *Ptr;
-    return nullptr;
-}
-
-UMjGeom* AMjArticulation::GetGeomByMjId(int32 id) const
-{
-    if (const auto* Ptr = GeomIdMap.Find(id)) return *Ptr;
-    return nullptr;
-}
+UMjBody* AMjArticulation::GetBodyByMjId(int32 id) const { return GetComponentByIdInMap(id, BodyIdMap); }
+UMjGeom* AMjArticulation::GetGeomByMjId(int32 id) const { return GetComponentByIdInMap(id, GeomIdMap); }
 
 bool AMjArticulation::SetActuatorControl(const FString& ActuatorName, float Value)
 {
@@ -1140,14 +1021,14 @@ void AMjArticulation::DrawDebugJoints()
         {
             // Runtime mode: use compiled model data
             const JointView& JV = Joint->GetMj();
-            MjType = JV.type;
+            MjType = JV.jnt_type;
             Anchor = Joint->GetWorldAnchor();
             Axis = Joint->GetWorldAxis();
 
-            if (JV.range)
+            if (JV.jnt_range)
             {
-                RangeMin = (float)JV.range[0];
-                RangeMax = (float)JV.range[1];
+                RangeMin = (float)JV.jnt_range[0];
+                RangeMax = (float)JV.jnt_range[1];
                 bLimited = (RangeMin != 0.0f || RangeMax != 0.0f);
             }
             else
@@ -1229,13 +1110,13 @@ void AMjArticulation::DrawDebugSites()
         {
             // Runtime: use compiled model data
             const SiteView& SV = Site->GetMj();
-            if (!SV.xpos) continue;
+            if (!SV.site_xpos) continue;
 
-            Pos = MjUtils::MjToUEPosition(SV.xpos);
-            Radius = (SV.size) ? (float)SV.size[0] * 100.0f : 1.0f; // meters → cm
-            Color = (SV.rgba) ? FColor(
-                (uint8)(SV.rgba[0] * 255), (uint8)(SV.rgba[1] * 255),
-                (uint8)(SV.rgba[2] * 255), 200) : FColor(128, 128, 128, 200);
+            Pos = MjUtils::MjToUEPosition(SV.site_xpos);
+            Radius = (SV.site_size) ? (float)SV.site_size[0] * 100.0f : 1.0f; // meters → cm
+            Color = (SV.site_rgba) ? FColor(
+                (uint8)(SV.site_rgba[0] * 255), (uint8)(SV.site_rgba[1] * 255),
+                (uint8)(SV.site_rgba[2] * 255), 200) : FColor(128, 128, 128, 200);
         }
         else
         {
diff --git a/Source/URLab/Private/MuJoCo/Core/MjDebugVisualizer.cpp b/Source/URLab/Private/MuJoCo/Core/MjDebugVisualizer.cpp
index 5c32888..3fc5fef 100644
--- a/Source/URLab/Private/MuJoCo/Core/MjDebugVisualizer.cpp
+++ b/Source/URLab/Private/MuJoCo/Core/MjDebugVisualizer.cpp
@@ -495,7 +495,7 @@ void UMjDebugVisualizer::UpdateBodyOverlays()
         for (UMjGeom* Geom : Geoms)
         {
             if (!Geom || !Geom->IsBound()) continue;
-            const int32 BodyId = Geom->GetMj().body_id;
+            const int32 BodyId = Geom->GetMj().geom_bodyid;
 
             ApplyToMesh(Geom->GetVisualizerMesh(), BodyId, ArtHash);
 
@@ -654,7 +654,7 @@ void UMjDebugVisualizer::BuildSegPool(EMjCameraMode mode)
         for (UMjGeom* Geom : Geoms)
         {
             if (!Geom || !Geom->IsBound()) continue;
-            const int32 BodyId = Geom->GetMj().body_id;
+            const int32 BodyId = Geom->GetMj().geom_bodyid;
 
             AddSibling(Geom->GetVisualizerMesh(), BodyId, ArtHash);
 
diff --git a/Source/URLab/Private/MuJoCo/Core/MjSimOptions.cpp b/Source/URLab/Private/MuJoCo/Core/MjSimOptions.cpp
deleted file mode 100644
index af9a63c..0000000
--- a/Source/URLab/Private/MuJoCo/Core/MjSimOptions.cpp
+++ /dev/null
@@ -1,142 +0,0 @@
-// Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
-//
-// 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.
-//
-// --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
-// trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
-//
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
-// CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
-#include "MuJoCo/Core/MjSimOptions.h"
-#include <mujoco/mujoco.h>
-
-void FMuJoCoOptions::ApplyToSpec(mjSpec* Spec) const
-{
-    if (!Spec) return;
-
-    if (bOverride_MemoryMB)
-    {
-        Spec->memory = static_cast<mjtSize>(MemoryMB) * 1024 * 1024;
-    }
-
-    Spec->option.timestep = Timestep;
-
-    // Gravity: UE cm/s² → MuJoCo m/s², negate Y for handedness
-    Spec->option.gravity[0] =  Gravity.X / 100.0;
-    Spec->option.gravity[1] = -Gravity.Y / 100.0;
-    Spec->option.gravity[2] =  Gravity.Z / 100.0;
-
-    Spec->option.wind[0] =  Wind.X / 100.0;
-    Spec->option.wind[1] = -Wind.Y / 100.0;
-    Spec->option.wind[2] =  Wind.Z / 100.0;
-
-    Spec->option.magnetic[0] =  Magnetic.X;
-    Spec->option.magnetic[1] = -Magnetic.Y;
-    Spec->option.magnetic[2] =  Magnetic.Z;
-
-    Spec->option.density = Density;
-    Spec->option.viscosity = Viscosity;
-    Spec->option.impratio = Impratio;
-    Spec->option.tolerance = Tolerance;
-    Spec->option.iterations = Iterations;
-    Spec->option.ls_iterations = LsIterations;
-
-    if (bOverride_Integrator) Spec->option.integrator = (int)Integrator;
-    if (bOverride_Cone)       Spec->option.cone = (int)Cone;
-    if (bOverride_Solver)     Spec->option.solver = (int)Solver;
-
-    if (bOverride_NoslipIterations) Spec->option.noslip_iterations = NoslipIterations;
-    if (bOverride_NoslipTolerance)  Spec->option.noslip_tolerance = NoslipTolerance;
-    if (bOverride_CCD_Iterations)   Spec->option.ccd_iterations = CCD_Iterations;
-    if (bOverride_CCD_Tolerance)    Spec->option.ccd_tolerance = CCD_Tolerance;
-
-    // MultiCCD
-    constexpr int MJ_ENBL_MULTICCD = 1 << 4;
-    if (bEnableMultiCCD)
-        Spec->option.enableflags |= MJ_ENBL_MULTICCD;
-    else
-        Spec->option.enableflags &= ~MJ_ENBL_MULTICCD;
-
-    // Sleep
-    constexpr int MJ_ENBL_SLEEP = 1 << 5;
-    if (bEnableSleep)
-    {
-        Spec->option.enableflags |= MJ_ENBL_SLEEP;
-        Spec->option.sleep_tolerance = SleepTolerance;
-    }
-}
-
-void FMuJoCoOptions::ApplyOverridesToModel(mjModel* Model) const
-{
-    if (!Model) return;
-
-    if (bOverride_Timestep) Model->opt.timestep = Timestep;
-
-    if (bOverride_Gravity)
-    {
-        Model->opt.gravity[0] =  Gravity.X / 100.0;
-        Model->opt.gravity[1] = -Gravity.Y / 100.0;
-        Model->opt.gravity[2] =  Gravity.Z / 100.0;
-    }
-
-    if (bOverride_Wind)
-    {
-        Model->opt.wind[0] =  Wind.X / 100.0;
-        Model->opt.wind[1] = -Wind.Y / 100.0;
-        Model->opt.wind[2] =  Wind.Z / 100.0;
-    }
-
-    if (bOverride_Magnetic)
-    {
-        Model->opt.magnetic[0] =  Magnetic.X;
-        Model->opt.magnetic[1] = -Magnetic.Y;
-        Model->opt.magnetic[2] =  Magnetic.Z;
-    }
-
-    if (bOverride_Density)      Model->opt.density = Density;
-    if (bOverride_Viscosity)    Model->opt.viscosity = Viscosity;
-    if (bOverride_Impratio)     Model->opt.impratio = Impratio;
-    if (bOverride_Tolerance)    Model->opt.tolerance = Tolerance;
-    if (bOverride_Iterations)   Model->opt.iterations = Iterations;
-    if (bOverride_LsIterations) Model->opt.ls_iterations = LsIterations;
-
-    if (bOverride_Integrator) Model->opt.integrator = (int)Integrator;
-    if (bOverride_Cone)       Model->opt.cone = (int)Cone;
-    if (bOverride_Solver)     Model->opt.solver = (int)Solver;
-
-    if (bOverride_NoslipIterations) Model->opt.noslip_iterations = NoslipIterations;
-    if (bOverride_NoslipTolerance)  Model->opt.noslip_tolerance = NoslipTolerance;
-    if (bOverride_CCD_Iterations)   Model->opt.ccd_iterations = CCD_Iterations;
-    if (bOverride_CCD_Tolerance)    Model->opt.ccd_tolerance = CCD_Tolerance;
-
-    // MultiCCD
-    constexpr int MJ_ENBL_MULTICCD = 1 << 4;
-    if (bEnableMultiCCD)
-        Model->opt.enableflags |= MJ_ENBL_MULTICCD;
-    else
-        Model->opt.enableflags &= ~MJ_ENBL_MULTICCD;
-
-    // Sleep
-    constexpr int MJ_ENBL_SLEEP = 1 << 5;
-    if (bEnableSleep)
-    {
-        Model->opt.enableflags |= MJ_ENBL_SLEEP;
-        Model->opt.sleep_tolerance = SleepTolerance;
-    }
-    else
-    {
-        Model->opt.enableflags &= ~MJ_ENBL_SLEEP;
-    }
-}
diff --git a/Source/URLab/Private/MuJoCo/Core/Spec/MjSpecWrapper.cpp b/Source/URLab/Private/MuJoCo/Core/Spec/MjSpecWrapper.cpp
index a9fbd4c..a1abd63 100644
--- a/Source/URLab/Private/MuJoCo/Core/Spec/MjSpecWrapper.cpp
+++ b/Source/URLab/Private/MuJoCo/Core/Spec/MjSpecWrapper.cpp
@@ -97,7 +97,7 @@ FString FMujocoSpecWrapper::AddMeshAsset(const FString& MeshName, const FString&
 
     mjsMesh* MeshAsset = mjs_addMesh(Spec, nullptr);
     mjs_setName(MeshAsset->element, TCHAR_TO_UTF8(*UniqueMeshName));
-    mjs_setString(MeshAsset->file, TCHAR_TO_UTF8(*FilePath));
+    MjSetStringRaw(MeshAsset->file, FilePath);
 
     MeshAsset->scale[0] = scale.X;
     MeshAsset->scale[1] = scale.Y;
diff --git a/Source/URLab/Private/MuJoCo/Generated/MjOptionGenerated.cpp b/Source/URLab/Private/MuJoCo/Generated/MjOptionGenerated.cpp
new file mode 100644
index 0000000..59c1c6b
--- /dev/null
+++ b/Source/URLab/Private/MuJoCo/Generated/MjOptionGenerated.cpp
@@ -0,0 +1,79 @@
+// Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+//
+// 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.
+//
+// --- LEGAL DISCLAIMER ---
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
+// trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
+//
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
+// CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
+
+#include "MuJoCo/Generated/MjOptionGenerated.h"
+#include <mujoco/mujoco.h>
+
+void FMjOptionGenerated::ApplyToSpec(mjSpec* Spec) const
+{
+    if (!Spec) return;
+    if (bOverride_Timestep) { Spec->option.timestep = Timestep; }
+    if (bOverride_Impratio) { Spec->option.impratio = Impratio; }
+    if (bOverride_Tolerance) { Spec->option.tolerance = Tolerance; }
+    if (bOverride_LsTolerance) { Spec->option.ls_tolerance = LsTolerance; }
+    if (bOverride_NoslipTolerance) { Spec->option.noslip_tolerance = NoslipTolerance; }
+    if (bOverride_CCD_Tolerance) { Spec->option.ccd_tolerance = CCD_Tolerance; }
+    if (bOverride_Gravity) { Spec->option.gravity[0] =  Gravity.X / 100.0; Spec->option.gravity[1] = -Gravity.Y / 100.0; Spec->option.gravity[2] =  Gravity.Z / 100.0; }
+    if (bOverride_Wind) { Spec->option.wind[0] =  Wind.X / 100.0; Spec->option.wind[1] = -Wind.Y / 100.0; Spec->option.wind[2] =  Wind.Z / 100.0; }
+    if (bOverride_Magnetic) { Spec->option.magnetic[0] =  Magnetic.X; Spec->option.magnetic[1] = -Magnetic.Y; Spec->option.magnetic[2] =  Magnetic.Z; }
+    if (bOverride_Density) { Spec->option.density = Density; }
+    if (bOverride_Viscosity) { Spec->option.viscosity = Viscosity; }
+    if (bOverride_OMargin) { Spec->option.o_margin = OMargin; }
+    if (bOverride_Integrator) { Spec->option.integrator = static_cast<int>(Integrator); }
+    if (bOverride_Cone) { Spec->option.cone = static_cast<int>(Cone); }
+    if (bOverride_Jacobian) { Spec->option.jacobian = Jacobian; }
+    if (bOverride_Solver) { Spec->option.solver = static_cast<int>(Solver); }
+    if (bOverride_Iterations) { Spec->option.iterations = Iterations; }
+    if (bOverride_LsIterations) { Spec->option.ls_iterations = LsIterations; }
+    if (bOverride_NoslipIterations) { Spec->option.noslip_iterations = NoslipIterations; }
+    if (bOverride_CCD_Iterations) { Spec->option.ccd_iterations = CCD_Iterations; }
+    ApplyMjOptionExtras(&Spec->option, *this, Spec);
+}
+
+void FMjOptionGenerated::ApplyOverridesToModel(mjModel* Model) const
+{
+    if (!Model) return;
+    if (bOverride_Timestep) { Model->opt.timestep = Timestep; }
+    if (bOverride_Impratio) { Model->opt.impratio = Impratio; }
+    if (bOverride_Tolerance) { Model->opt.tolerance = Tolerance; }
+    if (bOverride_LsTolerance) { Model->opt.ls_tolerance = LsTolerance; }
+    if (bOverride_NoslipTolerance) { Model->opt.noslip_tolerance = NoslipTolerance; }
+    if (bOverride_CCD_Tolerance) { Model->opt.ccd_tolerance = CCD_Tolerance; }
+    if (bOverride_Gravity) { Model->opt.gravity[0] =  Gravity.X / 100.0; Model->opt.gravity[1] = -Gravity.Y / 100.0; Model->opt.gravity[2] =  Gravity.Z / 100.0; }
+    if (bOverride_Wind) { Model->opt.wind[0] =  Wind.X / 100.0; Model->opt.wind[1] = -Wind.Y / 100.0; Model->opt.wind[2] =  Wind.Z / 100.0; }
+    if (bOverride_Magnetic) { Model->opt.magnetic[0] =  Magnetic.X; Model->opt.magnetic[1] = -Magnetic.Y; Model->opt.magnetic[2] =  Magnetic.Z; }
+    if (bOverride_Density) { Model->opt.density = Density; }
+    if (bOverride_Viscosity) { Model->opt.viscosity = Viscosity; }
+    if (bOverride_OMargin) { Model->opt.o_margin = OMargin; }
+    if (bOverride_Integrator) { Model->opt.integrator = static_cast<int>(Integrator); }
+    if (bOverride_Cone) { Model->opt.cone = static_cast<int>(Cone); }
+    if (bOverride_Jacobian) { Model->opt.jacobian = Jacobian; }
+    if (bOverride_Solver) { Model->opt.solver = static_cast<int>(Solver); }
+    if (bOverride_Iterations) { Model->opt.iterations = Iterations; }
+    if (bOverride_LsIterations) { Model->opt.ls_iterations = LsIterations; }
+    if (bOverride_NoslipIterations) { Model->opt.noslip_iterations = NoslipIterations; }
+    if (bOverride_CCD_Iterations) { Model->opt.ccd_iterations = CCD_Iterations; }
+    ApplyMjOptionExtras(&Model->opt, *this, nullptr, Model);
+}
diff --git a/Source/URLab/Private/MuJoCo/Generated/MjOptionGeneratedExtras.cpp b/Source/URLab/Private/MuJoCo/Generated/MjOptionGeneratedExtras.cpp
new file mode 100644
index 0000000..79958ca
--- /dev/null
+++ b/Source/URLab/Private/MuJoCo/Generated/MjOptionGeneratedExtras.cpp
@@ -0,0 +1,46 @@
+// Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+//
+// 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
+//
+// URLab pseudo-field side of FMjOptionGenerated. The codegen emits the
+// mjOption mirror struct + its ApplyToSpec / ApplyOverridesToModel
+// bodies (in MjOptionGenerated.cpp) and ends each with a call to
+// ApplyMjOptionExtras for the URLab-specific bits the spec/model layout
+// doesn't carry (MemoryMB, MultiCCD bitflag, Sleep bitflag + tolerance).
+// Edit this file directly — it is NOT codegen-managed.
+
+#include "MuJoCo/Generated/MjOptionGenerated.h"
+#include <mujoco/mujoco.h>
+
+// Bit positions in mjOption.enableflags. Match mjENBL_MULTICCD / mjENBL_SLEEP
+// from mjmodel.h; hardcoded to avoid pulling mjtEnableBit into the header.
+static constexpr int MJ_ENBL_MULTICCD = 1 << 4;
+static constexpr int MJ_ENBL_SLEEP    = 1 << 5;
+
+void ApplyMjOptionExtras(mjOption* Opt, const FMjOptionGenerated& Self,
+                         mjSpec* Spec, mjModel* /*Model*/)
+{
+    if (!Opt) return;
+
+    if (Spec && Self.bOverride_MemoryMB)
+    {
+        Spec->memory = static_cast<mjtSize>(Self.MemoryMB) * 1024 * 1024;
+    }
+
+    if (Self.bEnableMultiCCD) Opt->enableflags |=  MJ_ENBL_MULTICCD;
+    else                      Opt->enableflags &= ~MJ_ENBL_MULTICCD;
+
+    if (Self.bEnableSleep)
+    {
+        Opt->enableflags |= MJ_ENBL_SLEEP;
+        Opt->sleep_tolerance = Self.SleepTolerance;
+    }
+    else
+    {
+        Opt->enableflags &= ~MJ_ENBL_SLEEP;
+    }
+}
diff --git a/Source/URLab/Private/MuJoCo/Input/MjPerturbation.cpp b/Source/URLab/Private/MuJoCo/Input/MjPerturbation.cpp
index 222371e..f2bf226 100644
--- a/Source/URLab/Private/MuJoCo/Input/MjPerturbation.cpp
+++ b/Source/URLab/Private/MuJoCo/Input/MjPerturbation.cpp
@@ -117,7 +117,7 @@ int32 UMjPerturbation::ResolveBodyIdFromActor(const AActor* Actor, const FVector
     if (!Actor) return -1;
 
     // Articulation path: find the nearest UMjGeom to the hit point (any geom
-    // on the actor belongs to a body via Geom->GetMj().body_id).
+    // on the actor belongs to a body via Geom->GetMj().geom_bodyid).
     TArray<UMjGeom*> Geoms;
     const_cast<AActor*>(Actor)->GetComponents<UMjGeom>(Geoms);
     if (Geoms.Num() > 0)
@@ -132,7 +132,7 @@ int32 UMjPerturbation::ResolveBodyIdFromActor(const AActor* Actor, const FVector
         }
         if (Best)
         {
-            return Best->GetMj().body_id;
+            return Best->GetMj().geom_bodyid;
         }
     }
 
diff --git a/Source/URLab/Private/MuJoCo/Utils/MjOrientationUtils.cpp b/Source/URLab/Private/MuJoCo/Utils/MjOrientationUtils.cpp
index 3e8d7b9..2c6878c 100644
--- a/Source/URLab/Private/MuJoCo/Utils/MjOrientationUtils.cpp
+++ b/Source/URLab/Private/MuJoCo/Utils/MjOrientationUtils.cpp
@@ -24,6 +24,7 @@
 #include "XmlNode.h"
 #include "MuJoCo/Utils/MjXmlUtils.h"
 #include "Utils/URLabLogging.h"
+#include "mujoco/mujoco.h"
 
 // ============================================================================
 // Compiler Settings
@@ -126,7 +127,7 @@ bool MjOrientationUtils::OrientationToMjQuat(const FXmlNode* Node, const FMjComp
             OutQuat[1] = (double)Parts[1];
             OutQuat[2] = (double)Parts[2];
             OutQuat[3] = (double)Parts[3];
-            QuatNormalize(OutQuat);
+            mju_normalize4(OutQuat);
             return true;
         }
     }
@@ -213,49 +214,19 @@ bool MjOrientationUtils::OrientationToMjQuat(const FXmlNode* Node, const FMjComp
 
 void MjOrientationUtils::AxisAngleToQuat(double x, double y, double z, double AngleRad, double OutQuat[4])
 {
-    // Normalize axis
-    double Len = FMath::Sqrt(x * x + y * y + z * z);
-    if (Len < 1e-10)
+    double Axis[3] = { x, y, z };
+    if (mju_normalize3(Axis) < 1e-10)
     {
         OutQuat[0] = 1.0; OutQuat[1] = 0.0; OutQuat[2] = 0.0; OutQuat[3] = 0.0;
         return;
     }
-
-    double InvLen = 1.0 / Len;
-    double nx = x * InvLen;
-    double ny = y * InvLen;
-    double nz = z * InvLen;
-
-    double HalfAngle = AngleRad * 0.5;
-    double s = FMath::Sin(HalfAngle);
-    double c = FMath::Cos(HalfAngle);
-
-    OutQuat[0] = c;
-    OutQuat[1] = s * nx;
-    OutQuat[2] = s * ny;
-    OutQuat[3] = s * nz;
-    QuatNormalize(OutQuat);
+    mju_axisAngle2Quat(OutQuat, Axis, AngleRad);
 }
 
 // ============================================================================
 // Euler → Quaternion
 // ============================================================================
 
-void MjOrientationUtils::ElementalRotQuat(int AxisIndex, double AngleRad, double OutQuat[4])
-{
-    double HalfAngle = AngleRad * 0.5;
-    double s = FMath::Sin(HalfAngle);
-    double c = FMath::Cos(HalfAngle);
-
-    OutQuat[0] = c;
-    OutQuat[1] = 0.0;
-    OutQuat[2] = 0.0;
-    OutQuat[3] = 0.0;
-
-    // AxisIndex: 0=x, 1=y, 2=z
-    OutQuat[1 + AxisIndex] = s;
-}
-
 void MjOrientationUtils::EulerToQuat(double e1, double e2, double e3, const FString& EulerSeq, double OutQuat[4])
 {
     if (EulerSeq.Len() != 3)
@@ -265,62 +236,9 @@ void MjOrientationUtils::EulerToQuat(double e1, double e2, double e3, const FStr
         return;
     }
 
-    double Angles[3] = { e1, e2, e3 };
-    int AxisIndices[3];
-    bool bExtrinsic[3];
-
-    for (int i = 0; i < 3; ++i)
-    {
-        TCHAR Ch = EulerSeq[i];
-        switch (Ch)
-        {
-            case 'x': AxisIndices[i] = 0; bExtrinsic[i] = false; break;
-            case 'y': AxisIndices[i] = 1; bExtrinsic[i] = false; break;
-            case 'z': AxisIndices[i] = 2; bExtrinsic[i] = false; break;
-            case 'X': AxisIndices[i] = 0; bExtrinsic[i] = true;  break;
-            case 'Y': AxisIndices[i] = 1; bExtrinsic[i] = true;  break;
-            case 'Z': AxisIndices[i] = 2; bExtrinsic[i] = true;  break;
-            default:
-                UE_LOG(LogURLabImport, Warning, TEXT("[MjOrientationUtils] Invalid euler axis character '%c'"), Ch);
-                AxisIndices[i] = 0; bExtrinsic[i] = false;
-                break;
-        }
-    }
-
-    // Build three elemental rotation quaternions
-    double Q1[4], Q2[4], Q3[4];
-    ElementalRotQuat(AxisIndices[0], Angles[0], Q1);
-    ElementalRotQuat(AxisIndices[1], Angles[1], Q2);
-    ElementalRotQuat(AxisIndices[2], Angles[2], Q3);
-
-    // Per-axis composition following MuJoCo convention (user_objects.cc):
-    //   Lowercase (intrinsic): rotate in body-fixed frame → accumulate on the right: R = R_prev * Ri
-    //   Uppercase (extrinsic): rotate in world-fixed frame → accumulate on the left:  R = Ri * R_prev
-    // This handles pure intrinsic, pure extrinsic, and mixed sequences correctly.
-
-    const double* Qi[3] = { Q1, Q2, Q3 };
-    // Start with identity
-    double Result[4] = { 1.0, 0.0, 0.0, 0.0 };
-    double Temp[4];
-
-    for (int i = 0; i < 3; ++i)
-    {
-        if (bExtrinsic[i])
-        {
-            // Extrinsic: new = Qi * Result
-            QuatMul(Qi[i], Result, Temp);
-        }
-        else
-        {
-            // Intrinsic: new = Result * Qi
-            QuatMul(Result, Qi[i], Temp);
-        }
-        Result[0] = Temp[0]; Result[1] = Temp[1]; Result[2] = Temp[2]; Result[3] = Temp[3];
-    }
-
-    OutQuat[0] = Result[0]; OutQuat[1] = Result[1]; OutQuat[2] = Result[2]; OutQuat[3] = Result[3];
-
-    QuatNormalize(OutQuat);
+    double Euler[3] = { e1, e2, e3 };
+    const auto SeqAnsi = StringCast<ANSICHAR>(*EulerSeq);
+    mju_euler2Quat(OutQuat, Euler, SeqAnsi.Get());
 }
 
 // ============================================================================
@@ -329,43 +247,37 @@ void MjOrientationUtils::EulerToQuat(double e1, double e2, double e3, const FStr
 
 void MjOrientationUtils::XYAxesToQuat(const double XYAxes[6], double OutQuat[4])
 {
-    // X axis
-    double Xx = XYAxes[0], Xy = XYAxes[1], Xz = XYAxes[2];
-    double XLen = FMath::Sqrt(Xx * Xx + Xy * Xy + Xz * Xz);
-    if (XLen < 1e-10)
+    double X[3] = { XYAxes[0], XYAxes[1], XYAxes[2] };
+    if (mju_normalize3(X) < 1e-10)
     {
         OutQuat[0] = 1.0; OutQuat[1] = 0.0; OutQuat[2] = 0.0; OutQuat[3] = 0.0;
         return;
     }
-    Xx /= XLen; Xy /= XLen; Xz /= XLen;
-
-    // Y axis (orthogonalize against X)
-    double Yx = XYAxes[3], Yy = XYAxes[4], Yz = XYAxes[5];
-    double Dot = Xx * Yx + Xy * Yy + Xz * Yz;
-    Yx -= Dot * Xx;
-    Yy -= Dot * Xy;
-    Yz -= Dot * Xz;
-    double YLen = FMath::Sqrt(Yx * Yx + Yy * Yy + Yz * Yz);
-    if (YLen < 1e-10)
+
+    double Y[3] = { XYAxes[3], XYAxes[4], XYAxes[5] };
+    const double Dot = X[0]*Y[0] + X[1]*Y[1] + X[2]*Y[2];
+    Y[0] -= Dot*X[0]; Y[1] -= Dot*X[1]; Y[2] -= Dot*X[2];
+    if (mju_normalize3(Y) < 1e-10)
     {
         OutQuat[0] = 1.0; OutQuat[1] = 0.0; OutQuat[2] = 0.0; OutQuat[3] = 0.0;
         return;
     }
-    Yx /= YLen; Yy /= YLen; Yz /= YLen;
-
-    // Z axis = X cross Y
-    double Zx = Xy * Yz - Xz * Yy;
-    double Zy = Xz * Yx - Xx * Yz;
-    double Zz = Xx * Yy - Xy * Yx;
-
-    // Build rotation matrix (row-major): row 0 = X, row 1 = Y, row 2 = Z
-    double R[9] = {
-        Xx, Xy, Xz,
-        Yx, Yy, Yz,
-        Zx, Zy, Zz
-    };
 
-    RotMatToQuat(R, OutQuat);
+    double Z[3];
+    mju_cross(Z, X, Y);
+    if (mju_normalize3(Z) < 1e-10)
+    {
+        OutQuat[0] = 1.0; OutQuat[1] = 0.0; OutQuat[2] = 0.0; OutQuat[3] = 0.0;
+        return;
+    }
+
+    // MuJoCo rotation matrix is row-major with COLUMNS = local axes (mjuu_frame2quat convention).
+    const double Mat[9] = {
+        X[0], Y[0], Z[0],
+        X[1], Y[1], Z[1],
+        X[2], Y[2], Z[2]
+    };
+    mju_mat2Quat(OutQuat, Mat);
 }
 
 // ============================================================================
@@ -374,119 +286,27 @@ void MjOrientationUtils::XYAxesToQuat(const double XYAxes[6], double OutQuat[4])
 
 void MjOrientationUtils::ZAxisToQuat(const double ZAxis[3], double OutQuat[4])
 {
-    double Zx = ZAxis[0], Zy = ZAxis[1], Zz = ZAxis[2];
-    double Len = FMath::Sqrt(Zx * Zx + Zy * Zy + Zz * Zz);
-    if (Len < 1e-10)
+    double Vec[3] = { ZAxis[0], ZAxis[1], ZAxis[2] };
+    if (mju_normalize3(Vec) < 1e-10)
     {
         OutQuat[0] = 1.0; OutQuat[1] = 0.0; OutQuat[2] = 0.0; OutQuat[3] = 0.0;
         return;
     }
-    Zx /= Len; Zy /= Len; Zz /= Len;
-
-    // Source direction: (0, 0, 1)
-    // target direction: (Zx, Zy, Zz)
-    // Rotation axis = cross(source, target), angle = acos(dot(source, target))
-
-    double DotVal = Zz; // dot((0,0,1), (Zx,Zy,Zz)) = Zz
 
-    if (DotVal > 0.9999999)
+    if (Vec[2] > 0.9999999)
     {
-        // Nearly identical — identity rotation
         OutQuat[0] = 1.0; OutQuat[1] = 0.0; OutQuat[2] = 0.0; OutQuat[3] = 0.0;
         return;
     }
-
-    if (DotVal < -0.9999999)
+    if (Vec[2] < -0.9999999)
     {
-        // Nearly opposite — 180 degree rotation around any perpendicular axis
-        // Choose X axis as the rotation axis
         OutQuat[0] = 0.0; OutQuat[1] = 1.0; OutQuat[2] = 0.0; OutQuat[3] = 0.0;
         return;
     }
 
-    // cross((0,0,1), (Zx,Zy,Zz)) = (-Zy, Zx, 0)
-    double Ax = -Zy;
-    double Ay = Zx;
-    double Az = 0.0;
-
-    double Angle = FMath::Acos(FMath::Clamp(DotVal, -1.0, 1.0));
-    AxisAngleToQuat(Ax, Ay, Az, Angle, OutQuat);
-}
-
-// ============================================================================
-// Quaternion Helpers
-// ============================================================================
-
-void MjOrientationUtils::QuatMul(const double A[4], const double B[4], double Result[4])
-{
-    // Hamilton product: A * B  with layout [w, x, y, z]
-    Result[0] = A[0]*B[0] - A[1]*B[1] - A[2]*B[2] - A[3]*B[3];
-    Result[1] = A[0]*B[1] + A[1]*B[0] + A[2]*B[3] - A[3]*B[2];
-    Result[2] = A[0]*B[2] - A[1]*B[3] + A[2]*B[0] + A[3]*B[1];
-    Result[3] = A[0]*B[3] + A[1]*B[2] - A[2]*B[1] + A[3]*B[0];
-}
-
-void MjOrientationUtils::QuatNormalize(double Q[4])
-{
-    double Len = FMath::Sqrt(Q[0]*Q[0] + Q[1]*Q[1] + Q[2]*Q[2] + Q[3]*Q[3]);
-    if (Len < 1e-10)
-    {
-        Q[0] = 1.0; Q[1] = 0.0; Q[2] = 0.0; Q[3] = 0.0;
-        return;
-    }
-    double InvLen = 1.0 / Len;
-    Q[0] *= InvLen; Q[1] *= InvLen; Q[2] *= InvLen; Q[3] *= InvLen;
-}
-
-void MjOrientationUtils::RotMatToQuat(const double R[9], double OutQuat[4])
-{
-    // Rotation matrix in row-major order:
-    //   R[0] R[1] R[2]     =  row 0 (X axis)
-    //   R[3] R[4] R[5]     =  row 1 (Y axis)
-    //   R[6] R[7] R[8]     =  row 2 (Z axis)
-    //
-    // MuJoCo stores rotation matrices as 3x3 row-major where columns are frame axes.
-    // For xyaxes, we constructed R with rows = axes, so the rotation matrix for MuJoCo
-    // frame would have the columns as the axes. Since our R has rows as axes,
-    // the equivalent rotation matrix (transforms world to local) has columns as the axes.
-    // We need to convert from this rotation matrix to quaternion.
-    //
-    // Standard Shepperd method for robust matrix-to-quaternion conversion:
-
-    double Trace = R[0] + R[4] + R[8];
-
-    if (Trace > 0.0)
-    {
-        double s = FMath::Sqrt(Trace + 1.0) * 2.0; // s = 4*w
-        OutQuat[0] = 0.25 * s;
-        OutQuat[1] = (R[5] - R[7]) / s;  // (R[1][2] - R[2][1]) / s
-        OutQuat[2] = (R[6] - R[2]) / s;  // (R[2][0] - R[0][2]) / s
-        OutQuat[3] = (R[1] - R[3]) / s;  // (R[0][1] - R[1][0]) / s
-    }
-    else if (R[0] > R[4] && R[0] > R[8])
-    {
-        double s = FMath::Sqrt(1.0 + R[0] - R[4] - R[8]) * 2.0; // s = 4*x
-        OutQuat[0] = (R[5] - R[7]) / s;
-        OutQuat[1] = 0.25 * s;
-        OutQuat[2] = (R[1] + R[3]) / s;
-        OutQuat[3] = (R[2] + R[6]) / s;
-    }
-    else if (R[4] > R[8])
-    {
-        double s = FMath::Sqrt(1.0 + R[4] - R[0] - R[8]) * 2.0; // s = 4*y
-        OutQuat[0] = (R[6] - R[2]) / s;
-        OutQuat[1] = (R[1] + R[3]) / s;
-        OutQuat[2] = 0.25 * s;
-        OutQuat[3] = (R[5] + R[7]) / s;
-    }
-    else
-    {
-        double s = FMath::Sqrt(1.0 + R[8] - R[0] - R[4]) * 2.0; // s = 4*z
-        OutQuat[0] = (R[1] - R[3]) / s;
-        OutQuat[1] = (R[2] + R[6]) / s;
-        OutQuat[2] = (R[5] + R[7]) / s;
-        OutQuat[3] = 0.25 * s;
-    }
-
-    QuatNormalize(OutQuat);
+    // axis = cross((0,0,1), Vec) = (-Vec[1], Vec[0], 0)
+    double Axis[3] = { -Vec[1], Vec[0], 0.0 };
+    const double Angle = FMath::Acos(FMath::Clamp(Vec[2], -1.0, 1.0));
+    mju_normalize3(Axis);
+    mju_axisAngle2Quat(OutQuat, Axis, Angle);
 }
diff --git a/Source/URLab/Private/MuJoCo/Utils/MjUtils.cpp b/Source/URLab/Private/MuJoCo/Utils/MjUtils.cpp
index 38b2061..5adb75c 100644
--- a/Source/URLab/Private/MuJoCo/Utils/MjUtils.cpp
+++ b/Source/URLab/Private/MuJoCo/Utils/MjUtils.cpp
@@ -142,9 +142,9 @@ void MjUtils::DrawDebugGeom(UWorld* World, const mjModel* m, const GeomView& geo
 {
     if (!World || !m) return;
 
-    mjtNum* pos = geom_view.xpos;
-    mjtNum* mat = geom_view.xmat;
-    mjtNum* size = geom_view.size;
+    mjtNum* pos = geom_view.geom_xpos;
+    mjtNum* mat = geom_view.geom_xmat;
+    mjtNum* size = geom_view.geom_size;
     
     // Draw if group 3 (collision convention) OR if both contype and conaffinity are non-zero (active collider)
     int group = geom_view._m->geom_group[geom_view.id];
@@ -163,7 +163,7 @@ void MjUtils::DrawDebugGeom(UWorld* World, const mjModel* m, const GeomView& geo
     mju_mat2Quat(_quat, mat);
     FQuat Rotation = MjToUERotation(_quat);
 
-    switch (geom_view.type) {
+    switch (geom_view.geom_type) {
         case mjGEOM_BOX:
         {
             FColor Color = DrawColor != FColor::Magenta ? DrawColor : FColor::Cyan;
@@ -200,7 +200,7 @@ void MjUtils::DrawDebugGeom(UWorld* World, const mjModel* m, const GeomView& geo
         case mjGEOM_MESH:
         {
             FColor Color = DrawColor != FColor::Magenta ? DrawColor : FColor::Magenta;
-            int meshId = geom_view.dataid;
+            int meshId = geom_view.geom_dataid;
             if (meshId < 0) break;
 
             float* vertices = m->mesh_vert + m->mesh_vertadr[meshId] * 3;
diff --git a/Source/URLab/Public/MuJoCo/Components/Actuators/MjActuator.h b/Source/URLab/Public/MuJoCo/Components/Actuators/MjActuator.h
index a630ff5..8fbd64a 100644
--- a/Source/URLab/Public/MuJoCo/Components/Actuators/MjActuator.h
+++ b/Source/URLab/Public/MuJoCo/Components/Actuators/MjActuator.h
@@ -29,6 +29,7 @@
 #include "MuJoCo/Core/Spec/MjSpecElement.h"
 #include "MuJoCo/Components/MjComponent.h"
 #include "MuJoCo/Components/Defaults/MjDefault.h"
+#include "MuJoCo/Generated/MjArticulationEnums.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
 #include <atomic>
 #include "MjActuator.generated.h"
@@ -51,64 +52,8 @@ enum class EMjActuatorType : uint8
     DcMotor
 };
 
-/**
- * @enum EMjActuatorTrnType
- * @brief Defines the type of transmission for the actuator.
- */
-UENUM(BlueprintType)
-enum class EMjActuatorTrnType : uint8
-{
-    Joint,
-    JointInParent,
-    SliderCrank,
-    Tendon,
-    Site,
-    Body,
-    Undefined
-};
-
-/**
- * @enum EMjGainType
- * @brief MJCF actuator gain function. Mirrors MuJoCo's mjtGain.
- *        Most actuator subtypes hardcode this via mjs_setToX; useful on
- *        ``<general>`` to express the raw gainprm formula directly.
- */
-UENUM(BlueprintType)
-enum class EMjGainType : uint8
-{
-    Fixed,
-    Affine,
-    Muscle,
-    User
-};
-
-/**
- * @enum EMjBiasType
- * @brief MJCF actuator bias function. Mirrors MuJoCo's mjtBias.
- */
-UENUM(BlueprintType)
-enum class EMjBiasType : uint8
-{
-    None,
-    Affine,
-    Muscle,
-    User
-};
-
-/**
- * @enum EMjDynType
- * @brief MJCF actuator activation dynamics. Mirrors MuJoCo's mjtDyn.
- */
-UENUM(BlueprintType)
-enum class EMjDynType : uint8
-{
-    None,
-    Integrator,
-    Filter,
-    FilterExact,
-    Muscle,
-    User
-};
+// EMjActuatorTrnType, EMjGainType, EMjBiasType, EMjDynType moved to
+// MuJoCo/Generated/MjArticulationEnums.h (codegen-owned).
 
 /**
  * @class UMjActuator
@@ -124,137 +69,120 @@ class URLAB_API UMjActuator : public UMjComponent
 
 public:	
     // --- CODEGEN_PROPERTIES_START ---
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_group = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_group"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_group"))
     int32 group = 0;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_nsample = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_nsample"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_nsample"))
     int32 nsample = 0;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_interp = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_interp"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_interp"))
     int32 interp = 0;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_delay = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_delay"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_delay"))
     float delay = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_ctrllimited = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_ctrllimited"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_ctrllimited"))
     bool ctrllimited = false;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_forcelimited = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_forcelimited"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_forcelimited"))
     bool forcelimited = false;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_actlimited = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_actlimited"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_actlimited"))
     bool actlimited = false;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_ctrlrange = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_ctrlrange"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_ctrlrange"))
     TArray<float> ctrlrange = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_forcerange = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_forcerange"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_forcerange"))
     TArray<float> forcerange = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_actrange = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_actrange"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_actrange"))
     TArray<float> actrange = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_lengthrange = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_lengthrange"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_lengthrange"))
     TArray<float> lengthrange = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_gear = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_gear"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_gear"))
     TArray<float> gear = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_damping = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_damping"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_damping"))
     TArray<float> damping = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_armature = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_armature"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_armature"))
     float armature = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_cranklength = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_cranklength"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_cranklength"))
     float cranklength = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_gainprm = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_gainprm"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_gainprm"))
     TArray<float> gainprm = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_biasprm = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_biasprm"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_biasprm"))
     TArray<float> biasprm = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_dynprm = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_dynprm"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_dynprm"))
     TArray<float> dynprm = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_actdim = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_actdim"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_actdim"))
     int32 actdim = 0;
-    // --- CODEGEN_PROPERTIES_END ---
-
-    UMjActuator();
-
-    /** @brief The type of actuator dynamics (e.g. Motor, Position). */
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator")
-    EMjActuatorType Type;
 
-    /** @brief The transmission type connecting the actuator to the system. */
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator")
-    EMjActuatorTrnType TransmissionType;
-
-    // gaintype / biastype / dyntype: hand-declared because the UPROPERTY type
-    // is a URLab enum. Codegen owns the XML-string -> enum import and the
-    // enum -> mjtGain/mjtBias/mjtDyn export switch via xml_enum_attrs in
-    // codegen_rules.json. Useful primarily on <general> actuators; for
-    // motor/position/velocity/etc. subtypes the mjs_setToX preset writes
-    // these fields and overrides the user's choice.
     UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_GainType = false;
 
@@ -272,13 +200,32 @@ class URLAB_API UMjActuator : public UMjComponent
 
     UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_DynType"))
     EMjDynType DynType = EMjDynType::None;
+    // --- CODEGEN_PROPERTIES_END ---
+
+    UMjActuator();
+
+    /** @brief The type of actuator dynamics (e.g. Motor, Position). */
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator")
+    EMjActuatorType Type;
+
+    /** @brief The transmission type connecting the actuator to the system. */
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator")
+    EMjActuatorTrnType TransmissionType;
+
+    // gaintype / biastype / dyntype UPROPERTYs are codegen-emitted inside
+    // CODEGEN_PROPERTIES via xml_enum_attrs.emit_property_decl. Useful
+    // primarily on <general> actuators; for motor/position/velocity/etc.
+    // subtypes the mjs_setToX preset writes these fields and overrides
+    // the user's choice.
 
     /** @brief Optional MuJoCo class name to inherit defaults from (string fallback). */
     UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(GetOptions="GetDefaultClassOptions"))
     FString MjClassName;
 
-    /** @brief Reference to a UMjDefault component for default class inheritance. */
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator")
+    /** @brief Reference to a UMjDefault component for default class inheritance. Hidden from the
+     *  Details panel — synced from MjClassName via the editor dropdown. */
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator",
+              meta=(EditCondition="false", EditConditionHides))
     UMjDefault* DefaultClass = nullptr;
 
     virtual FString GetMjClassName() const override
diff --git a/Source/URLab/Public/MuJoCo/Components/Actuators/MjAdhesionActuator.h b/Source/URLab/Public/MuJoCo/Components/Actuators/MjAdhesionActuator.h
index eddad98..aebb876 100644
--- a/Source/URLab/Public/MuJoCo/Components/Actuators/MjAdhesionActuator.h
+++ b/Source/URLab/Public/MuJoCo/Components/Actuators/MjAdhesionActuator.h
@@ -36,10 +36,10 @@ class URLAB_API UMjAdhesionActuator : public UMjActuator
     GENERATED_BODY()
 public:
     // --- CODEGEN_PROPERTIES_START ---
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_gain = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_gain"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_gain"))
     float gain = 0.0f;
     // --- CODEGEN_PROPERTIES_END ---
 
diff --git a/Source/URLab/Public/MuJoCo/Components/Actuators/MjCylinderActuator.h b/Source/URLab/Public/MuJoCo/Components/Actuators/MjCylinderActuator.h
index d9b20c9..263bb3b 100644
--- a/Source/URLab/Public/MuJoCo/Components/Actuators/MjCylinderActuator.h
+++ b/Source/URLab/Public/MuJoCo/Components/Actuators/MjCylinderActuator.h
@@ -36,28 +36,28 @@ class URLAB_API UMjCylinderActuator : public UMjActuator
     GENERATED_BODY()
 public:
     // --- CODEGEN_PROPERTIES_START ---
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_timeconst = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_timeconst"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_timeconst"))
     TArray<float> timeconst = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_area = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_area"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_area"))
     float area = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_diameter = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_diameter"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_diameter"))
     float diameter = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_bias = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_bias"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_bias"))
     float bias = 0.0f;
     // --- CODEGEN_PROPERTIES_END ---
 
diff --git a/Source/URLab/Public/MuJoCo/Components/Actuators/MjDamperActuator.h b/Source/URLab/Public/MuJoCo/Components/Actuators/MjDamperActuator.h
index eab9d3e..3a94e06 100644
--- a/Source/URLab/Public/MuJoCo/Components/Actuators/MjDamperActuator.h
+++ b/Source/URLab/Public/MuJoCo/Components/Actuators/MjDamperActuator.h
@@ -36,10 +36,10 @@ class URLAB_API UMjDamperActuator : public UMjActuator
     GENERATED_BODY()
 public:
     // --- CODEGEN_PROPERTIES_START ---
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_kv = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_kv"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_kv"))
     float kv = 0.0f;
     // --- CODEGEN_PROPERTIES_END ---
 
diff --git a/Source/URLab/Public/MuJoCo/Components/Actuators/MjDcMotorActuator.h b/Source/URLab/Public/MuJoCo/Components/Actuators/MjDcMotorActuator.h
index 03255e3..eafe9e5 100644
--- a/Source/URLab/Public/MuJoCo/Components/Actuators/MjDcMotorActuator.h
+++ b/Source/URLab/Public/MuJoCo/Components/Actuators/MjDcMotorActuator.h
@@ -57,58 +57,58 @@ class URLAB_API UMjDcMotorActuator : public UMjActuator
 
 public:
     // --- CODEGEN_PROPERTIES_START ---
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_motorconst = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_motorconst"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_motorconst"))
     TArray<float> motorconst = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_resistance = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_resistance"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_resistance"))
     float resistance = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_nominal = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_nominal"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_nominal"))
     TArray<float> nominal = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_saturation = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_saturation"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_saturation"))
     TArray<float> saturation = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_inductance = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_inductance"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_inductance"))
     TArray<float> inductance = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_cogging = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_cogging"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_cogging"))
     TArray<float> cogging = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_controller = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_controller"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_controller"))
     TArray<float> controller = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_thermal = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_thermal"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_thermal"))
     TArray<float> thermal = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_lugre = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_lugre"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_lugre"))
     TArray<float> lugre = {};
     // --- CODEGEN_PROPERTIES_END ---
 
diff --git a/Source/URLab/Public/MuJoCo/Components/Actuators/MjGeneralActuator.h b/Source/URLab/Public/MuJoCo/Components/Actuators/MjGeneralActuator.h
index 2ec662d..0d83b74 100644
--- a/Source/URLab/Public/MuJoCo/Components/Actuators/MjGeneralActuator.h
+++ b/Source/URLab/Public/MuJoCo/Components/Actuators/MjGeneralActuator.h
@@ -36,10 +36,10 @@ class URLAB_API UMjGeneralActuator : public UMjActuator
     GENERATED_BODY()
 public:
     // --- CODEGEN_PROPERTIES_START ---
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_actearly = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_actearly"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_actearly"))
     bool actearly = false;
     // --- CODEGEN_PROPERTIES_END ---
 
diff --git a/Source/URLab/Public/MuJoCo/Components/Actuators/MjIntVelocityActuator.h b/Source/URLab/Public/MuJoCo/Components/Actuators/MjIntVelocityActuator.h
index 404890c..038ebd9 100644
--- a/Source/URLab/Public/MuJoCo/Components/Actuators/MjIntVelocityActuator.h
+++ b/Source/URLab/Public/MuJoCo/Components/Actuators/MjIntVelocityActuator.h
@@ -36,28 +36,28 @@ class URLAB_API UMjIntVelocityActuator : public UMjActuator
     GENERATED_BODY()
 public:
     // --- CODEGEN_PROPERTIES_START ---
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_inheritrange = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_inheritrange"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_inheritrange"))
     float inheritrange = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_kp = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_kp"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_kp"))
     float kp = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_kv = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_kv"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_kv"))
     float kv = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_dampratio = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_dampratio"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_dampratio"))
     float dampratio = 0.0f;
     // --- CODEGEN_PROPERTIES_END ---
 
diff --git a/Source/URLab/Public/MuJoCo/Components/Actuators/MjMotorActuator.h b/Source/URLab/Public/MuJoCo/Components/Actuators/MjMotorActuator.h
index 2fb077a..6a15df5 100644
--- a/Source/URLab/Public/MuJoCo/Components/Actuators/MjMotorActuator.h
+++ b/Source/URLab/Public/MuJoCo/Components/Actuators/MjMotorActuator.h
@@ -36,7 +36,6 @@ class URLAB_API UMjMotorActuator : public UMjActuator
     GENERATED_BODY()
 public:
     // --- CODEGEN_PROPERTIES_START ---
-
     // --- CODEGEN_PROPERTIES_END ---
 
     UMjMotorActuator();
diff --git a/Source/URLab/Public/MuJoCo/Components/Actuators/MjMuscleActuator.h b/Source/URLab/Public/MuJoCo/Components/Actuators/MjMuscleActuator.h
index c140a8f..5bbd3e2 100644
--- a/Source/URLab/Public/MuJoCo/Components/Actuators/MjMuscleActuator.h
+++ b/Source/URLab/Public/MuJoCo/Components/Actuators/MjMuscleActuator.h
@@ -36,64 +36,64 @@ class URLAB_API UMjMuscleActuator : public UMjActuator
     GENERATED_BODY()
 public:
     // --- CODEGEN_PROPERTIES_START ---
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_timeconst = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_timeconst"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_timeconst"))
     TArray<float> timeconst = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_tausmooth = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_tausmooth"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_tausmooth"))
     float tausmooth = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_range = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_range"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_range"))
     TArray<float> range = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_force = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_force"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_force"))
     float force = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_scale = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_scale"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_scale"))
     TArray<float> scale = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_lmin = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_lmin"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_lmin"))
     float lmin = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_lmax = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_lmax"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_lmax"))
     float lmax = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_vmax = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_vmax"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_vmax"))
     float vmax = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_fpmax = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_fpmax"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_fpmax"))
     float fpmax = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_fvmax = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_fvmax"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_fvmax"))
     float fvmax = 0.0f;
     // --- CODEGEN_PROPERTIES_END ---
 
diff --git a/Source/URLab/Public/MuJoCo/Components/Actuators/MjPositionActuator.h b/Source/URLab/Public/MuJoCo/Components/Actuators/MjPositionActuator.h
index 837bad4..373d052 100644
--- a/Source/URLab/Public/MuJoCo/Components/Actuators/MjPositionActuator.h
+++ b/Source/URLab/Public/MuJoCo/Components/Actuators/MjPositionActuator.h
@@ -36,34 +36,34 @@ class URLAB_API UMjPositionActuator : public UMjActuator
     GENERATED_BODY()
 public:
     // --- CODEGEN_PROPERTIES_START ---
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_inheritrange = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_inheritrange"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_inheritrange"))
     float inheritrange = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_kp = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_kp"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_kp"))
     float kp = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_kv = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_kv"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_kv"))
     float kv = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_dampratio = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_dampratio"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_dampratio"))
     float dampratio = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_timeconst = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_timeconst"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_timeconst"))
     TArray<float> timeconst = {};
     // --- CODEGEN_PROPERTIES_END ---
 
diff --git a/Source/URLab/Public/MuJoCo/Components/Actuators/MjVelocityActuator.h b/Source/URLab/Public/MuJoCo/Components/Actuators/MjVelocityActuator.h
index 78799f9..1d535b1 100644
--- a/Source/URLab/Public/MuJoCo/Components/Actuators/MjVelocityActuator.h
+++ b/Source/URLab/Public/MuJoCo/Components/Actuators/MjVelocityActuator.h
@@ -36,10 +36,10 @@ class URLAB_API UMjVelocityActuator : public UMjActuator
     GENERATED_BODY()
 public:
     // --- CODEGEN_PROPERTIES_START ---
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjActuator", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Actuator", meta=(InlineEditConditionToggle))
     bool bOverride_kv = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjActuator", meta=(EditCondition="bOverride_kv"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Actuator", meta=(EditCondition="bOverride_kv"))
     float kv = 0.0f;
     // --- CODEGEN_PROPERTIES_END ---
 
diff --git a/Source/URLab/Public/MuJoCo/Components/Bodies/MjBody.h b/Source/URLab/Public/MuJoCo/Components/Bodies/MjBody.h
index e8ad076..b1f1c7b 100644
--- a/Source/URLab/Public/MuJoCo/Components/Bodies/MjBody.h
+++ b/Source/URLab/Public/MuJoCo/Components/Bodies/MjBody.h
@@ -34,13 +34,20 @@
 
 /**
  * @enum EMjBodySleepPolicy
- * @brief Per-body sleep policy, matching MuJoCo's mjtSleepPolicy (user-settable subset).
+ * @brief Per-body sleep policy, matching MuJoCo's mjtSleepPolicy.
+ *
+ * The canonical way to opt out of per-body override is the
+ * bOverride_SleepPolicy toggle on UMjBody (toggle off => no per-body sleep
+ * value is written; mjsBody.sleep keeps the compiled default). The Auto
+ * entry exists because UHT requires UENUMs to have a 0-valued entry; it
+ * maps to mjSLEEP_AUTO so writing it explicitly is equivalent to not
+ * overriding at all.
  */
 UENUM(BlueprintType)
 enum class EMjBodySleepPolicy : uint8
 {
-    /** Let the compiler/global policy decide (mjSLEEP_AUTO). */
-    Default     = 0  UMETA(DisplayName="Default (Auto)"),
+    /** Use the global option (mjSLEEP_AUTO). Equivalent to leaving the override toggle off. */
+    Auto        = 0  UMETA(DisplayName="Auto (use global)"),
     /** This body's tree is never allowed to sleep (mjSLEEP_NEVER). */
     Never       = 3  UMETA(DisplayName="Never"),
     /** This body's tree is allowed to sleep (mjSLEEP_ALLOWED). */
@@ -56,34 +63,34 @@ class URLAB_API UMjBody : public UMjComponent
 
 public:
     // --- CODEGEN_PROPERTIES_START ---
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjBody|Spatial Pose", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Body|Spatial Pose", meta=(InlineEditConditionToggle))
     bool bOverride_Pos = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjBody|Spatial Pose", meta=(EditCondition="bOverride_Pos"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Body|Spatial Pose", meta=(EditCondition="false", EditConditionHides))
     FVector Pos = FVector::ZeroVector;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjBody|Orientation", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Body|Orientation", meta=(InlineEditConditionToggle))
     bool bOverride_Quat = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjBody|Orientation", meta=(EditCondition="bOverride_Quat"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Body|Orientation", meta=(EditCondition="false", EditConditionHides))
     FQuat Quat = FQuat::Identity;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjBody", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Body", meta=(InlineEditConditionToggle))
     bool bOverride_childclass = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjBody", meta=(EditCondition="bOverride_childclass", GetOptions="GetChildClassOptions"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Body", meta=(EditCondition="bOverride_childclass", GetOptions="GetChildClassOptions"))
     FString childclass = TEXT("");
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjBody", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Body", meta=(InlineEditConditionToggle))
     bool bOverride_mocap = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjBody", meta=(EditCondition="bOverride_mocap", DisplayName="Driven By Unreal"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Body", meta=(EditCondition="bOverride_mocap", DisplayName="Driven By Unreal"))
     bool mocap = false;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjBody", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Body", meta=(InlineEditConditionToggle))
     bool bOverride_gravcomp = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjBody", meta=(EditCondition="bOverride_gravcomp"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Body", meta=(EditCondition="bOverride_gravcomp"))
     float gravcomp = 0.0f;
     // --- CODEGEN_PROPERTIES_END ---
 
@@ -97,10 +104,15 @@ class URLAB_API UMjBody : public UMjComponent
     TArray<FString> GetChildClassOptions() const;
 #endif
 
-    /** @brief Per-body sleep policy (MuJoCo 3.4+). Default lets the global option decide. */
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Body|sleep", meta=(InlineEditConditionToggle))
+    bool bOverride_SleepPolicy = false;
+
+    /** @brief Per-body sleep policy (MuJoCo 3.4+). When the override toggle is off,
+     *  the global option decides; when on, this value is written to mjsBody.sleep. */
     UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Body|sleep",
-        meta=(ToolTip="sleep policy for this body's kinematic tree. Only has effect when sleep is enabled in AMjManager Options."))
-    EMjBodySleepPolicy SleepPolicy = EMjBodySleepPolicy::Default;
+        meta=(EditCondition="bOverride_SleepPolicy",
+              ToolTip="sleep policy for this body's kinematic tree. Only has effect when sleep is enabled in AMjManager Options."))
+    EMjBodySleepPolicy SleepPolicy = EMjBodySleepPolicy::Never;
 
     /**
      * @brief Registers this body to the MuJoCo spec.
diff --git a/Source/URLab/Public/MuJoCo/Components/Bodies/MjFrame.h b/Source/URLab/Public/MuJoCo/Components/Bodies/MjFrame.h
index cdc2fbf..5d9eee8 100644
--- a/Source/URLab/Public/MuJoCo/Components/Bodies/MjFrame.h
+++ b/Source/URLab/Public/MuJoCo/Components/Bodies/MjFrame.h
@@ -45,22 +45,22 @@ class URLAB_API UMjFrame : public UMjComponent
 
 public:
     // --- CODEGEN_PROPERTIES_START ---
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjFrame|Spatial Pose", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Frame|Spatial Pose", meta=(InlineEditConditionToggle))
     bool bOverride_Pos = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjFrame|Spatial Pose", meta=(EditCondition="bOverride_Pos"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Frame|Spatial Pose", meta=(EditCondition="false", EditConditionHides))
     FVector Pos = FVector::ZeroVector;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjFrame|Orientation", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Frame|Orientation", meta=(InlineEditConditionToggle))
     bool bOverride_Quat = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjFrame|Orientation", meta=(EditCondition="bOverride_Quat"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Frame|Orientation", meta=(EditCondition="false", EditConditionHides))
     FQuat Quat = FQuat::Identity;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjFrame", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Frame", meta=(InlineEditConditionToggle))
     bool bOverride_childclass = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjFrame", meta=(EditCondition="bOverride_childclass"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Frame", meta=(EditCondition="bOverride_childclass"))
     FString childclass = TEXT("");
     // --- CODEGEN_PROPERTIES_END ---
 
diff --git a/Source/URLab/Public/MuJoCo/Components/Constraints/MjEquality.h b/Source/URLab/Public/MuJoCo/Components/Constraints/MjEquality.h
index ef60903..56abc66 100644
--- a/Source/URLab/Public/MuJoCo/Components/Constraints/MjEquality.h
+++ b/Source/URLab/Public/MuJoCo/Components/Constraints/MjEquality.h
@@ -58,58 +58,58 @@ class URLAB_API UMjEquality : public UMjComponent
 
 public:
     // --- CODEGEN_PROPERTIES_START ---
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjEquality", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Equality", meta=(InlineEditConditionToggle))
     bool bOverride_relpose = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjEquality", meta=(EditCondition="bOverride_relpose"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Equality", meta=(EditCondition="bOverride_relpose"))
     TArray<float> relpose = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjEquality", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Equality", meta=(InlineEditConditionToggle))
     bool bOverride_anchor = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjEquality", meta=(EditCondition="bOverride_anchor"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Equality", meta=(EditCondition="bOverride_anchor", MjUnit="cm"))
     TArray<float> anchor = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjEquality", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Equality", meta=(InlineEditConditionToggle))
     bool bOverride_site1 = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjEquality", meta=(EditCondition="bOverride_site1"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Equality", meta=(EditCondition="bOverride_site1"))
     FString site1 = TEXT("");
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjEquality", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Equality", meta=(InlineEditConditionToggle))
     bool bOverride_site2 = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjEquality", meta=(EditCondition="bOverride_site2"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Equality", meta=(EditCondition="bOverride_site2"))
     FString site2 = TEXT("");
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjEquality", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Equality", meta=(InlineEditConditionToggle))
     bool bOverride_active = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjEquality", meta=(EditCondition="bOverride_active"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Equality", meta=(EditCondition="bOverride_active"))
     float active = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjEquality", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Equality", meta=(InlineEditConditionToggle))
     bool bOverride_solref = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjEquality", meta=(EditCondition="bOverride_solref"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Equality", meta=(EditCondition="bOverride_solref"))
     TArray<float> solref = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjEquality", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Equality", meta=(InlineEditConditionToggle))
     bool bOverride_solimp = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjEquality", meta=(EditCondition="bOverride_solimp"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Equality", meta=(EditCondition="bOverride_solimp"))
     TArray<float> solimp = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjEquality", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Equality", meta=(InlineEditConditionToggle))
     bool bOverride_torquescale = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjEquality", meta=(EditCondition="bOverride_torquescale"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Equality", meta=(EditCondition="bOverride_torquescale"))
     float torquescale = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjEquality", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Equality", meta=(InlineEditConditionToggle))
     bool bOverride_polycoef = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjEquality", meta=(EditCondition="bOverride_polycoef"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Equality", meta=(EditCondition="bOverride_polycoef"))
     TArray<float> polycoef = {};
     // --- CODEGEN_PROPERTIES_END ---
 
diff --git a/Source/URLab/Public/MuJoCo/Components/Defaults/MjDefault.h b/Source/URLab/Public/MuJoCo/Components/Defaults/MjDefault.h
index 0956192..3bab9ed 100644
--- a/Source/URLab/Public/MuJoCo/Components/Defaults/MjDefault.h
+++ b/Source/URLab/Public/MuJoCo/Components/Defaults/MjDefault.h
@@ -55,12 +55,16 @@ class URLAB_API UMjDefault : public UMjComponent
     /** @brief Default constructor. */
 	UMjDefault();
 
-    /** @brief Name of the default class. If empty, these defaults apply to the global scope. */
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Default")
+    /** @brief Name of the default class. If empty, these defaults apply to the global scope.
+     *  Hidden from the Details panel — synced from the SCS hierarchy. */
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Default",
+              meta=(EditCondition="false", EditConditionHides))
     FString ClassName;
 
-    /** @brief Name of the parent default class for inheritance. */
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Default")
+    /** @brief Name of the parent default class for inheritance. Hidden from the Details panel —
+     *  synced from the SCS hierarchy. */
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Default",
+              meta=(EditCondition="false", EditConditionHides))
     FString ParentClassName;
 
     /**
diff --git a/Source/URLab/Public/MuJoCo/Components/Deformable/MjFlexcomp.h b/Source/URLab/Public/MuJoCo/Components/Deformable/MjFlexcomp.h
index 7d60c16..157c55e 100644
--- a/Source/URLab/Public/MuJoCo/Components/Deformable/MjFlexcomp.h
+++ b/Source/URLab/Public/MuJoCo/Components/Deformable/MjFlexcomp.h
@@ -24,6 +24,7 @@
 
 #include "CoreMinimal.h"
 #include "MuJoCo/Components/MjComponent.h"
+#include "MuJoCo/Generated/MjArticulationEnums.h"
 #include <mujoco/mjspec.h>
 #include <mujoco/mujoco.h>
 #include "MuJoCo/Utils/MjOrientationUtils.h"
@@ -46,15 +47,7 @@ enum class EMjFlexcompType : uint8
     Direct     UMETA(DisplayName = "Direct (manual point + element)"),
 };
 
-UENUM(BlueprintType)
-enum class EMjFlexcompDof : uint8
-{
-    Full       UMETA(DisplayName = "Full"),
-    Radial     UMETA(DisplayName = "Radial"),
-    Trilinear  UMETA(DisplayName = "Trilinear"),
-    Quadratic  UMETA(DisplayName = "Quadratic"),
-    TwoD       UMETA(DisplayName = "2D"),
-};
+// EMjFlexcompDof moved to MuJoCo/Generated/MjArticulationEnums.h.
 
 /**
  * @class UMjFlexcomp
@@ -71,124 +64,124 @@ class URLAB_API UMjFlexcomp : public UMjComponent
 
 public:
     // --- CODEGEN_PROPERTIES_START ---
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjFlexcomp|Spatial Pose", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Flexcomp|Spatial Pose", meta=(InlineEditConditionToggle))
     bool bOverride_Pos = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjFlexcomp|Spatial Pose", meta=(EditCondition="bOverride_Pos"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Flexcomp|Spatial Pose", meta=(EditCondition="false", EditConditionHides))
     FVector Pos = FVector::ZeroVector;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjFlexcomp|Orientation", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Flexcomp|Orientation", meta=(InlineEditConditionToggle))
     bool bOverride_Quat = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjFlexcomp|Orientation", meta=(EditCondition="bOverride_Quat"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Flexcomp|Orientation", meta=(EditCondition="false", EditConditionHides))
     FQuat Quat = FQuat::Identity;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjFlexcomp", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Flexcomp", meta=(InlineEditConditionToggle))
     bool bOverride_group = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjFlexcomp", meta=(EditCondition="bOverride_group"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Flexcomp", meta=(EditCondition="bOverride_group"))
     int32 group = 0;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjFlexcomp", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Flexcomp", meta=(InlineEditConditionToggle))
     bool bOverride_dim = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjFlexcomp", meta=(EditCondition="bOverride_dim"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Flexcomp", meta=(EditCondition="bOverride_dim"))
     int32 dim = 0;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjFlexcomp", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Flexcomp", meta=(InlineEditConditionToggle))
     bool bOverride_count = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjFlexcomp", meta=(EditCondition="bOverride_count"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Flexcomp", meta=(EditCondition="bOverride_count"))
     TArray<int32> count = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjFlexcomp", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Flexcomp", meta=(InlineEditConditionToggle))
     bool bOverride_cellcount = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjFlexcomp", meta=(EditCondition="bOverride_cellcount"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Flexcomp", meta=(EditCondition="bOverride_cellcount"))
     TArray<int32> cellcount = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjFlexcomp", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Flexcomp", meta=(InlineEditConditionToggle))
     bool bOverride_spacing = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjFlexcomp", meta=(EditCondition="bOverride_spacing"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Flexcomp", meta=(EditCondition="bOverride_spacing", MjUnit="m"))
     TArray<float> spacing = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjFlexcomp", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Flexcomp", meta=(InlineEditConditionToggle))
     bool bOverride_radius = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjFlexcomp", meta=(EditCondition="bOverride_radius"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Flexcomp", meta=(EditCondition="bOverride_radius", Units="m"))
     float radius = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjFlexcomp", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Flexcomp", meta=(InlineEditConditionToggle))
     bool bOverride_rigid = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjFlexcomp", meta=(EditCondition="bOverride_rigid"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Flexcomp", meta=(EditCondition="bOverride_rigid"))
     bool rigid = false;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjFlexcomp", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Flexcomp", meta=(InlineEditConditionToggle))
     bool bOverride_mass = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjFlexcomp", meta=(EditCondition="bOverride_mass"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Flexcomp", meta=(EditCondition="bOverride_mass"))
     float mass = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjFlexcomp", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Flexcomp", meta=(InlineEditConditionToggle))
     bool bOverride_inertiabox = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjFlexcomp", meta=(EditCondition="bOverride_inertiabox"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Flexcomp", meta=(EditCondition="bOverride_inertiabox"))
     float inertiabox = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjFlexcomp", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Flexcomp", meta=(InlineEditConditionToggle))
     bool bOverride_scale = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjFlexcomp", meta=(EditCondition="bOverride_scale"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Flexcomp", meta=(EditCondition="bOverride_scale"))
     TArray<float> scale = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjFlexcomp", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Flexcomp", meta=(InlineEditConditionToggle))
     bool bOverride_file = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjFlexcomp", meta=(EditCondition="bOverride_file"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Flexcomp", meta=(EditCondition="bOverride_file"))
     FString file = TEXT("");
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjFlexcomp", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Flexcomp", meta=(InlineEditConditionToggle))
     bool bOverride_point = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjFlexcomp", meta=(EditCondition="bOverride_point"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Flexcomp", meta=(EditCondition="bOverride_point"))
     TArray<float> point = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjFlexcomp", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Flexcomp", meta=(InlineEditConditionToggle))
     bool bOverride_element = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjFlexcomp", meta=(EditCondition="bOverride_element"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Flexcomp", meta=(EditCondition="bOverride_element"))
     TArray<int32> element = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjFlexcomp", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Flexcomp", meta=(InlineEditConditionToggle))
     bool bOverride_texcoord = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjFlexcomp", meta=(EditCondition="bOverride_texcoord"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Flexcomp", meta=(EditCondition="bOverride_texcoord"))
     TArray<float> texcoord = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjFlexcomp", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Flexcomp", meta=(InlineEditConditionToggle))
     bool bOverride_material = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjFlexcomp", meta=(EditCondition="bOverride_material"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Flexcomp", meta=(EditCondition="bOverride_material"))
     FString material = TEXT("");
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjFlexcomp", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Flexcomp", meta=(InlineEditConditionToggle))
     bool bOverride_rgba = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjFlexcomp", meta=(EditCondition="bOverride_rgba"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Flexcomp", meta=(EditCondition="bOverride_rgba"))
     FLinearColor rgba = FLinearColor::White;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjFlexcomp", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Flexcomp", meta=(InlineEditConditionToggle))
     bool bOverride_flatskin = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjFlexcomp", meta=(EditCondition="bOverride_flatskin"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Flexcomp", meta=(EditCondition="bOverride_flatskin"))
     bool flatskin = false;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjFlexcomp", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Flexcomp", meta=(InlineEditConditionToggle))
     bool bOverride_origin = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjFlexcomp", meta=(EditCondition="bOverride_origin"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Flexcomp", meta=(EditCondition="bOverride_origin"))
     TArray<float> origin = {};
     // --- CODEGEN_PROPERTIES_END ---
 
diff --git a/Source/URLab/Public/MuJoCo/Components/Geometry/MjGeom.h b/Source/URLab/Public/MuJoCo/Components/Geometry/MjGeom.h
index 58c24f0..8ebb79d 100644
--- a/Source/URLab/Public/MuJoCo/Components/Geometry/MjGeom.h
+++ b/Source/URLab/Public/MuJoCo/Components/Geometry/MjGeom.h
@@ -27,12 +27,10 @@
 #include "mujoco/mujoco.h"
 #include "MuJoCo/Utils/MjBind.h"
 #include "MuJoCo/Components/MjComponent.h"
+#include "MuJoCo/Generated/MjArticulationEnums.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
 #include "MjGeom.generated.h"
 
-
-
-
 /**
  * @enum EMjGeomType
  * @brief Defines the geometric primitive type for the MuJoCo geom.
@@ -51,35 +49,7 @@ enum class EMjGeomType : uint8
 	SDF			UMETA(DisplayName = "SDF")
 };
 
-/**
- * @enum EMjGeomInertia
- * @brief MJCF ``<geom shellinertia="...">``. Mirrors MuJoCo's mjtGeomInertia.
- *
- *  - Volume: inertia computed from solid volume (MuJoCo default).
- *  - Shell:  inertia computed from a thin shell (mass distributed on surface).
- */
-UENUM(BlueprintType)
-enum class EMjGeomInertia : uint8
-{
-	Volume UMETA(DisplayName = "Volume"),
-	Shell  UMETA(DisplayName = "Shell"),
-};
-
-/**
- * @enum EMjFluidShape
- * @brief MJCF ``<geom fluidshape="...">``. Selects the ellipsoid-fluid
- *        interaction model in MuJoCo. Stored as 0.0/1.0 in
- *        mjsGeom.fluid_ellipsoid.
- *
- *  - None:      no ellipsoid fluid model (MuJoCo default).
- *  - Ellipsoid: enable the ellipsoid-fluid interaction model.
- */
-UENUM(BlueprintType)
-enum class EMjFluidShape : uint8
-{
-	None      UMETA(DisplayName = "None"),
-	Ellipsoid UMETA(DisplayName = "Ellipsoid"),
-};
+// EMjGeomInertia, EMjFluidShape moved to MuJoCo/Generated/MjArticulationEnums.h.
 
 /**
  * @class UMjGeom
@@ -97,136 +67,136 @@ class URLAB_API UMjGeom : public UMjComponent
 
 public:
     // --- CODEGEN_PROPERTIES_START ---
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjGeom|Spatial Pose", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Geom|Spatial Pose", meta=(InlineEditConditionToggle))
     bool bOverride_Pos = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjGeom|Spatial Pose", meta=(EditCondition="bOverride_Pos"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Geom|Spatial Pose", meta=(EditCondition="false", EditConditionHides))
     FVector Pos = FVector::ZeroVector;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjGeom|Orientation", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Geom|Orientation", meta=(InlineEditConditionToggle))
     bool bOverride_Quat = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjGeom|Orientation", meta=(EditCondition="bOverride_Quat"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Geom|Orientation", meta=(EditCondition="false", EditConditionHides))
     FQuat Quat = FQuat::Identity;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjGeom", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Geom", meta=(InlineEditConditionToggle))
     bool bOverride_contype = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjGeom", meta=(EditCondition="bOverride_contype"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Geom", meta=(EditCondition="bOverride_contype"))
     int32 contype = 0;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjGeom", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Geom", meta=(InlineEditConditionToggle))
     bool bOverride_conaffinity = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjGeom", meta=(EditCondition="bOverride_conaffinity"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Geom", meta=(EditCondition="bOverride_conaffinity"))
     int32 conaffinity = 0;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjGeom", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Geom", meta=(InlineEditConditionToggle))
     bool bOverride_condim = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjGeom", meta=(EditCondition="bOverride_condim"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Geom", meta=(EditCondition="bOverride_condim"))
     int32 condim = 0;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjGeom", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Geom", meta=(InlineEditConditionToggle))
     bool bOverride_group = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjGeom", meta=(EditCondition="bOverride_group"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Geom", meta=(EditCondition="bOverride_group"))
     int32 group = 0;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjGeom", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Geom", meta=(InlineEditConditionToggle))
     bool bOverride_priority = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjGeom", meta=(EditCondition="bOverride_priority"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Geom", meta=(EditCondition="bOverride_priority"))
     int32 priority = 0;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjGeom", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Geom", meta=(InlineEditConditionToggle))
     bool bOverride_size = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjGeom", meta=(EditCondition="bOverride_size"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Geom", meta=(EditCondition="false", EditConditionHides))
     TArray<float> size = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjGeom", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Geom", meta=(InlineEditConditionToggle))
     bool bOverride_material = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjGeom", meta=(EditCondition="bOverride_material"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Geom", meta=(EditCondition="bOverride_material"))
     FString material = TEXT("");
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjGeom", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Geom", meta=(InlineEditConditionToggle))
     bool bOverride_friction = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjGeom", meta=(EditCondition="bOverride_friction"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Geom", meta=(EditCondition="bOverride_friction"))
     TArray<float> friction = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjGeom", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Geom", meta=(InlineEditConditionToggle))
     bool bOverride_mass = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjGeom", meta=(EditCondition="bOverride_mass"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Geom", meta=(EditCondition="bOverride_mass"))
     float mass = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjGeom", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Geom", meta=(InlineEditConditionToggle))
     bool bOverride_density = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjGeom", meta=(EditCondition="bOverride_density"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Geom", meta=(EditCondition="bOverride_density"))
     float density = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjGeom", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Geom", meta=(InlineEditConditionToggle))
     bool bOverride_solmix = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjGeom", meta=(EditCondition="bOverride_solmix"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Geom", meta=(EditCondition="bOverride_solmix"))
     float solmix = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjGeom", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Geom", meta=(InlineEditConditionToggle))
     bool bOverride_solref = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjGeom", meta=(EditCondition="bOverride_solref"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Geom", meta=(EditCondition="bOverride_solref"))
     TArray<float> solref = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjGeom", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Geom", meta=(InlineEditConditionToggle))
     bool bOverride_solimp = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjGeom", meta=(EditCondition="bOverride_solimp"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Geom", meta=(EditCondition="bOverride_solimp"))
     TArray<float> solimp = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjGeom", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Geom", meta=(InlineEditConditionToggle))
     bool bOverride_margin = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjGeom", meta=(EditCondition="bOverride_margin"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Geom", meta=(EditCondition="bOverride_margin", Units="m"))
     float margin = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjGeom", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Geom", meta=(InlineEditConditionToggle))
     bool bOverride_gap = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjGeom", meta=(EditCondition="bOverride_gap"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Geom", meta=(EditCondition="bOverride_gap", Units="m"))
     float gap = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjGeom", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Geom", meta=(InlineEditConditionToggle))
     bool bOverride_hfield = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjGeom", meta=(EditCondition="bOverride_hfield"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Geom", meta=(EditCondition="bOverride_hfield"))
     FString hfield = TEXT("");
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjGeom", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Geom", meta=(InlineEditConditionToggle))
     bool bOverride_mesh = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjGeom", meta=(EditCondition="bOverride_mesh"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Geom", meta=(EditCondition="bOverride_mesh"))
     FString mesh = TEXT("");
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjGeom", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Geom", meta=(InlineEditConditionToggle))
     bool bOverride_fitscale = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjGeom", meta=(EditCondition="bOverride_fitscale"))
-    bool fitscale = false;
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Geom", meta=(EditCondition="bOverride_fitscale"))
+    double fitscale = 0.0;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjGeom", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Geom", meta=(InlineEditConditionToggle))
     bool bOverride_rgba = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjGeom", meta=(EditCondition="bOverride_rgba"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Geom", meta=(EditCondition="bOverride_rgba"))
     FLinearColor rgba = FLinearColor::White;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjGeom", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Geom", meta=(InlineEditConditionToggle))
     bool bOverride_fluidcoef = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjGeom", meta=(EditCondition="bOverride_fluidcoef"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Geom", meta=(EditCondition="bOverride_fluidcoef"))
     TArray<float> fluidcoef = {};
     // --- CODEGEN_PROPERTIES_END ---
 
@@ -418,9 +388,11 @@ class URLAB_API UMjGeom : public UMjComponent
         meta=(EditCondition="Type != EMjGeomType::Mesh", EditConditionHides))
     TObjectPtr<UMaterialInterface> OverrideMaterial;
 
-    /** @brief Reference to a UMjDefault component for default class inheritance. Set via detail customization dropdown. */
-	UPROPERTY(BlueprintReadWrite, Category = "MuJoCo|Geom")
-	UMjDefault* DefaultClass;
+    /** @brief Reference to a UMjDefault component for default class inheritance. Hidden from the
+     *  Details panel — synced from MjClassName via the editor dropdown. */
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Geom",
+              meta=(EditCondition="false", EditConditionHides))
+    UMjDefault* DefaultClass = nullptr;
 
     /**
      * @brief Registers this geom to the MuJoCo spec.
diff --git a/Source/URLab/Public/MuJoCo/Components/Geometry/MjSite.h b/Source/URLab/Public/MuJoCo/Components/Geometry/MjSite.h
index 1a1b1e5..3f4a9f4 100644
--- a/Source/URLab/Public/MuJoCo/Components/Geometry/MjSite.h
+++ b/Source/URLab/Public/MuJoCo/Components/Geometry/MjSite.h
@@ -32,7 +32,10 @@
 
 /**
  * @enum EMjSiteType
- * @brief Defines the geometric shape of a MuJoCo site.
+ * @brief Defines the geometric shape of a MuJoCo site. Mirrors the
+ * geom-subset MuJoCo permits on <site>. Plane and SDF stay omitted
+ * (infinite extent / heavy data, not what a site spawn surface
+ * wants).
  */
 UENUM(BlueprintType)
 enum class EMjSiteType : uint8
@@ -42,6 +45,8 @@ enum class EMjSiteType : uint8
 	Ellipsoid	UMETA(DisplayName = "Ellipsoid"),
 	Cylinder	UMETA(DisplayName = "Cylinder"),
 	Box			UMETA(DisplayName = "Box"),
+	Mesh		UMETA(DisplayName = "Mesh"),
+	Hfield		UMETA(DisplayName = "Hfield"),
 };
 
 /**
@@ -61,40 +66,40 @@ class URLAB_API UMjSite : public UMjComponent
 public:
 
     // --- CODEGEN_PROPERTIES_START ---
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjSite|Spatial Pose", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Site|Spatial Pose", meta=(InlineEditConditionToggle))
     bool bOverride_Pos = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjSite|Spatial Pose", meta=(EditCondition="bOverride_Pos"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Site|Spatial Pose", meta=(EditCondition="false", EditConditionHides))
     FVector Pos = FVector::ZeroVector;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjSite|Orientation", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Site|Orientation", meta=(InlineEditConditionToggle))
     bool bOverride_Quat = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjSite|Orientation", meta=(EditCondition="bOverride_Quat"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Site|Orientation", meta=(EditCondition="false", EditConditionHides))
     FQuat Quat = FQuat::Identity;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjSite", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Site", meta=(InlineEditConditionToggle))
     bool bOverride_group = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjSite", meta=(EditCondition="bOverride_group"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Site", meta=(EditCondition="bOverride_group"))
     int32 group = 0;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjSite", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Site", meta=(InlineEditConditionToggle))
     bool bOverride_material = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjSite", meta=(EditCondition="bOverride_material"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Site", meta=(EditCondition="bOverride_material"))
     FString material = TEXT("");
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjSite", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Site", meta=(InlineEditConditionToggle))
     bool bOverride_size = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjSite", meta=(EditCondition="bOverride_size"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Site", meta=(EditCondition="false", EditConditionHides))
     TArray<float> size = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjSite", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Site", meta=(InlineEditConditionToggle))
     bool bOverride_rgba = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjSite", meta=(EditCondition="bOverride_rgba"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Site", meta=(EditCondition="bOverride_rgba"))
     FLinearColor rgba = FLinearColor::White;
     // --- CODEGEN_PROPERTIES_END ---
 
@@ -145,8 +150,10 @@ class URLAB_API UMjSite : public UMjComponent
     TArray<FString> GetDefaultClassOptions() const;
 #endif
 
-    /** @brief Reference to a UMjDefault component for default class inheritance. */
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Site")
+    /** @brief Reference to a UMjDefault component for default class inheritance. Hidden from the
+     *  Details panel — synced from MjClassName via the editor dropdown. */
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Site",
+              meta=(EditCondition="false", EditConditionHides))
     UMjDefault* DefaultClass = nullptr;
 
     virtual FString GetMjClassName() const override
diff --git a/Source/URLab/Public/MuJoCo/Components/Geometry/Primitives/MjBox.h b/Source/URLab/Public/MuJoCo/Components/Geometry/Primitives/MjBox.h
index 0c5f041..06e9790 100644
--- a/Source/URLab/Public/MuJoCo/Components/Geometry/Primitives/MjBox.h
+++ b/Source/URLab/Public/MuJoCo/Components/Geometry/Primitives/MjBox.h
@@ -37,13 +37,15 @@ class URLAB_API UMjBox : public UMjGeom
 
 public:
     // --- CODEGEN_PROPERTIES_START ---
-
     // --- CODEGEN_PROPERTIES_END ---
 
 	UMjBox();
 	virtual void OnRegister() override;
 
-	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Primitive")
+	/** @brief Half-extents along local X/Y/Z in MJ metres. Authored via
+	 *  the UE Transform's Scale handle; hidden from the Details panel. */
+	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Primitive",
+	          meta=(EditCondition="false", EditConditionHides))
 	FVector Extents = FVector(0.0f);
 
     /** @brief Internal-only visual mesh for the editor. */
diff --git a/Source/URLab/Public/MuJoCo/Components/Geometry/Primitives/MjCapsule.h b/Source/URLab/Public/MuJoCo/Components/Geometry/Primitives/MjCapsule.h
index fb50bc7..677121c 100644
--- a/Source/URLab/Public/MuJoCo/Components/Geometry/Primitives/MjCapsule.h
+++ b/Source/URLab/Public/MuJoCo/Components/Geometry/Primitives/MjCapsule.h
@@ -44,20 +44,22 @@ class URLAB_API UMjCapsule : public UMjGeom
 
 public:
     // --- CODEGEN_PROPERTIES_START ---
-
     // --- CODEGEN_PROPERTIES_END ---
 
     UMjCapsule();
     virtual void OnRegister() override;
 
-    /** @brief Capsule radius. */
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Primitive")
+    /** @brief Capsule radius in MJ metres. Authored via the UE Transform's
+     *  Scale handle; hidden from the Details panel. */
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Primitive",
+              meta=(EditCondition="false", EditConditionHides))
     float Radius = 0.0f;
 
-    /** @brief Capsule half-length measured along the component's local Z
-     *         axis. Total axial length from tip-to-tip is `2 * (Radius + HalfLength)`;
-     *         the shaft length (between the cap centres) is `2 * HalfLength`. */
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Primitive")
+    /** @brief Capsule half-length along the component's local Z axis.
+     *  Shaft length = ``2 * HalfLength``; tip-to-tip = ``2 * (Radius + HalfLength)``.
+     *  Authored via the Scale handle; hidden from the Details panel. */
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Primitive",
+              meta=(EditCondition="false", EditConditionHides))
     float HalfLength = 0.0f;
 
     // UPROPERTY-decorated so Unreal's GC tracks these sub-component pointers.
diff --git a/Source/URLab/Public/MuJoCo/Components/Geometry/Primitives/MjCylinder.h b/Source/URLab/Public/MuJoCo/Components/Geometry/Primitives/MjCylinder.h
index 3158aff..3b96fc6 100644
--- a/Source/URLab/Public/MuJoCo/Components/Geometry/Primitives/MjCylinder.h
+++ b/Source/URLab/Public/MuJoCo/Components/Geometry/Primitives/MjCylinder.h
@@ -37,17 +37,19 @@ class URLAB_API UMjCylinder : public UMjGeom
 
 public:
     // --- CODEGEN_PROPERTIES_START ---
-
     // --- CODEGEN_PROPERTIES_END ---
 
 	UMjCylinder();
 	virtual void OnRegister() override;
 
-	/** @brief Radius of the cylinder. */
-	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Primitive")
+	/** @brief Cylinder radius in MJ metres. Hidden — authored via Scale handle. */
+	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Primitive",
+	          meta=(EditCondition="false", EditConditionHides))
 	float Radius = 0.0f;
 
-	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Primitive")
+	/** @brief Cylinder half-length in MJ metres. Hidden — authored via Scale handle. */
+	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Primitive",
+	          meta=(EditCondition="false", EditConditionHides))
 	float HalfLength = 0.0f;
 
     /** @brief Internal-only visual mesh for the editor. */
diff --git a/Source/URLab/Public/MuJoCo/Components/Geometry/Primitives/MjEllipsoid.h b/Source/URLab/Public/MuJoCo/Components/Geometry/Primitives/MjEllipsoid.h
index 4bd629c..1b3be16 100644
--- a/Source/URLab/Public/MuJoCo/Components/Geometry/Primitives/MjEllipsoid.h
+++ b/Source/URLab/Public/MuJoCo/Components/Geometry/Primitives/MjEllipsoid.h
@@ -44,7 +44,7 @@ class URLAB_API UMjEllipsoid : public UMjGeom
     UMjEllipsoid();
 
     // --- CODEGEN_PROPERTIES_START ---
-
+    // (no type-specific schema attrs)
     // --- CODEGEN_PROPERTIES_END ---
 
     virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
diff --git a/Source/URLab/Public/MuJoCo/Components/Geometry/Primitives/MjPlane.h b/Source/URLab/Public/MuJoCo/Components/Geometry/Primitives/MjPlane.h
index 3a43e87..0d1a3ce 100644
--- a/Source/URLab/Public/MuJoCo/Components/Geometry/Primitives/MjPlane.h
+++ b/Source/URLab/Public/MuJoCo/Components/Geometry/Primitives/MjPlane.h
@@ -44,7 +44,7 @@ class URLAB_API UMjPlane : public UMjGeom
     UMjPlane();
 
     // --- CODEGEN_PROPERTIES_START ---
-
+    // (no type-specific schema attrs)
     // --- CODEGEN_PROPERTIES_END ---
 
     virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
diff --git a/Source/URLab/Public/MuJoCo/Components/Geometry/Primitives/MjSdf.h b/Source/URLab/Public/MuJoCo/Components/Geometry/Primitives/MjSdf.h
index be1a853..9d37141 100644
--- a/Source/URLab/Public/MuJoCo/Components/Geometry/Primitives/MjSdf.h
+++ b/Source/URLab/Public/MuJoCo/Components/Geometry/Primitives/MjSdf.h
@@ -44,7 +44,7 @@ class URLAB_API UMjSdf : public UMjGeom
     UMjSdf();
 
     // --- CODEGEN_PROPERTIES_START ---
-
+    // (no type-specific schema attrs)
     // --- CODEGEN_PROPERTIES_END ---
 
     virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
diff --git a/Source/URLab/Public/MuJoCo/Components/Geometry/Primitives/MjSphere.h b/Source/URLab/Public/MuJoCo/Components/Geometry/Primitives/MjSphere.h
index 521896d..b979072 100644
--- a/Source/URLab/Public/MuJoCo/Components/Geometry/Primitives/MjSphere.h
+++ b/Source/URLab/Public/MuJoCo/Components/Geometry/Primitives/MjSphere.h
@@ -37,13 +37,15 @@ class URLAB_API UMjSphere : public UMjGeom
 
 public:
     // --- CODEGEN_PROPERTIES_START ---
-
     // --- CODEGEN_PROPERTIES_END ---
 
 	UMjSphere();
 	virtual void OnRegister() override;
 
-	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Primitive")
+	/** @brief Sphere radius in MJ metres. Authored via the UE Transform's
+	 *  Scale handle; hidden from the Details panel widget. */
+	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Primitive",
+	          meta=(EditCondition="false", EditConditionHides))
 	float Radius = 0.0f;
 
     /** @brief Internal-only visual mesh for the editor. */
diff --git a/Source/URLab/Public/MuJoCo/Components/Joints/MjBallJoint.h b/Source/URLab/Public/MuJoCo/Components/Joints/MjBallJoint.h
index 58550ed..a22c5f1 100644
--- a/Source/URLab/Public/MuJoCo/Components/Joints/MjBallJoint.h
+++ b/Source/URLab/Public/MuJoCo/Components/Joints/MjBallJoint.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjBallJoint
- * @brief Specific Ball Joint component.
+ * @brief Autogenerated component representing the MuJoCo 'ball' element.
+ *
+ * Subclass of UMjJoint. Sets Type = Ball in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjBallJoint : public UMjJoint
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjBallJoint();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsJoint* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Joints/MjFreeJoint.h b/Source/URLab/Public/MuJoCo/Components/Joints/MjFreeJoint.h
index 7affd36..7627611 100644
--- a/Source/URLab/Public/MuJoCo/Components/Joints/MjFreeJoint.h
+++ b/Source/URLab/Public/MuJoCo/Components/Joints/MjFreeJoint.h
@@ -36,7 +36,6 @@ class URLAB_API UMjFreeJoint : public UMjJoint
     GENERATED_BODY()
 public:
     // --- CODEGEN_PROPERTIES_START ---
-
     // --- CODEGEN_PROPERTIES_END ---
 
     UMjFreeJoint();
diff --git a/Source/URLab/Public/MuJoCo/Components/Joints/MjHingeJoint.h b/Source/URLab/Public/MuJoCo/Components/Joints/MjHingeJoint.h
index 05a8c55..7639668 100644
--- a/Source/URLab/Public/MuJoCo/Components/Joints/MjHingeJoint.h
+++ b/Source/URLab/Public/MuJoCo/Components/Joints/MjHingeJoint.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjHingeJoint
- * @brief Specific Hinge Joint component.
+ * @brief Autogenerated component representing the MuJoCo 'hinge' element.
+ *
+ * Subclass of UMjJoint. Sets Type = Hinge in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjHingeJoint : public UMjJoint
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjHingeJoint();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsJoint* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Joints/MjJoint.h b/Source/URLab/Public/MuJoCo/Components/Joints/MjJoint.h
index f479928..2c7da8b 100644
--- a/Source/URLab/Public/MuJoCo/Components/Joints/MjJoint.h
+++ b/Source/URLab/Public/MuJoCo/Components/Joints/MjJoint.h
@@ -59,112 +59,124 @@ class URLAB_API UMjJoint : public UMjComponent
 
 public:	
     // --- CODEGEN_PROPERTIES_START ---
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjJoint", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Joint|Spatial Pose", meta=(InlineEditConditionToggle))
+    bool bOverride_Pos = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Joint|Spatial Pose", meta=(EditCondition="false", EditConditionHides))
+    FVector Pos = FVector::ZeroVector;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Joint", meta=(InlineEditConditionToggle))
     bool bOverride_group = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjJoint", meta=(EditCondition="bOverride_group"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Joint", meta=(EditCondition="bOverride_group", ToolTip="Visualisation group 0–5. MuJoCo viewer uses this to toggle visibility."))
     int32 group = 0;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjJoint", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Joint", meta=(InlineEditConditionToggle))
+    bool bOverride_Axis = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Joint", meta=(EditCondition="bOverride_Axis", ToolTip="Joint axis in the parent body's local frame. Hinge: rotation axis. Slide: translation direction. Ignored for Ball and Free."))
+    FVector Axis = FVector(0.0f, 0.0f, 1.0f);
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Joint", meta=(InlineEditConditionToggle))
     bool bOverride_springdamper = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjJoint", meta=(EditCondition="bOverride_springdamper"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Joint", meta=(EditCondition="bOverride_springdamper", ToolTip="Compact spring spec [stiffness, damping]; populated when MJCF used <joint springdamper='...'>. Overrides stiffness + damping when set."))
     TArray<float> springdamper = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjJoint", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Joint", meta=(InlineEditConditionToggle))
     bool bOverride_limited = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjJoint", meta=(EditCondition="bOverride_limited"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Joint", meta=(EditCondition="bOverride_limited"))
     bool limited = false;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjJoint", meta=(InlineEditConditionToggle))
-    bool bOverride_actuatorfrclimited = false;
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Joint", meta=(InlineEditConditionToggle))
+    bool bOverride_ActFrcLimited = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjJoint", meta=(EditCondition="bOverride_actuatorfrclimited"))
-    bool actuatorfrclimited = false;
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Joint", meta=(EditCondition="bOverride_ActFrcLimited"))
+    bool ActFrcLimited = false;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjJoint", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Joint", meta=(InlineEditConditionToggle))
     bool bOverride_solreflimit = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjJoint", meta=(EditCondition="bOverride_solreflimit"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Joint", meta=(EditCondition="bOverride_solreflimit"))
     TArray<float> solreflimit = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjJoint", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Joint", meta=(InlineEditConditionToggle))
     bool bOverride_solimplimit = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjJoint", meta=(EditCondition="bOverride_solimplimit"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Joint", meta=(EditCondition="bOverride_solimplimit"))
     TArray<float> solimplimit = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjJoint", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Joint", meta=(InlineEditConditionToggle))
     bool bOverride_solreffriction = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjJoint", meta=(EditCondition="bOverride_solreffriction"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Joint", meta=(EditCondition="bOverride_solreffriction"))
     TArray<float> solreffriction = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjJoint", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Joint", meta=(InlineEditConditionToggle))
     bool bOverride_solimpfriction = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjJoint", meta=(EditCondition="bOverride_solimpfriction"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Joint", meta=(EditCondition="bOverride_solimpfriction"))
     TArray<float> solimpfriction = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjJoint", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Joint", meta=(InlineEditConditionToggle))
     bool bOverride_stiffness = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjJoint", meta=(EditCondition="bOverride_stiffness"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Joint", meta=(EditCondition="bOverride_stiffness", ToolTip="Per-axis spring stiffness. Hinge/Ball: N·m/rad. Slide: N/m."))
     TArray<float> stiffness = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjJoint", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Joint", meta=(InlineEditConditionToggle))
     bool bOverride_range = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjJoint", meta=(EditCondition="bOverride_range"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Joint", meta=(EditCondition="bOverride_range", ToolTip="Joint position bounds [min, max]. Hinge/Ball: degrees. Slide: centimetres. Ignored when 'limited' is false."))
     TArray<float> range = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjJoint", meta=(InlineEditConditionToggle))
-    bool bOverride_actuatorfrcrange = false;
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Joint", meta=(InlineEditConditionToggle))
+    bool bOverride_ActFrcRange = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjJoint", meta=(EditCondition="bOverride_actuatorfrcrange"))
-    TArray<float> actuatorfrcrange = {};
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Joint", meta=(EditCondition="bOverride_ActFrcRange", ToolTip="Actuator force bounds [min, max] when 'ActFrcLimited' is true. Hinge/Ball: N·m. Slide: N."))
+    TArray<float> ActFrcRange = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjJoint", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Joint", meta=(InlineEditConditionToggle))
     bool bOverride_actuatorgravcomp = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjJoint", meta=(EditCondition="bOverride_actuatorgravcomp"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Joint", meta=(EditCondition="bOverride_actuatorgravcomp", ToolTip="Gravity compensation factor passed to actuators on this joint. 0 = no compensation, 1 = full gravity cancelled."))
     float actuatorgravcomp = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjJoint", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Joint", meta=(InlineEditConditionToggle))
     bool bOverride_margin = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjJoint", meta=(EditCondition="bOverride_margin"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Joint", meta=(EditCondition="bOverride_margin", ToolTip="Contact-detection margin (m). Joints inherit this when computing limit constraints."))
     float margin = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjJoint", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Joint", meta=(InlineEditConditionToggle))
     bool bOverride_ref = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjJoint", meta=(EditCondition="bOverride_ref"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Joint", meta=(EditCondition="bOverride_ref", ToolTip="Reference (zero) position. Hinge/Ball: degrees. Slide: centimetres."))
     float ref = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjJoint", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Joint", meta=(InlineEditConditionToggle))
     bool bOverride_springref = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjJoint", meta=(EditCondition="bOverride_springref"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Joint", meta=(EditCondition="bOverride_springref", ToolTip="Equilibrium position for the joint spring. Hinge/Ball: degrees. Slide: centimetres."))
     float springref = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjJoint", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Joint", meta=(InlineEditConditionToggle))
     bool bOverride_armature = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjJoint", meta=(EditCondition="bOverride_armature"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Joint", meta=(EditCondition="bOverride_armature", ToolTip="Added rotor inertia for hinges (kg·m²) or added mass for slides (kg). Models reflected inertia of the actuator."))
     float armature = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjJoint", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Joint", meta=(InlineEditConditionToggle))
     bool bOverride_damping = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjJoint", meta=(EditCondition="bOverride_damping"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Joint", meta=(EditCondition="bOverride_damping", ToolTip="Per-axis damping. Hinge/Ball: N·m·s/rad. Slide: N·s/m."))
     TArray<float> damping = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjJoint", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Joint", meta=(InlineEditConditionToggle))
     bool bOverride_frictionloss = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjJoint", meta=(EditCondition="bOverride_frictionloss"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Joint", meta=(EditCondition="bOverride_frictionloss", ToolTip="Dry-friction loss. Hinge/Ball: N·m. Slide: N."))
     float frictionloss = 0.0f;
     // --- CODEGEN_PROPERTIES_END ---
 
@@ -255,8 +267,10 @@ class URLAB_API UMjJoint : public UMjComponent
     TArray<FString> GetDefaultClassOptions() const;
 #endif
 
-    /** @brief Reference to a UMjDefault component for default class inheritance. */
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Joint")
+    /** @brief Reference to a UMjDefault component for default class inheritance. Hidden from the
+     *  Details panel — synced from MjClassName via the editor dropdown. */
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Joint",
+              meta=(EditCondition="false", EditConditionHides))
     class UMjDefault* DefaultClass = nullptr;
 
     virtual FString GetMjClassName() const override
@@ -289,15 +303,10 @@ class URLAB_API UMjJoint : public UMjComponent
     UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Joint", meta=(EditCondition="bOverride_Type"))
     EMjJointType Type = EMjJointType::Hinge;
 
-    /** @brief Override toggle for Axis. */
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|Joint", meta=(InlineEditConditionToggle))
-    bool bOverride_Axis = false;
-
-    /** @brief Local joint axis vector (relative to parent body). Ignored for Free/Ball joints. */
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Joint", meta=(EditCondition="bOverride_Axis"))
-    FVector Axis = FVector(0.0f, 0.0f, 1.0f);
-
-
+    // Axis UPROPERTY is codegen-emitted (rules.joint.vec3_convert.axis =
+    // y_negate, property_renames.axis = "Axis", property_defaults.axis =
+    // FVector(0,0,1)). The Pos UPROPERTY is codegen-emitted via the
+    // spatial_pose canon.
 
     // --- Physics Properties (with override toggles) ---
 
diff --git a/Source/URLab/Public/MuJoCo/Components/Joints/MjSlideJoint.h b/Source/URLab/Public/MuJoCo/Components/Joints/MjSlideJoint.h
index 68c7a6f..775e35c 100644
--- a/Source/URLab/Public/MuJoCo/Components/Joints/MjSlideJoint.h
+++ b/Source/URLab/Public/MuJoCo/Components/Joints/MjSlideJoint.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjSlideJoint
- * @brief Specific Slide Joint component.
+ * @brief Autogenerated component representing the MuJoCo 'slide' element.
+ *
+ * Subclass of UMjJoint. Sets Type = Slide in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjSlideJoint : public UMjJoint
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjSlideJoint();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsJoint* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Keyframes/MjKeyframe.h b/Source/URLab/Public/MuJoCo/Components/Keyframes/MjKeyframe.h
index 4016eeb..cfec868 100644
--- a/Source/URLab/Public/MuJoCo/Components/Keyframes/MjKeyframe.h
+++ b/Source/URLab/Public/MuJoCo/Components/Keyframes/MjKeyframe.h
@@ -41,46 +41,46 @@ class URLAB_API UMjKeyframe : public UMjComponent
 
 public:
     // --- CODEGEN_PROPERTIES_START ---
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjKeyframe", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Keyframe", meta=(InlineEditConditionToggle))
     bool bOverride_Time = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjKeyframe", meta=(EditCondition="bOverride_Time"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Keyframe", meta=(EditCondition="bOverride_Time"))
     float Time = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjKeyframe", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Keyframe", meta=(InlineEditConditionToggle))
     bool bOverride_Qpos = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjKeyframe", meta=(EditCondition="bOverride_Qpos"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Keyframe", meta=(EditCondition="bOverride_Qpos"))
     TArray<float> Qpos = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjKeyframe", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Keyframe", meta=(InlineEditConditionToggle))
     bool bOverride_Qvel = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjKeyframe", meta=(EditCondition="bOverride_Qvel"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Keyframe", meta=(EditCondition="bOverride_Qvel"))
     TArray<float> Qvel = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjKeyframe", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Keyframe", meta=(InlineEditConditionToggle))
     bool bOverride_Act = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjKeyframe", meta=(EditCondition="bOverride_Act"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Keyframe", meta=(EditCondition="bOverride_Act"))
     TArray<float> Act = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjKeyframe", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Keyframe", meta=(InlineEditConditionToggle))
     bool bOverride_Mpos = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjKeyframe", meta=(EditCondition="bOverride_Mpos"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Keyframe", meta=(EditCondition="bOverride_Mpos"))
     TArray<float> Mpos = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjKeyframe", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Keyframe", meta=(InlineEditConditionToggle))
     bool bOverride_Mquat = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjKeyframe", meta=(EditCondition="bOverride_Mquat"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Keyframe", meta=(EditCondition="bOverride_Mquat"))
     TArray<float> Mquat = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjKeyframe", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Keyframe", meta=(InlineEditConditionToggle))
     bool bOverride_Ctrl = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjKeyframe", meta=(EditCondition="bOverride_Ctrl"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Keyframe", meta=(EditCondition="bOverride_Ctrl"))
     TArray<float> Ctrl = {};
     // --- CODEGEN_PROPERTIES_END ---
 
diff --git a/Source/URLab/Public/MuJoCo/Components/MjComponent.h b/Source/URLab/Public/MuJoCo/Components/MjComponent.h
index 9c2ef78..091ff90 100644
--- a/Source/URLab/Public/MuJoCo/Components/MjComponent.h
+++ b/Source/URLab/Public/MuJoCo/Components/MjComponent.h
@@ -105,10 +105,14 @@ class URLAB_API UMjComponent : public USceneComponent, public IMjSpecElement
             return ViewType();
         }
 
-        // 1. Try ID based binding via spec element
-        // NOTE: mjs_getId can return stale/garbage IDs after mjs_attach merges
-        // a child spec into the root. We validate the ID against model bounds
-        // before using it.
+        // 1. Try ID based binding via spec element (the primary path)
+        // EMPIRICAL (probe test 2026-05-27, MjBindingPathProbeTest):
+        // mjs_getId returns the correct compiled-model id after mjs_attach
+        // in current MuJoCo (mj=6882095). Path 1 == Path 2 == Bind in every
+        // tested scenario. We keep the bounds check + Path 2 fallback as
+        // safety nets for edge cases not yet observed (recompile mid-PIE,
+        // multi-attach order, runtime component churn). The old v0.1-alpha
+        // comment claiming "stale/garbage IDs after mjs_attach" is obsolete.
         if (m_SpecElement)
         {
             int id = mjs_getId(m_SpecElement);
@@ -150,6 +154,39 @@ class URLAB_API UMjComponent : public USceneComponent, public IMjSpecElement
         return ViewType();
     }
 
+    /**
+     * @brief Bind a typed view AND cache the resulting mjId on the component.
+     *
+     * Collapses the 4-line pattern that 16 base components currently hand-roll:
+     *   Super::Bind(model, data, prefix);
+     *   m_<X>View = BindToView<XView>(prefix);
+     *   if (m_<X>View.id != -1) m_ID = m_<X>View.id;
+     *   else UE_LOG warning;
+     *
+     * After v8 the per-category Bind() override becomes:
+     *   Super::Bind(model, data, prefix);
+     *   BindAndCacheView(m_<X>View, prefix);
+     *
+     * 3 components keep their own Bind() override for real work
+     * (MjTactileSensor runtime Dim sync, MjPDController, MjFlexcomp); they
+     * are free to call this helper too if they want.
+     */
+    template <typename ViewType>
+    void BindAndCacheView(ViewType& OutView, const FString& Prefix)
+    {
+        OutView = BindToView<ViewType>(Prefix);
+        if (OutView.id != -1)
+        {
+            m_ID = OutView.id;
+        }
+        else
+        {
+            UE_LOG(LogURLabBind, Warning,
+                TEXT("[BindAndCacheView] '%s' — failed to bind (Prefix='%s', type=%d). m_ID stays -1."),
+                *GetName(), *Prefix, ViewType::obj_type);
+        }
+    }
+
     /** @brief Checks if the component is successfully bound to MuJoCo runtime. */
     UFUNCTION(BlueprintCallable, Category = "MuJoCo|Base")
     bool IsBound() const;
@@ -158,6 +195,9 @@ class URLAB_API UMjComponent : public USceneComponent, public IMjSpecElement
     UFUNCTION(BlueprintCallable, Category = "MuJoCo|Base")
     int GetMjID() const { return m_ID; }
 
+    /** @brief Diagnostic-only accessor for the spec element pointer. Used by binding-path probes. */
+    mjsElement* GetSpecElementForDiagnostics() const { return m_SpecElement; }
+
     /** @brief If true, this component is a template in a <default> block. Auto-resolved in Setup(). */
     UPROPERTY()
     bool bIsDefault = false;
diff --git a/Source/URLab/Public/MuJoCo/Components/Physics/MjContactExclude.h b/Source/URLab/Public/MuJoCo/Components/Physics/MjContactExclude.h
index 6bc92f5..2617a59 100644
--- a/Source/URLab/Public/MuJoCo/Components/Physics/MjContactExclude.h
+++ b/Source/URLab/Public/MuJoCo/Components/Physics/MjContactExclude.h
@@ -44,24 +44,25 @@ class URLAB_API UMjContactExclude : public USceneComponent, public IMjSpecElemen
 
 public:	
     // --- CODEGEN_PROPERTIES_START ---
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjContactExclude", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|ContactExclude", meta=(InlineEditConditionToggle))
     bool bOverride_body1 = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjContactExclude", meta=(EditCondition="bOverride_body1"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|ContactExclude", meta=(EditCondition="bOverride_body1"))
     FString body1 = TEXT("");
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjContactExclude", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|ContactExclude", meta=(InlineEditConditionToggle))
     bool bOverride_body2 = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjContactExclude", meta=(EditCondition="bOverride_body2"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|ContactExclude", meta=(EditCondition="bOverride_body2"))
     FString body2 = TEXT("");
     // --- CODEGEN_PROPERTIES_END ---
 
     /** @brief Default constructor. */
 	UMjContactExclude();
 
-    /** @brief Name of the contact exclusion (optional). */
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Contact Exclude")
+    /** @brief Name of the contact exclusion (optional). Hidden from the Details panel — synced from MjName. */
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Contact Exclude",
+              meta=(EditCondition="false", EditConditionHides))
     FString Name;
 
 
@@ -85,7 +86,8 @@ class URLAB_API UMjContactExclude : public USceneComponent, public IMjSpecElemen
      */
     virtual void RegisterToSpec(class FMujocoSpecWrapper& Wrapper, mjsBody* ParentBody = nullptr) override;
 
-    virtual void Bind(mjModel* model, mjData* data, const FString& Prefix = TEXT("")) override;
+    /** No-op: contact excludes are global static data, no per-instance runtime binding. Required by IMjSpecElement. */
+    virtual void Bind(mjModel* model, mjData* data, const FString& Prefix = TEXT("")) override {}
 
 #if WITH_EDITOR
     UFUNCTION()
diff --git a/Source/URLab/Public/MuJoCo/Components/Physics/MjContactPair.h b/Source/URLab/Public/MuJoCo/Components/Physics/MjContactPair.h
index 580fd4e..6729a15 100644
--- a/Source/URLab/Public/MuJoCo/Components/Physics/MjContactPair.h
+++ b/Source/URLab/Public/MuJoCo/Components/Physics/MjContactPair.h
@@ -44,66 +44,67 @@ class URLAB_API UMjContactPair : public USceneComponent, public IMjSpecElement
 
 public:	
     // --- CODEGEN_PROPERTIES_START ---
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjContactPair", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|ContactPair", meta=(InlineEditConditionToggle))
     bool bOverride_geom1 = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjContactPair", meta=(EditCondition="bOverride_geom1"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|ContactPair", meta=(EditCondition="bOverride_geom1"))
     FString geom1 = TEXT("");
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjContactPair", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|ContactPair", meta=(InlineEditConditionToggle))
     bool bOverride_geom2 = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjContactPair", meta=(EditCondition="bOverride_geom2"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|ContactPair", meta=(EditCondition="bOverride_geom2"))
     FString geom2 = TEXT("");
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjContactPair", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|ContactPair", meta=(InlineEditConditionToggle))
     bool bOverride_condim = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjContactPair", meta=(EditCondition="bOverride_condim"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|ContactPair", meta=(EditCondition="bOverride_condim"))
     int32 condim = 0;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjContactPair", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|ContactPair", meta=(InlineEditConditionToggle))
     bool bOverride_friction = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjContactPair", meta=(EditCondition="bOverride_friction"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|ContactPair", meta=(EditCondition="bOverride_friction"))
     TArray<float> friction = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjContactPair", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|ContactPair", meta=(InlineEditConditionToggle))
     bool bOverride_solref = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjContactPair", meta=(EditCondition="bOverride_solref"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|ContactPair", meta=(EditCondition="bOverride_solref"))
     TArray<float> solref = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjContactPair", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|ContactPair", meta=(InlineEditConditionToggle))
     bool bOverride_solreffriction = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjContactPair", meta=(EditCondition="bOverride_solreffriction"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|ContactPair", meta=(EditCondition="bOverride_solreffriction"))
     TArray<float> solreffriction = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjContactPair", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|ContactPair", meta=(InlineEditConditionToggle))
     bool bOverride_solimp = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjContactPair", meta=(EditCondition="bOverride_solimp"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|ContactPair", meta=(EditCondition="bOverride_solimp"))
     TArray<float> solimp = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjContactPair", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|ContactPair", meta=(InlineEditConditionToggle))
     bool bOverride_gap = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjContactPair", meta=(EditCondition="bOverride_gap"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|ContactPair", meta=(EditCondition="bOverride_gap", Units="m"))
     float gap = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjContactPair", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|ContactPair", meta=(InlineEditConditionToggle))
     bool bOverride_margin = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjContactPair", meta=(EditCondition="bOverride_margin"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|ContactPair", meta=(EditCondition="bOverride_margin", Units="m"))
     float margin = 0.0f;
     // --- CODEGEN_PROPERTIES_END ---
 
     /** @brief Default constructor. */
 	UMjContactPair();
 
-    /** @brief Name of the contact pair (optional). */
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Contact Pair")
+    /** @brief Name of the contact pair (optional). Hidden from the Details panel — synced from MjName. */
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Contact Pair",
+              meta=(EditCondition="false", EditConditionHides))
     FString Name;
 
 
@@ -133,7 +134,8 @@ class URLAB_API UMjContactPair : public USceneComponent, public IMjSpecElement
      */
     virtual void RegisterToSpec(class FMujocoSpecWrapper& Wrapper, mjsBody* ParentBody = nullptr) override;
 
-    virtual void Bind(mjModel* model, mjData* data, const FString& Prefix = TEXT("")) override;
+    /** No-op: contact pairs are global static data, no per-instance runtime binding. Required by IMjSpecElement. */
+    virtual void Bind(mjModel* model, mjData* data, const FString& Prefix = TEXT("")) override {}
 
 #if WITH_EDITOR
     UFUNCTION()
diff --git a/Source/URLab/Public/MuJoCo/Components/Physics/MjInertial.h b/Source/URLab/Public/MuJoCo/Components/Physics/MjInertial.h
index 095dd27..b641446 100644
--- a/Source/URLab/Public/MuJoCo/Components/Physics/MjInertial.h
+++ b/Source/URLab/Public/MuJoCo/Components/Physics/MjInertial.h
@@ -38,34 +38,34 @@ class URLAB_API UMjInertial : public UMjComponent
 
 public:
     // --- CODEGEN_PROPERTIES_START ---
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjInertial|Spatial Pose", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Inertial|Spatial Pose", meta=(InlineEditConditionToggle))
     bool bOverride_Pos = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjInertial|Spatial Pose", meta=(EditCondition="bOverride_Pos"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Inertial|Spatial Pose", meta=(EditCondition="false", EditConditionHides))
     FVector Pos = FVector::ZeroVector;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjInertial|Orientation", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Inertial|Orientation", meta=(InlineEditConditionToggle))
     bool bOverride_Quat = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjInertial|Orientation", meta=(EditCondition="bOverride_Quat"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Inertial|Orientation", meta=(EditCondition="false", EditConditionHides))
     FQuat Quat = FQuat::Identity;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjInertial", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Inertial", meta=(InlineEditConditionToggle))
     bool bOverride_mass = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjInertial", meta=(EditCondition="bOverride_mass"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Inertial", meta=(EditCondition="bOverride_mass"))
     float mass = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjInertial", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Inertial", meta=(InlineEditConditionToggle))
     bool bOverride_diaginertia = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjInertial", meta=(EditCondition="bOverride_diaginertia"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Inertial", meta=(EditCondition="bOverride_diaginertia"))
     TArray<float> diaginertia = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjInertial", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Inertial", meta=(InlineEditConditionToggle))
     bool bOverride_fullinertia = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjInertial", meta=(EditCondition="bOverride_fullinertia"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Inertial", meta=(EditCondition="bOverride_fullinertia"))
     TArray<float> fullinertia = {};
     // --- CODEGEN_PROPERTIES_END ---
 
@@ -87,12 +87,6 @@ class URLAB_API UMjInertial : public UMjComponent
     // Note: Inertial usually doesn't have a direct 'mjsInertial' struct in mjspec.h same as others,
     // it's often part of body or explicit inertial element. 
     // mjsBody has 'mass', 'inertia', 'fullinertia'.
-    
-    /**
-     * @brief Binds this component to the live MuJoCo simulation.
-     */
-    virtual void Bind(mjModel* model, mjData* data, const FString& Prefix = TEXT("")) override;
-
     /**
      * @brief Registers/Applies this inertial to the parent body in the spec.
      * @param Wrapper The spec wrapper instance.
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjAccelerometer.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjAccelerometer.h
index 8844dd5..a37f099 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjAccelerometer.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjAccelerometer.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjAccelerometer
- * @brief Specific Accelerometer Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'accelerometer' element.
+ *
+ * Subclass of UMjSensor. Sets Type = Accelerometer in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjAccelerometer : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjAccelerometer();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjActuatorFrcSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjActuatorFrcSensor.h
index 37bdc00..b9bd325 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjActuatorFrcSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjActuatorFrcSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjActuatorFrcSensor
- * @brief Specific ActuatorFrc Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'actuatorfrc' element.
+ *
+ * Subclass of UMjSensor. Sets Type = ActuatorFrc in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjActuatorFrcSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjActuatorFrcSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjActuatorPosSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjActuatorPosSensor.h
index e40c6b8..53985a5 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjActuatorPosSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjActuatorPosSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjActuatorPosSensor
- * @brief Specific ActuatorPos Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'actuatorpos' element.
+ *
+ * Subclass of UMjSensor. Sets Type = ActuatorPos in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjActuatorPosSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjActuatorPosSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjActuatorVelSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjActuatorVelSensor.h
index af8e89b..e199943 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjActuatorVelSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjActuatorVelSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjActuatorVelSensor
- * @brief Specific ActuatorVel Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'actuatorvel' element.
+ *
+ * Subclass of UMjSensor. Sets Type = ActuatorVel in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjActuatorVelSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjActuatorVelSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjBallAngVelSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjBallAngVelSensor.h
index 9951314..9085250 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjBallAngVelSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjBallAngVelSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjBallAngVelSensor
- * @brief Specific BallAngVel Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'ballangvel' element.
+ *
+ * Subclass of UMjSensor. Sets Type = BallAngVel in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjBallAngVelSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjBallAngVelSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjBallQuatSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjBallQuatSensor.h
index e116d94..e11918d 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjBallQuatSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjBallQuatSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjBallQuatSensor
- * @brief Specific BallQuat Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'ballquat' element.
+ *
+ * Subclass of UMjSensor. Sets Type = BallQuat in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjBallQuatSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjBallQuatSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjCamProjectionSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjCamProjectionSensor.h
index 29c3003..7c41bd5 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjCamProjectionSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjCamProjectionSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjCamProjectionSensor
- * @brief Specific CamProjection Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'camprojection' element.
+ *
+ * Subclass of UMjSensor. Sets Type = CamProjection in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjCamProjectionSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjCamProjectionSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjCamera.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjCamera.h
index 6d6830f..ca26b00 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjCamera.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjCamera.h
@@ -86,76 +86,76 @@ class URLAB_API UMjCamera : public UMjComponent
 
 public:
     // --- CODEGEN_PROPERTIES_START ---
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjCamera|Spatial Pose", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Camera|Spatial Pose", meta=(InlineEditConditionToggle))
     bool bOverride_Pos = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjCamera|Spatial Pose", meta=(EditCondition="bOverride_Pos"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Camera|Spatial Pose", meta=(EditCondition="false", EditConditionHides))
     FVector Pos = FVector::ZeroVector;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjCamera|Orientation", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Camera|Orientation", meta=(InlineEditConditionToggle))
     bool bOverride_Quat = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjCamera|Orientation", meta=(EditCondition="bOverride_Quat"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Camera|Orientation", meta=(EditCondition="false", EditConditionHides))
     FQuat Quat = FQuat::Identity;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjCamera", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Camera", meta=(InlineEditConditionToggle))
     bool bOverride_fovy = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjCamera", meta=(EditCondition="bOverride_fovy"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Camera", meta=(EditCondition="bOverride_fovy", Units="deg"))
     float fovy = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjCamera", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Camera", meta=(InlineEditConditionToggle))
     bool bOverride_ipd = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjCamera", meta=(EditCondition="bOverride_ipd"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Camera", meta=(EditCondition="bOverride_ipd", Units="m"))
     float ipd = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjCamera", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Camera", meta=(InlineEditConditionToggle))
     bool bOverride_resolution = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjCamera", meta=(EditCondition="bOverride_resolution"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Camera", meta=(EditCondition="bOverride_resolution"))
     TArray<int32> resolution = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjCamera", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Camera", meta=(InlineEditConditionToggle))
     bool bOverride_output = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjCamera", meta=(EditCondition="bOverride_output"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Camera", meta=(EditCondition="bOverride_output"))
     float output = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjCamera", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Camera", meta=(InlineEditConditionToggle))
     bool bOverride_target = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjCamera", meta=(EditCondition="bOverride_target"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Camera", meta=(EditCondition="bOverride_target"))
     FString target = TEXT("");
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjCamera", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Camera", meta=(InlineEditConditionToggle))
     bool bOverride_focal = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjCamera", meta=(EditCondition="bOverride_focal"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Camera", meta=(EditCondition="bOverride_focal", MjUnit="m"))
     TArray<float> focal = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjCamera", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Camera", meta=(InlineEditConditionToggle))
     bool bOverride_focalpixel = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjCamera", meta=(EditCondition="bOverride_focalpixel"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Camera", meta=(EditCondition="bOverride_focalpixel"))
     TArray<int32> focalpixel = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjCamera", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Camera", meta=(InlineEditConditionToggle))
     bool bOverride_principal = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjCamera", meta=(EditCondition="bOverride_principal"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Camera", meta=(EditCondition="bOverride_principal", MjUnit="m"))
     TArray<float> principal = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjCamera", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Camera", meta=(InlineEditConditionToggle))
     bool bOverride_principalpixel = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjCamera", meta=(EditCondition="bOverride_principalpixel"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Camera", meta=(EditCondition="bOverride_principalpixel"))
     TArray<int32> principalpixel = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjCamera", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Camera", meta=(InlineEditConditionToggle))
     bool bOverride_sensorsize = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjCamera", meta=(EditCondition="bOverride_sensorsize"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Camera", meta=(EditCondition="bOverride_sensorsize", MjUnit="m"))
     TArray<float> sensorsize = {};
     // --- CODEGEN_PROPERTIES_END ---
 
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjCameraTypes.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjCameraTypes.h
index f4f5cd4..7b54b45 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjCameraTypes.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjCameraTypes.h
@@ -20,6 +20,7 @@
 #pragma once
 
 #include "CoreMinimal.h"
+#include "MuJoCo/Generated/MjArticulationEnums.h"
 #include "MjCameraTypes.generated.h"
 
 /**
@@ -39,39 +40,5 @@ enum class EMjCameraMode : uint8
     InstanceSegmentation UMETA(DisplayName = "Instance Segmentation"),
 };
 
-/**
- * @enum EMjCameraTrackingMode
- * @brief MJCF ``<camera mode="...">`` tracking mode. Mirrors MuJoCo's
- *        mjtCamLight subset that applies to cameras. Distinct from
- *        ``EMjCameraMode`` above (which is URLab's render-mode selector,
- *        not a MuJoCo concept).
- *
- *  - Fixed:          camera attached to its body with a fixed transform.
- *  - Track:          camera follows the body's world position, keeps own rotation.
- *  - TrackCom:       like Track but follows the subtree centre of mass.
- *  - TargetBody:     camera stays at its own position; rotates to look at target body.
- *  - TargetBodyCom:  like TargetBody but aims at the target's subtree CoM.
- */
-UENUM(BlueprintType)
-enum class EMjCameraTrackingMode : uint8
-{
-    Fixed         UMETA(DisplayName = "Fixed"),
-    Track         UMETA(DisplayName = "Track"),
-    TrackCom      UMETA(DisplayName = "Track (Centre of Mass)"),
-    TargetBody    UMETA(DisplayName = "Target Body"),
-    TargetBodyCom UMETA(DisplayName = "Target Body (Centre of Mass)"),
-};
-
-/**
- * @enum EMjCameraProjection
- * @brief MJCF ``<camera projection="...">``. Mirrors MuJoCo's mjtProjection.
- *
- *  - Perspective:  standard pinhole camera (default).
- *  - Orthographic: parallel projection, no perspective foreshortening.
- */
-UENUM(BlueprintType)
-enum class EMjCameraProjection : uint8
-{
-    Perspective  UMETA(DisplayName = "Perspective"),
-    Orthographic UMETA(DisplayName = "Orthographic"),
-};
+// EMjCameraTrackingMode, EMjCameraProjection moved to
+// MuJoCo/Generated/MjArticulationEnums.h.
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjClockSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjClockSensor.h
index b399abc..2c610c6 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjClockSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjClockSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjClockSensor
- * @brief Specific Clock Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'clock' element.
+ *
+ * Subclass of UMjSensor. Sets Type = Clock in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjClockSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjClockSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjContactSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjContactSensor.h
index fed4c67..68aa046 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjContactSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjContactSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjContactSensor
- * @brief Specific Contact Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'contact' element.
+ *
+ * Subclass of UMjSensor. Sets Type = Contact in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjContactSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjContactSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjEKineticSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjEKineticSensor.h
index 00b4fdd..9854712 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjEKineticSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjEKineticSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjEKineticSensor
- * @brief Specific EKinetic Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'e_kinetic' element.
+ *
+ * Subclass of UMjSensor. Sets Type = EKinetic in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjEKineticSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjEKineticSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjEPotentialSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjEPotentialSensor.h
index 7376469..7f244d1 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjEPotentialSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjEPotentialSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjEPotentialSensor
- * @brief Specific EPotential Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'e_potential' element.
+ *
+ * Subclass of UMjSensor. Sets Type = EPotential in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjEPotentialSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjEPotentialSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjForceSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjForceSensor.h
index 00d3807..1b106b6 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjForceSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjForceSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjForceSensor
- * @brief Specific Force Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'force' element.
+ *
+ * Subclass of UMjSensor. Sets Type = Force in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjForceSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjForceSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjFrameAngAccSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjFrameAngAccSensor.h
index 4ddbe0f..4f4f01f 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjFrameAngAccSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjFrameAngAccSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjFrameAngAccSensor
- * @brief Specific FrameAngAcc Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'frameangacc' element.
+ *
+ * Subclass of UMjSensor. Sets Type = FrameAngAcc in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjFrameAngAccSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjFrameAngAccSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjFrameAngVelSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjFrameAngVelSensor.h
index 2047e11..d58259a 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjFrameAngVelSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjFrameAngVelSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjFrameAngVelSensor
- * @brief Specific FrameAngVel Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'frameangvel' element.
+ *
+ * Subclass of UMjSensor. Sets Type = FrameAngVel in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjFrameAngVelSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjFrameAngVelSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjFrameLinAccSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjFrameLinAccSensor.h
index 1d969dc..cbf6e4a 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjFrameLinAccSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjFrameLinAccSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjFrameLinAccSensor
- * @brief Specific FrameLinAcc Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'framelinacc' element.
+ *
+ * Subclass of UMjSensor. Sets Type = FrameLinAcc in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjFrameLinAccSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjFrameLinAccSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjFrameLinVelSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjFrameLinVelSensor.h
index c8d71ad..1bd0994 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjFrameLinVelSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjFrameLinVelSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjFrameLinVelSensor
- * @brief Specific FrameLinVel Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'framelinvel' element.
+ *
+ * Subclass of UMjSensor. Sets Type = FrameLinVel in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjFrameLinVelSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjFrameLinVelSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjFramePosSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjFramePosSensor.h
index 78a21cf..27d8640 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjFramePosSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjFramePosSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjFramePosSensor
- * @brief Specific FramePos Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'framepos' element.
+ *
+ * Subclass of UMjSensor. Sets Type = FramePos in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjFramePosSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjFramePosSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjFrameQuatSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjFrameQuatSensor.h
index f80797f..f6d43f6 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjFrameQuatSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjFrameQuatSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjFrameQuatSensor
- * @brief Specific FrameQuat Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'framequat' element.
+ *
+ * Subclass of UMjSensor. Sets Type = FrameQuat in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjFrameQuatSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjFrameQuatSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjFrameXAxisSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjFrameXAxisSensor.h
index 0891c29..27caf41 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjFrameXAxisSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjFrameXAxisSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjFrameXAxisSensor
- * @brief Specific FrameXAxis Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'framexaxis' element.
+ *
+ * Subclass of UMjSensor. Sets Type = FrameXAxis in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjFrameXAxisSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjFrameXAxisSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjFrameYAxisSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjFrameYAxisSensor.h
index a58d758..1fa5ee4 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjFrameYAxisSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjFrameYAxisSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjFrameYAxisSensor
- * @brief Specific FrameYAxis Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'frameyaxis' element.
+ *
+ * Subclass of UMjSensor. Sets Type = FrameYAxis in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjFrameYAxisSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjFrameYAxisSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjFrameZAxisSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjFrameZAxisSensor.h
index 8508c3c..14f797b 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjFrameZAxisSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjFrameZAxisSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjFrameZAxisSensor
- * @brief Specific FrameZAxis Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'framezaxis' element.
+ *
+ * Subclass of UMjSensor. Sets Type = FrameZAxis in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjFrameZAxisSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjFrameZAxisSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjGeomDistSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjGeomDistSensor.h
index 58c4457..fb8064d 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjGeomDistSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjGeomDistSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjGeomDistSensor
- * @brief Specific GeomDist Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'distance' element.
+ *
+ * Subclass of UMjSensor. Sets Type = GeomDist in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjGeomDistSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjGeomDistSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjGeomFromToSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjGeomFromToSensor.h
index 04046f0..951bfa6 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjGeomFromToSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjGeomFromToSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjGeomFromToSensor
- * @brief Specific GeomFromTo Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'fromto' element.
+ *
+ * Subclass of UMjSensor. Sets Type = GeomFromTo in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjGeomFromToSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjGeomFromToSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjGeomNormalSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjGeomNormalSensor.h
index 3c9f132..f55857f 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjGeomNormalSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjGeomNormalSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjGeomNormalSensor
- * @brief Specific GeomNormal Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'normal' element.
+ *
+ * Subclass of UMjSensor. Sets Type = GeomNormal in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjGeomNormalSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjGeomNormalSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjGyro.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjGyro.h
index 5481a95..43ccb21 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjGyro.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjGyro.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjGyro
- * @brief Specific Gyro Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'gyro' element.
+ *
+ * Subclass of UMjSensor. Sets Type = Gyro in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjGyro : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjGyro();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjInsideSiteSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjInsideSiteSensor.h
index a4a7c0e..ad8b774 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjInsideSiteSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjInsideSiteSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjInsideSiteSensor
- * @brief Specific InsideSite Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'insidesite' element.
+ *
+ * Subclass of UMjSensor. Sets Type = InsideSite in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjInsideSiteSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjInsideSiteSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjJointActFrcSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjJointActFrcSensor.h
index bc6cfcd..9e3cab8 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjJointActFrcSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjJointActFrcSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjJointActFrcSensor
- * @brief Specific JointActFrc Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'jointactuatorfrc' element.
+ *
+ * Subclass of UMjSensor. Sets Type = JointActFrc in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjJointActFrcSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjJointActFrcSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjJointLimitFrcSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjJointLimitFrcSensor.h
index 885e010..d885cb4 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjJointLimitFrcSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjJointLimitFrcSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjJointLimitFrcSensor
- * @brief Specific JointLimitFrc Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'jointlimitfrc' element.
+ *
+ * Subclass of UMjSensor. Sets Type = JointLimitFrc in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjJointLimitFrcSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjJointLimitFrcSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjJointLimitPosSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjJointLimitPosSensor.h
index 873bf63..5bfe8f5 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjJointLimitPosSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjJointLimitPosSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjJointLimitPosSensor
- * @brief Specific JointLimitPos Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'jointlimitpos' element.
+ *
+ * Subclass of UMjSensor. Sets Type = JointLimitPos in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjJointLimitPosSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjJointLimitPosSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjJointLimitVelSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjJointLimitVelSensor.h
index 3dafbbc..5091c92 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjJointLimitVelSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjJointLimitVelSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjJointLimitVelSensor
- * @brief Specific JointLimitVel Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'jointlimitvel' element.
+ *
+ * Subclass of UMjSensor. Sets Type = JointLimitVel in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjJointLimitVelSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjJointLimitVelSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjJointPosSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjJointPosSensor.h
index 613b657..0ce1cc7 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjJointPosSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjJointPosSensor.h
@@ -36,7 +36,6 @@ class URLAB_API UMjJointPosSensor : public UMjSensor
     GENERATED_BODY()
 public:
     // --- CODEGEN_PROPERTIES_START ---
-
     // --- CODEGEN_PROPERTIES_END ---
 
     UMjJointPosSensor();
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjJointVelSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjJointVelSensor.h
index 28eedb4..b1f5df1 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjJointVelSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjJointVelSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjJointVelSensor
- * @brief Specific JointVel Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'jointvel' element.
+ *
+ * Subclass of UMjSensor. Sets Type = JointVel in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjJointVelSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjJointVelSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjMagnetometer.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjMagnetometer.h
index a571e9b..0af7e9a 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjMagnetometer.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjMagnetometer.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjMagnetometer
- * @brief Specific Magnetometer Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'magnetometer' element.
+ *
+ * Subclass of UMjSensor. Sets Type = Magnetometer in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjMagnetometer : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjMagnetometer();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjPluginSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjPluginSensor.h
index 7f06590..7509f0e 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjPluginSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjPluginSensor.h
@@ -44,7 +44,7 @@ class URLAB_API UMjPluginSensor : public UMjSensor
     UMjPluginSensor();
 
     // --- CODEGEN_PROPERTIES_START ---
-
+    // (no type-specific schema attrs)
     // --- CODEGEN_PROPERTIES_END ---
 
     virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjRangeFinderSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjRangeFinderSensor.h
index 736fe41..14b333b 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjRangeFinderSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjRangeFinderSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjRangeFinderSensor
- * @brief Specific RangeFinder Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'rangefinder' element.
+ *
+ * Subclass of UMjSensor. Sets Type = RangeFinder in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjRangeFinderSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjRangeFinderSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjSensor.h
index f6e1798..387e07c 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjSensor.h
@@ -29,6 +29,7 @@
 #include "MuJoCo/Core/Spec/MjSpecElement.h"
 #include "MuJoCo/Components/MjComponent.h"
 #include "MuJoCo/Components/Defaults/MjDefault.h"
+#include "MuJoCo/Generated/MjArticulationEnums.h"
 #include "MuJoCo/Utils/MjBind.h"
 #include "MuJoCo/Utils/MjOrientationUtils.h"
 #include "MjSensor.generated.h"
@@ -112,38 +113,7 @@ enum class EMjSensorType : uint8
     User
 };
 
-/**
- * @enum EMjObjType
- * @brief Mirror of mjtObj for specifying sensor attachment/reference types.
- */
-UENUM(BlueprintType)
-enum class EMjObjType : uint8
-{
-    Unknown,
-    Body,
-    XBody,
-    Joint,
-    DoF,
-    Geom,
-    Site,
-    Camera,
-    Light,
-    Mesh,
-    HField,
-    Texture,
-    Material,
-    Pair,
-    Exclude,
-    Equality,
-    Tendon,
-    Actuator,
-    Sensor,
-    Numeric,
-    Text,
-    Tuple,
-    Key,
-    Plugin,
-};
+// EMjObjType moved to MuJoCo/Generated/MjArticulationEnums.h.
 
 /**
  * @class UMjSensor
@@ -159,40 +129,40 @@ class URLAB_API UMjSensor : public UMjComponent
 
 public:	
     // --- CODEGEN_PROPERTIES_START ---
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjSensor", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Sensor", meta=(InlineEditConditionToggle))
     bool bOverride_nsample = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjSensor", meta=(EditCondition="bOverride_nsample"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Sensor", meta=(EditCondition="bOverride_nsample"))
     int32 nsample = 0;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjSensor", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Sensor", meta=(InlineEditConditionToggle))
     bool bOverride_interp = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjSensor", meta=(EditCondition="bOverride_interp"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Sensor", meta=(EditCondition="bOverride_interp"))
     int32 interp = 0;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjSensor", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Sensor", meta=(InlineEditConditionToggle))
     bool bOverride_delay = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjSensor", meta=(EditCondition="bOverride_delay"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Sensor", meta=(EditCondition="bOverride_delay"))
     float delay = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjSensor", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Sensor", meta=(InlineEditConditionToggle))
     bool bOverride_interval = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjSensor", meta=(EditCondition="bOverride_interval"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Sensor", meta=(EditCondition="bOverride_interval"))
     TArray<float> interval = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjSensor", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Sensor", meta=(InlineEditConditionToggle))
     bool bOverride_cutoff = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjSensor", meta=(EditCondition="bOverride_cutoff"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Sensor", meta=(EditCondition="bOverride_cutoff"))
     float cutoff = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjSensor", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Sensor", meta=(InlineEditConditionToggle))
     bool bOverride_noise = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjSensor", meta=(EditCondition="bOverride_noise"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Sensor", meta=(EditCondition="bOverride_noise"))
     float noise = 0.0f;
     // --- CODEGEN_PROPERTIES_END ---
 
@@ -226,8 +196,10 @@ class URLAB_API UMjSensor : public UMjComponent
     UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Sensor", meta=(GetOptions="GetDefaultClassOptions"))
     FString MjClassName;
 
-    /** @brief Reference to a UMjDefault component for default class inheritance. */
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Sensor")
+    /** @brief Reference to a UMjDefault component for default class inheritance. Hidden from the
+     *  Details panel — synced from MjClassName via the editor dropdown. */
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Sensor",
+              meta=(EditCondition="false", EditConditionHides))
     UMjDefault* DefaultClass = nullptr;
 
     virtual FString GetMjClassName() const override
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjSubtreeAngMomSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjSubtreeAngMomSensor.h
index 10ad220..c76de53 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjSubtreeAngMomSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjSubtreeAngMomSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjSubtreeAngMomSensor
- * @brief Specific SubtreeAngMom Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'subtreeangmom' element.
+ *
+ * Subclass of UMjSensor. Sets Type = SubtreeAngMom in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjSubtreeAngMomSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjSubtreeAngMomSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjSubtreeComSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjSubtreeComSensor.h
index 0416196..cdc8ff1 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjSubtreeComSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjSubtreeComSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjSubtreeComSensor
- * @brief Specific SubtreeCom Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'subtreecom' element.
+ *
+ * Subclass of UMjSensor. Sets Type = SubtreeCom in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjSubtreeComSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjSubtreeComSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjSubtreeLinVelSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjSubtreeLinVelSensor.h
index 8fff017..d777b61 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjSubtreeLinVelSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjSubtreeLinVelSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjSubtreeLinVelSensor
- * @brief Specific SubtreeLinVel Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'subtreelinvel' element.
+ *
+ * Subclass of UMjSensor. Sets Type = SubtreeLinVel in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjSubtreeLinVelSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjSubtreeLinVelSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjTactileSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjTactileSensor.h
index 9189c4c..e1b69cf 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjTactileSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjTactileSensor.h
@@ -36,7 +36,6 @@ class URLAB_API UMjTactileSensor : public UMjSensor
     GENERATED_BODY()
 public:
     // --- CODEGEN_PROPERTIES_START ---
-
     // --- CODEGEN_PROPERTIES_END ---
 
     UMjTactileSensor();
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjTendonActFrcSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjTendonActFrcSensor.h
index 64672ab..be2ea3d 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjTendonActFrcSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjTendonActFrcSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjTendonActFrcSensor
- * @brief Specific TendonActFrc Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'tendonactuatorfrc' element.
+ *
+ * Subclass of UMjSensor. Sets Type = TendonActFrc in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjTendonActFrcSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjTendonActFrcSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjTendonLimitFrcSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjTendonLimitFrcSensor.h
index 1e39b2d..7b701ca 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjTendonLimitFrcSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjTendonLimitFrcSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjTendonLimitFrcSensor
- * @brief Specific TendonLimitFrc Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'tendonlimitfrc' element.
+ *
+ * Subclass of UMjSensor. Sets Type = TendonLimitFrc in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjTendonLimitFrcSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjTendonLimitFrcSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjTendonLimitPosSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjTendonLimitPosSensor.h
index e9d8ed1..31bebc3 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjTendonLimitPosSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjTendonLimitPosSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjTendonLimitPosSensor
- * @brief Specific TendonLimitPos Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'tendonlimitpos' element.
+ *
+ * Subclass of UMjSensor. Sets Type = TendonLimitPos in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjTendonLimitPosSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjTendonLimitPosSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjTendonLimitVelSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjTendonLimitVelSensor.h
index 735495d..6858e73 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjTendonLimitVelSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjTendonLimitVelSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjTendonLimitVelSensor
- * @brief Specific TendonLimitVel Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'tendonlimitvel' element.
+ *
+ * Subclass of UMjSensor. Sets Type = TendonLimitVel in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjTendonLimitVelSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjTendonLimitVelSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjTendonPosSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjTendonPosSensor.h
index b26c9c0..9b1bf45 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjTendonPosSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjTendonPosSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjTendonPosSensor
- * @brief Specific TendonPos Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'tendonpos' element.
+ *
+ * Subclass of UMjSensor. Sets Type = TendonPos in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjTendonPosSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjTendonPosSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjTendonVelSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjTendonVelSensor.h
index f115862..9587c1c 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjTendonVelSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjTendonVelSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjTendonVelSensor
- * @brief Specific TendonVel Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'tendonvel' element.
+ *
+ * Subclass of UMjSensor. Sets Type = TendonVel in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjTendonVelSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjTendonVelSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjTorqueSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjTorqueSensor.h
index 5aa7034..24d3a50 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjTorqueSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjTorqueSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjTorqueSensor
- * @brief Specific Torque Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'torque' element.
+ *
+ * Subclass of UMjSensor. Sets Type = Torque in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjTorqueSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjTorqueSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjTouchSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjTouchSensor.h
index dd9a3e8..0ec70cc 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjTouchSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjTouchSensor.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjTouchSensor
- * @brief Specific Touch Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'touch' element.
+ *
+ * Subclass of UMjSensor. Sets Type = Touch in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjTouchSensor : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjTouchSensor();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjUserSensor.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjUserSensor.h
index 9984711..0c1a06c 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjUserSensor.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjUserSensor.h
@@ -44,7 +44,7 @@ class URLAB_API UMjUserSensor : public UMjSensor
     UMjUserSensor();
 
     // --- CODEGEN_PROPERTIES_START ---
-
+    // (no type-specific schema attrs)
     // --- CODEGEN_PROPERTIES_END ---
 
     virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjVelocimeter.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjVelocimeter.h
index 0db16dc..2c15c44 100644
--- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjVelocimeter.h
+++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjVelocimeter.h
@@ -13,12 +13,15 @@
 // limitations under the License.
 //
 // --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
 // trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
 //
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
 
 #pragma once
 
@@ -28,20 +31,22 @@
 
 /**
  * @class UMjVelocimeter
- * @brief Specific Velocimeter Sensor component.
+ * @brief Autogenerated component representing the MuJoCo 'velocimeter' element.
+ *
+ * Subclass of UMjSensor. Sets Type = Velocimeter in the constructor.
  */
-UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
+UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
 class URLAB_API UMjVelocimeter : public UMjSensor
 {
     GENERATED_BODY()
-public:
-    // --- CODEGEN_PROPERTIES_START ---
-
-    // --- CODEGEN_PROPERTIES_END ---
 
+public:
     UMjVelocimeter();
 
-    virtual void ImportFromXml(const class FXmlNode* Node, const struct FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
+    // --- CODEGEN_PROPERTIES_START ---
+    // (no type-specific schema attrs)
+    // --- CODEGEN_PROPERTIES_END ---
 
+    virtual void ImportFromXml(const class FXmlNode* Node, const FMjCompilerSettings& CompilerSettings = FMjCompilerSettings{}) override;
     virtual void ExportTo(mjsSensor* Element, mjsDefault* Default = nullptr) override;
 };
diff --git a/Source/URLab/Public/MuJoCo/Components/Tendons/MjTendon.h b/Source/URLab/Public/MuJoCo/Components/Tendons/MjTendon.h
index 1b7a4be..66c0013 100644
--- a/Source/URLab/Public/MuJoCo/Components/Tendons/MjTendon.h
+++ b/Source/URLab/Public/MuJoCo/Components/Tendons/MjTendon.h
@@ -112,112 +112,112 @@ class URLAB_API UMjTendon : public UMjComponent
 
 public:
     // --- CODEGEN_PROPERTIES_START ---
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjTendon", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Tendon", meta=(InlineEditConditionToggle))
     bool bOverride_group = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjTendon", meta=(EditCondition="bOverride_group"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Tendon", meta=(EditCondition="bOverride_group"))
     int32 group = 0;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjTendon", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Tendon", meta=(InlineEditConditionToggle))
     bool bOverride_limited = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjTendon", meta=(EditCondition="bOverride_limited"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Tendon", meta=(EditCondition="bOverride_limited"))
     bool limited = false;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjTendon", meta=(InlineEditConditionToggle))
-    bool bOverride_actuatorfrclimited = false;
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Tendon", meta=(InlineEditConditionToggle))
+    bool bOverride_ActFrcLimited = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjTendon", meta=(EditCondition="bOverride_actuatorfrclimited"))
-    bool actuatorfrclimited = false;
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Tendon", meta=(EditCondition="bOverride_ActFrcLimited"))
+    bool ActFrcLimited = false;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjTendon", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Tendon", meta=(InlineEditConditionToggle))
     bool bOverride_range = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjTendon", meta=(EditCondition="bOverride_range"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Tendon", meta=(EditCondition="bOverride_range"))
     TArray<float> range = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjTendon", meta=(InlineEditConditionToggle))
-    bool bOverride_actuatorfrcrange = false;
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Tendon", meta=(InlineEditConditionToggle))
+    bool bOverride_ActFrcRange = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjTendon", meta=(EditCondition="bOverride_actuatorfrcrange"))
-    TArray<float> actuatorfrcrange = {};
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Tendon", meta=(EditCondition="bOverride_ActFrcRange"))
+    TArray<float> ActFrcRange = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjTendon", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Tendon", meta=(InlineEditConditionToggle))
     bool bOverride_solreflimit = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjTendon", meta=(EditCondition="bOverride_solreflimit"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Tendon", meta=(EditCondition="bOverride_solreflimit"))
     TArray<float> solreflimit = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjTendon", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Tendon", meta=(InlineEditConditionToggle))
     bool bOverride_solimplimit = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjTendon", meta=(EditCondition="bOverride_solimplimit"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Tendon", meta=(EditCondition="bOverride_solimplimit"))
     TArray<float> solimplimit = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjTendon", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Tendon", meta=(InlineEditConditionToggle))
     bool bOverride_solreffriction = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjTendon", meta=(EditCondition="bOverride_solreffriction"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Tendon", meta=(EditCondition="bOverride_solreffriction"))
     TArray<float> solreffriction = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjTendon", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Tendon", meta=(InlineEditConditionToggle))
     bool bOverride_solimpfriction = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjTendon", meta=(EditCondition="bOverride_solimpfriction"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Tendon", meta=(EditCondition="bOverride_solimpfriction"))
     TArray<float> solimpfriction = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjTendon", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Tendon", meta=(InlineEditConditionToggle))
     bool bOverride_frictionloss = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjTendon", meta=(EditCondition="bOverride_frictionloss"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Tendon", meta=(EditCondition="bOverride_frictionloss"))
     float frictionloss = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjTendon", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Tendon", meta=(InlineEditConditionToggle))
     bool bOverride_springlength = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjTendon", meta=(EditCondition="bOverride_springlength"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Tendon", meta=(EditCondition="bOverride_springlength", MjUnit="m"))
     TArray<float> springlength = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjTendon", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Tendon", meta=(InlineEditConditionToggle))
     bool bOverride_width = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjTendon", meta=(EditCondition="bOverride_width"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Tendon", meta=(EditCondition="bOverride_width"))
     float width = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjTendon", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Tendon", meta=(InlineEditConditionToggle))
     bool bOverride_material = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjTendon", meta=(EditCondition="bOverride_material"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Tendon", meta=(EditCondition="bOverride_material"))
     FString material = TEXT("");
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjTendon", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Tendon", meta=(InlineEditConditionToggle))
     bool bOverride_margin = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjTendon", meta=(EditCondition="bOverride_margin"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Tendon", meta=(EditCondition="bOverride_margin"))
     float margin = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjTendon", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Tendon", meta=(InlineEditConditionToggle))
     bool bOverride_stiffness = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjTendon", meta=(EditCondition="bOverride_stiffness"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Tendon", meta=(EditCondition="bOverride_stiffness"))
     TArray<float> stiffness = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjTendon", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Tendon", meta=(InlineEditConditionToggle))
     bool bOverride_damping = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjTendon", meta=(EditCondition="bOverride_damping"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Tendon", meta=(EditCondition="bOverride_damping"))
     TArray<float> damping = {};
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjTendon", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Tendon", meta=(InlineEditConditionToggle))
     bool bOverride_armature = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjTendon", meta=(EditCondition="bOverride_armature"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Tendon", meta=(EditCondition="bOverride_armature"))
     float armature = 0.0f;
 
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjTendon", meta=(InlineEditConditionToggle))
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Tendon", meta=(InlineEditConditionToggle))
     bool bOverride_rgba = false;
 
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjTendon", meta=(EditCondition="bOverride_rgba"))
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Tendon", meta=(EditCondition="bOverride_rgba"))
     FLinearColor rgba = FLinearColor::White;
     // --- CODEGEN_PROPERTIES_END ---
 
@@ -290,8 +290,10 @@ class URLAB_API UMjTendon : public UMjComponent
     UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Tendon", meta=(GetOptions="GetDefaultClassOptions"))
     FString MjClassName;
 
-    /** @brief Reference to a UMjDefault component for default class inheritance. */
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Tendon")
+    /** @brief Reference to a UMjDefault component for default class inheritance. Hidden from the
+     *  Details panel — synced from MjClassName via the editor dropdown. */
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Tendon",
+              meta=(EditCondition="false", EditConditionHides))
     UMjDefault* DefaultClass = nullptr;
 
     virtual FString GetMjClassName() const override
@@ -328,23 +330,10 @@ class URLAB_API UMjTendon : public UMjComponent
 
 
 
-    // --- Actuator Limits ---
-
-    /** @brief Override toggle for bActFrcLimited. */
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|Tendon|Limits", meta=(InlineEditConditionToggle))
-    bool bOverride_ActFrcLimited = false;
-
-    /** @brief Whether the tendon has actuator force limits. */
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Tendon|Limits", meta=(EditCondition="bOverride_ActFrcLimited"))
-    bool bActFrcLimited = false;
-
-    /** @brief Override toggle for ActFrcRange. */
-    UPROPERTY(EditAnywhere, Category = "MuJoCo|Tendon|Limits", meta=(InlineEditConditionToggle))
-    bool bOverride_ActFrcRange = false;
-
-    /** @brief Tendon actuator force limits [min, max]. */
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Tendon|Limits", meta=(EditCondition="bOverride_ActFrcRange"))
-    TArray<float> ActFrcRange = {0.0f, 0.0f};
+    // ActFrcLimited / ActFrcRange UPROPERTYs are codegen-emitted in the
+    // CODEGEN_PROPERTIES block above (driven by codegen_rules.json's
+    // property_renames: actuatorfrclimited -> ActFrcLimited / actuatorfrcrange
+    // -> ActFrcRange).
 
     // --- Solver Parameters ---
 
diff --git a/Source/URLab/Public/MuJoCo/Core/MjArticulation.h b/Source/URLab/Public/MuJoCo/Core/MjArticulation.h
index 42a5e3d..cddf57b 100644
--- a/Source/URLab/Public/MuJoCo/Core/MjArticulation.h
+++ b/Source/URLab/Public/MuJoCo/Core/MjArticulation.h
@@ -25,7 +25,7 @@
 #include "CoreMinimal.h"
 #include <mujoco/mjspec.h>
 #include "MuJoCo/Core/Spec/MjSpecWrapper.h"
-#include "MuJoCo/Core/MjSimOptions.h"
+#include "MuJoCo/Generated/MjOptionGenerated.h"
 #include "GameFramework/Pawn.h"
 #include "MuJoCo/Components/Bodies/MjBody.h"
 #include "MuJoCo/Components/Joints/MjJoint.h"
@@ -207,8 +207,8 @@ class URLAB_API AMjArticulation : public APawn
     UFUNCTION(BlueprintCallable, Category = "MuJoCo|Discovery")
     TArray<USceneComponent*> GetRuntimeComponentsOfClass(TSubclassOf<USceneComponent> ComponentClass) const;
 
-    /** 
-     * @brief Templated helper to find all components of type T belonging to this articulation 
+    /**
+     * @brief Templated helper to find all components of type T belonging to this articulation
      * that are NOT marked as 'bIsDefault'. This is the standard way to find simulation-active components.
      */
     template<typename T>
@@ -231,6 +231,51 @@ class URLAB_API AMjArticulation : public APawn
         }
     }
 
+    // ============================================================================
+    // Templated map-lookup helpers — collapse the 9 hand-rolled `Get<X>Names` /
+    // `Get<X>s` / `Get<X>ByMjId` methods into 1-line BP wrappers per category.
+    // Equality and Keyframe getters do NOT use these (name-keyed maps + dedup +
+    // different name-source) — they stay hand-rolled. See deviation D-future.
+    // ============================================================================
+
+    /** Return all values from an id-keyed component map. */
+    template <typename T>
+    TArray<T*> GetComponentsFromMap(const TMap<int, T*>& IdMap) const
+    {
+        TArray<T*> Out;
+        IdMap.GenerateValueArray(Out);
+        return Out;
+    }
+
+    /** Return MuJoCo names of all components in an id-keyed map (skipping null values). */
+    template <typename T>
+    TArray<FString> GetComponentNamesFromMap(const TMap<int, T*>& IdMap) const
+    {
+        TArray<FString> Names;
+        Names.Reserve(IdMap.Num());
+        for (const auto& Pair : IdMap)
+        {
+            if (Pair.Value) Names.Add(Pair.Value->GetMjName());
+        }
+        return Names;
+    }
+
+    /** Lookup a component by its compiled mjId in an id-keyed map. Returns nullptr if absent. */
+    template <typename T>
+    T* GetComponentByIdInMap(int Id, const TMap<int, T*>& IdMap) const
+    {
+        if (T* const* Found = IdMap.Find(Id)) return *Found;
+        return nullptr;
+    }
+
+    /** Lookup a component by name in a name-keyed map. Returns nullptr if absent. */
+    template <typename T>
+    T* GetComponentByNameInMap(const FString& Name, const TMap<FString, T*>& NameMap) const
+    {
+        if (T* const* Found = NameMap.Find(Name)) return *Found;
+        return nullptr;
+    }
+
     // =========================================================================
     // Blueprint Runtime API — Component Accessors (delegate to the component)
     // =========================================================================
@@ -370,6 +415,9 @@ class URLAB_API AMjArticulation : public APawn
     UPROPERTY(BlueprintReadOnly, Category = "MuJoCo|Status")
     bool bAttachFailed = false;
 
+    /** @brief Diagnostic-only accessor for the articulation prefix used by mjs_attach. */
+    FString GetPrefixForDiagnostics() const { return m_prefix; }
+
 protected:
     mjVFS* m_vfs = nullptr;
     mjSpec* m_spec = nullptr;
@@ -417,7 +465,7 @@ class URLAB_API AMjArticulation : public APawn
      * Parsed from the MJCF <option> element during import.
      */
     UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Options")
-    FMuJoCoOptions SimOptions;
+    FMjOptionGenerated SimOptions;
     
     /**
      * @brief Initializes the articulation, adding its components to the provided mjSpec.
diff --git a/Source/URLab/Public/MuJoCo/Core/MjPhysicsEngine.h b/Source/URLab/Public/MuJoCo/Core/MjPhysicsEngine.h
index 62445d3..8cf2151 100644
--- a/Source/URLab/Public/MuJoCo/Core/MjPhysicsEngine.h
+++ b/Source/URLab/Public/MuJoCo/Core/MjPhysicsEngine.h
@@ -25,7 +25,7 @@
 #include "CoreMinimal.h"
 #include "Components/ActorComponent.h"
 #include "mujoco/mujoco.h"
-#include "MuJoCo/Core/MjSimOptions.h"
+#include "MuJoCo/Generated/MjOptionGenerated.h"
 #include <functional>
 #include <atomic>
 #include "MjPhysicsEngine.generated.h"
@@ -124,7 +124,7 @@ class URLAB_API UMjPhysicsEngine : public UActorComponent
 
     /** @brief Simulation Options (Timestep, Gravity, etc.). */
     UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Options")
-    FMuJoCoOptions Options;
+    FMjOptionGenerated Options;
 
     /** @brief Simulation speed percentage (0-100). 100 = realtime, 50 = half speed. */
     UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Options", meta=(ClampMin="5", ClampMax="100"))
diff --git a/Source/URLab/Public/MuJoCo/Core/MjSimOptions.h b/Source/URLab/Public/MuJoCo/Core/MjSimOptions.h
deleted file mode 100644
index 56b0f08..0000000
--- a/Source/URLab/Public/MuJoCo/Core/MjSimOptions.h
+++ /dev/null
@@ -1,214 +0,0 @@
-// Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
-//
-// 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.
-//
-// --- LEGAL DISCLAIMER ---
-// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
-// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
-// trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
-//
-// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
-// CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
-
-#pragma once
-
-#include "CoreMinimal.h"
-#include "mujoco/mujoco.h"
-#include "MjSimOptions.generated.h"
-
-UENUM(BlueprintType)
-enum class EMjIntegrator : uint8
-{
-    Euler        = 0,
-    RK4          = 1,
-    Implicit     = 2,
-    ImplicitFast = 3
-};
-
-UENUM(BlueprintType)
-enum class EMjCone : uint8
-{
-    Pyramidal = 0,
-    Elliptic  = 1
-};
-
-UENUM(BlueprintType)
-enum class EMjSolver : uint8
-{
-    PGS    = 0,
-    CG     = 1,
-    Newton = 2
-};
-
-/**
- * @struct FMuJoCoOptions
- * @brief MuJoCo simulation options (mjOption).
- *
- * Used in two contexts:
- * - On AMjArticulation: ALL fields are written to the child spec's option.
- *   The bOverride_* toggles are ignored (all values apply).
- * - On AAMjManager: acts as post-compile overrides on m_model->opt.
- *   Only fields with bOverride_* = true are applied; others keep the
- *   compiled model's values (from the articulation specs).
- */
-USTRUCT(BlueprintType)
-struct URLAB_API FMuJoCoOptions
-{
-    GENERATED_BODY()
-
-    // --- Timing ---
-
-    UPROPERTY(EditAnywhere, Category = "MuJoCo Options", meta=(InlineEditConditionToggle))
-    bool bOverride_Timestep = false;
-
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo Options", meta=(EditCondition="bOverride_Timestep"))
-    float Timestep = 0.002f;
-
-    UPROPERTY(EditAnywhere, Category = "MuJoCo Options", meta=(InlineEditConditionToggle))
-    bool bOverride_MemoryMB = false;
-
-    /** @brief MuJoCo spec-level arena size in megabytes. Maps to
-     *  applied pre-compile (the unified arena is sized at mjData creation). */
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo Options",
-        meta=(EditCondition="bOverride_MemoryMB", ClampMin="1"))
-    int32 MemoryMB = 100;
-
-    // --- Physics Environment ---
-
-    UPROPERTY(EditAnywhere, Category = "MuJoCo Options", meta=(InlineEditConditionToggle))
-    bool bOverride_Gravity = false;
-
-    /** @brief Gravity in UE coordinates (cm/s²). Converted to MuJoCo (m/s², Y-negated) at apply time. */
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo Options", meta=(EditCondition="bOverride_Gravity"))
-    FVector Gravity = FVector(0.0f, 0.0f, -981.0f);
-
-    UPROPERTY(EditAnywhere, Category = "MuJoCo Options", meta=(InlineEditConditionToggle))
-    bool bOverride_Wind = false;
-
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo Options", meta=(EditCondition="bOverride_Wind"))
-    FVector Wind = FVector(0.0f, 0.0f, 0.0f);
-
-    UPROPERTY(EditAnywhere, Category = "MuJoCo Options", meta=(InlineEditConditionToggle))
-    bool bOverride_Magnetic = false;
-
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo Options", meta=(EditCondition="bOverride_Magnetic"))
-    FVector Magnetic = FVector(0.0f, -0.5f, 0.0f);
-
-    UPROPERTY(EditAnywhere, Category = "MuJoCo Options", meta=(InlineEditConditionToggle))
-    bool bOverride_Density = false;
-
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo Options", meta=(EditCondition="bOverride_Density"))
-    float Density = 0.0f;
-
-    UPROPERTY(EditAnywhere, Category = "MuJoCo Options", meta=(InlineEditConditionToggle))
-    bool bOverride_Viscosity = false;
-
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo Options", meta=(EditCondition="bOverride_Viscosity"))
-    float Viscosity = 0.0f;
-
-    // --- Solver ---
-
-    UPROPERTY(EditAnywhere, Category = "MuJoCo Options", meta=(InlineEditConditionToggle))
-    bool bOverride_Impratio = false;
-
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo Options", meta=(EditCondition="bOverride_Impratio"))
-    float Impratio = 1.0f;
-
-    UPROPERTY(EditAnywhere, Category = "MuJoCo Options", meta=(InlineEditConditionToggle))
-    bool bOverride_Tolerance = false;
-
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo Options", meta=(EditCondition="bOverride_Tolerance"))
-    float Tolerance = 1e-8f;
-
-    UPROPERTY(EditAnywhere, Category = "MuJoCo Options", meta=(InlineEditConditionToggle))
-    bool bOverride_Iterations = false;
-
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo Options", meta=(EditCondition="bOverride_Iterations"))
-    int Iterations = 100;
-
-    UPROPERTY(EditAnywhere, Category = "MuJoCo Options", meta=(InlineEditConditionToggle))
-    bool bOverride_LsIterations = false;
-
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo Options", meta=(EditCondition="bOverride_LsIterations"))
-    int LsIterations = 50;
-
-    UPROPERTY(EditAnywhere, Category = "MuJoCo Options", meta=(InlineEditConditionToggle))
-    bool bOverride_Integrator = false;
-
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo Options", meta=(EditCondition="bOverride_Integrator"))
-    EMjIntegrator Integrator = EMjIntegrator::Euler;
-
-    UPROPERTY(EditAnywhere, Category = "MuJoCo Options", meta=(InlineEditConditionToggle))
-    bool bOverride_Cone = false;
-
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo Options", meta=(EditCondition="bOverride_Cone"))
-    EMjCone Cone = EMjCone::Pyramidal;
-
-    UPROPERTY(EditAnywhere, Category = "MuJoCo Options", meta=(InlineEditConditionToggle))
-    bool bOverride_Solver = false;
-
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo Options", meta=(EditCondition="bOverride_Solver"))
-    EMjSolver Solver = EMjSolver::Newton;
-
-    UPROPERTY(EditAnywhere, Category = "MuJoCo Options", meta=(InlineEditConditionToggle))
-    bool bOverride_NoslipIterations = false;
-
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo Options", meta=(EditCondition="bOverride_NoslipIterations"))
-    int NoslipIterations = 0;
-
-    UPROPERTY(EditAnywhere, Category = "MuJoCo Options", meta=(InlineEditConditionToggle))
-    bool bOverride_NoslipTolerance = false;
-
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo Options", meta=(EditCondition="bOverride_NoslipTolerance"))
-    float NoslipTolerance = 1e-6f;
-
-    UPROPERTY(EditAnywhere, Category = "MuJoCo Options", meta=(InlineEditConditionToggle))
-    bool bOverride_CCD_Iterations = false;
-
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo Options", meta=(EditCondition="bOverride_CCD_Iterations"))
-    int CCD_Iterations = 0;
-
-    UPROPERTY(EditAnywhere, Category = "MuJoCo Options", meta=(InlineEditConditionToggle))
-    bool bOverride_CCD_Tolerance = false;
-
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo Options", meta=(EditCondition="bOverride_CCD_Tolerance"))
-    float CCD_Tolerance = 1e-6f;
-
-    // --- Multi-CCD (MuJoCo 3.0+) ---
-
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo Options|Collision",
-        meta=(ToolTip="Enable multi-contact convex collision detection. Useful for flat surfaces (e.g. mesh-mesh) where single-point contact causes sliding or wobbling."))
-    bool bEnableMultiCCD = false;
-
-    // --- Sleep (MuJoCo 3.4+) ---
-
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo Options|Sleep")
-    bool bEnableSleep = false;
-
-    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo Options|Sleep",
-        meta=(EditCondition="bEnableSleep"))
-    float SleepTolerance = 1e-4f;
-
-    /**
-     * @brief Writes ALL option values to an mjSpec's option struct.
-     * Used by articulations to set their child spec options.
-     * Applies UE→MJ coordinate conversion for vectors.
-     */
-    void ApplyToSpec(mjSpec* Spec) const;
-
-    /**
-     * @brief Applies only overridden values to a compiled mjModel.
-     * Used by the manager for post-compile runtime overrides.
-     */
-    void ApplyOverridesToModel(mjModel* Model) const;
-};
diff --git a/Source/URLab/Public/MuJoCo/Generated/MjArticulationEnums.h b/Source/URLab/Public/MuJoCo/Generated/MjArticulationEnums.h
new file mode 100644
index 0000000..ca02c3b
--- /dev/null
+++ b/Source/URLab/Public/MuJoCo/Generated/MjArticulationEnums.h
@@ -0,0 +1,156 @@
+// Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+//
+// 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.
+//
+// --- LEGAL DISCLAIMER ---
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
+// trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
+//
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
+// CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
+
+#pragma once
+
+#include "CoreMinimal.h"
+#include "MjArticulationEnums.generated.h"
+
+/** EMjGainType — mirror of MuJoCo's mjtGain. */
+UENUM(BlueprintType)
+enum class EMjGainType : uint8
+{
+    Fixed = 0,
+    Affine = 1,
+    Muscle = 2,
+    DcMotor = 3,
+    User = 4
+};
+
+/** EMjBiasType — mirror of MuJoCo's mjtBias. */
+UENUM(BlueprintType)
+enum class EMjBiasType : uint8
+{
+    None = 0,
+    Affine = 1,
+    Muscle = 2,
+    DcMotor = 3,
+    User = 4
+};
+
+/** EMjDynType — mirror of MuJoCo's mjtDyn. */
+UENUM(BlueprintType)
+enum class EMjDynType : uint8
+{
+    None = 0,
+    Integrator = 1,
+    Filter = 2,
+    FilterExact = 3,
+    Muscle = 4,
+    DcMotor = 5,
+    User = 6
+};
+
+/** EMjGeomInertia — URLab editorial enum (no mjt counterpart). */
+UENUM(BlueprintType)
+enum class EMjGeomInertia : uint8
+{
+    Volume = 0,
+    Shell = 1
+};
+
+/** EMjFluidShape — URLab editorial enum (no mjt counterpart). */
+UENUM(BlueprintType)
+enum class EMjFluidShape : uint8
+{
+    None = 0,
+    Ellipsoid = 1
+};
+
+/** EMjCameraTrackingMode — mirror of MuJoCo's mjtCamLight. */
+UENUM(BlueprintType)
+enum class EMjCameraTrackingMode : uint8
+{
+    Fixed = 0,
+    Track = 1,
+    TrackCom = 2,
+    TargetBody = 3,
+    TargetBodyCom = 4
+};
+
+/** EMjCameraProjection — mirror of MuJoCo's mjtProjection. */
+UENUM(BlueprintType)
+enum class EMjCameraProjection : uint8
+{
+    Perspective = 0,
+    Orthographic = 1
+};
+
+/** EMjObjType — mirror of MuJoCo's mjtObj. */
+UENUM(BlueprintType)
+enum class EMjObjType : uint8
+{
+    Unknown = 0,
+    Body = 1,
+    XBody = 2,
+    Joint = 3,
+    DoF = 4,
+    Geom = 5,
+    Site = 6,
+    Camera = 7,
+    Light = 8,
+    Flex = 9,
+    Mesh = 10,
+    Skin = 11,
+    HField = 12,
+    Texture = 13,
+    Material = 14,
+    Pair = 15,
+    Exclude = 16,
+    Equality = 17,
+    Tendon = 18,
+    Actuator = 19,
+    Sensor = 20,
+    Numeric = 21,
+    Text = 22,
+    Tuple = 23,
+    Key = 24,
+    Plugin = 25,
+    Frame = 100
+};
+
+/** EMjActuatorTrnType — mirror of MuJoCo's mjtTrn. */
+UENUM(BlueprintType)
+enum class EMjActuatorTrnType : uint8
+{
+    Joint = 0,
+    JointInParent = 1,
+    SliderCrank = 2,
+    Tendon = 3,
+    Site = 4,
+    Body = 5,
+    Undefined = 6
+};
+
+/** EMjFlexcompDof — URLab editorial enum (no mjt counterpart). */
+UENUM(BlueprintType)
+enum class EMjFlexcompDof : uint8
+{
+    Full = 0,
+    Radial = 1,
+    Trilinear = 2,
+    Quadratic = 3,
+    TwoD = 4
+};
diff --git a/Source/URLab/Public/MuJoCo/Generated/MjArticulationRegistry.h b/Source/URLab/Public/MuJoCo/Generated/MjArticulationRegistry.h
new file mode 100644
index 0000000..c379f7d
--- /dev/null
+++ b/Source/URLab/Public/MuJoCo/Generated/MjArticulationRegistry.h
@@ -0,0 +1,61 @@
+// Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+//
+// 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.
+//
+// --- LEGAL DISCLAIMER ---
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
+// trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
+//
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
+// CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
+
+#pragma once
+
+#include "CoreMinimal.h"
+
+// Compile-time registry of every URLab-managed MuJoCo category.
+// Codegen-owned — driven by codegen_rules.json[categories].
+namespace MjArticulationRegistry
+{
+    struct CategoryEntry
+    {
+        const TCHAR* Name;          // schema element name, e.g. "body"
+        const TCHAR* BaseClassName; // UE base UClass, e.g. "UMjBody"
+        const TCHAR* MjsStruct;     // MuJoCo spec struct, e.g. "mjsBody"
+        const TCHAR* Layout;        // "single_uclass_per_file" / "multi_uclass" / "no_subclasses"
+        int          NumSubtypes;
+    };
+
+    inline constexpr CategoryEntry kCategories[] = {
+    { TEXT("actuator"), TEXT("UMjActuator"), TEXT("mjsActuator"), TEXT("single_uclass_per_file"), 10 },
+    { TEXT("body"), TEXT("UMjBody"), TEXT("mjsBody"), TEXT("no_subclasses"), 0 },
+    { TEXT("camera"), TEXT("UMjCamera"), TEXT("mjsCamera"), TEXT("no_subclasses"), 0 },
+    { TEXT("contact_exclude"), TEXT("UMjContactExclude"), TEXT("mjsExclude"), TEXT("no_subclasses"), 0 },
+    { TEXT("contact_pair"), TEXT("UMjContactPair"), TEXT("mjsPair"), TEXT("no_subclasses"), 0 },
+    { TEXT("equality"), TEXT("UMjEquality"), TEXT("mjsEquality"), TEXT("multi_uclass"), 7 },
+    { TEXT("flexcomp"), TEXT("UMjFlexcomp"), TEXT("mjsFlex"), TEXT("no_subclasses"), 0 },
+    { TEXT("frame"), TEXT("UMjFrame"), TEXT("mjsFrame"), TEXT("no_subclasses"), 0 },
+    { TEXT("geom"), TEXT("UMjGeom"), TEXT("mjsGeom"), TEXT("single_uclass_per_file"), 7 },
+    { TEXT("inertial"), TEXT("UMjInertial"), TEXT("mjsInertial"), TEXT("no_subclasses"), 0 },
+    { TEXT("joint"), TEXT("UMjJoint"), TEXT("mjsJoint"), TEXT("single_uclass_per_file"), 4 },
+    { TEXT("keyframe"), TEXT("UMjKeyframe"), TEXT("mjsKey"), TEXT("no_subclasses"), 0 },
+    { TEXT("sensor"), TEXT("UMjSensor"), TEXT("mjsSensor"), TEXT("single_uclass_per_file"), 49 },
+    { TEXT("site"), TEXT("UMjSite"), TEXT("mjsSite"), TEXT("multi_uclass"), 5 },
+    { TEXT("tendon"), TEXT("UMjTendon"), TEXT("mjsTendon"), TEXT("multi_uclass"), 2 },
+    };
+    inline constexpr int kNumCategories = sizeof(kCategories) / sizeof(kCategories[0]);
+}
diff --git a/Source/URLab/Public/MuJoCo/Generated/MjOptionEnums.h b/Source/URLab/Public/MuJoCo/Generated/MjOptionEnums.h
new file mode 100644
index 0000000..1b32086
--- /dev/null
+++ b/Source/URLab/Public/MuJoCo/Generated/MjOptionEnums.h
@@ -0,0 +1,56 @@
+// Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+//
+// 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.
+//
+// --- LEGAL DISCLAIMER ---
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
+// trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
+//
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
+// CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
+
+#pragma once
+
+#include "CoreMinimal.h"
+#include "MjOptionEnums.generated.h"
+
+/** EMjIntegrator — mirror of MuJoCo's mjtIntegrator. */
+UENUM(BlueprintType)
+enum class EMjIntegrator : uint8
+{
+    Euler = 0,
+    RK4 = 1,
+    Implicit = 2,
+    ImplicitFast = 3
+};
+
+/** EMjCone — mirror of MuJoCo's mjtCone. */
+UENUM(BlueprintType)
+enum class EMjCone : uint8
+{
+    Pyramidal = 0,
+    Elliptic = 1
+};
+
+/** EMjSolver — mirror of MuJoCo's mjtSolver. */
+UENUM(BlueprintType)
+enum class EMjSolver : uint8
+{
+    PGS = 0,
+    CG = 1,
+    Newton = 2
+};
diff --git a/Source/URLab/Public/MuJoCo/Generated/MjOptionGenerated.h b/Source/URLab/Public/MuJoCo/Generated/MjOptionGenerated.h
new file mode 100644
index 0000000..855776d
--- /dev/null
+++ b/Source/URLab/Public/MuJoCo/Generated/MjOptionGenerated.h
@@ -0,0 +1,221 @@
+// Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+//
+// 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.
+//
+// --- LEGAL DISCLAIMER ---
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
+// trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
+//
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
+// CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
+
+#pragma once
+
+#include "CoreMinimal.h"
+#include "mujoco/mujoco.h"
+#include "MuJoCo/Generated/MjOptionEnums.h"
+#include "MjOptionGenerated.generated.h"
+
+// Forward-decl: hand-written body lives in MjOptionGeneratedExtras.cpp.
+struct FMjOptionGenerated;
+void ApplyMjOptionExtras(mjOption* Opt, const FMjOptionGenerated& Self, mjSpec* Spec, mjModel* Model = nullptr);
+
+// Mirror of MuJoCo's ``mjOption`` (MJOPTION_FIELDS). Codegen-owned —
+// hand-edits get clobbered on next regen. Add fields by editing
+// codegen_rules.json[synthetic_categories.MjOption].
+USTRUCT(BlueprintType)
+struct URLAB_API FMjOptionGenerated
+{
+    GENERATED_BODY()
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjOption", meta=(InlineEditConditionToggle))
+    bool bOverride_Timestep = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjOption", meta=(EditCondition="bOverride_Timestep"))
+    float Timestep = 0.002;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjOption", meta=(InlineEditConditionToggle))
+    bool bOverride_Impratio = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjOption", meta=(EditCondition="bOverride_Impratio"))
+    double Impratio = 1.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjOption", meta=(InlineEditConditionToggle))
+    bool bOverride_Tolerance = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjOption", meta=(EditCondition="bOverride_Tolerance"))
+    double Tolerance = 1e-8;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjOption", meta=(InlineEditConditionToggle))
+    bool bOverride_LsTolerance = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjOption", meta=(EditCondition="bOverride_LsTolerance"))
+    double LsTolerance = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjOption", meta=(InlineEditConditionToggle))
+    bool bOverride_NoslipTolerance = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjOption", meta=(EditCondition="bOverride_NoslipTolerance"))
+    double NoslipTolerance = 1e-6;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjOption", meta=(InlineEditConditionToggle))
+    bool bOverride_CCD_Tolerance = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjOption", meta=(EditCondition="bOverride_CCD_Tolerance"))
+    double CCD_Tolerance = 1e-6;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjOption", meta=(InlineEditConditionToggle))
+    bool bOverride_SleepToleranceMj = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjOption", meta=(EditCondition="bOverride_SleepToleranceMj"))
+    double SleepToleranceMj = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjOption", meta=(InlineEditConditionToggle))
+    bool bOverride_Gravity = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjOption", meta=(EditCondition="bOverride_Gravity"))
+    FVector Gravity = FVector(0.0f, 0.0f, -981.0f);
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjOption", meta=(InlineEditConditionToggle))
+    bool bOverride_Wind = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjOption", meta=(EditCondition="bOverride_Wind"))
+    FVector Wind = FVector::ZeroVector;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjOption", meta=(InlineEditConditionToggle))
+    bool bOverride_Magnetic = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjOption", meta=(EditCondition="bOverride_Magnetic"))
+    FVector Magnetic = FVector(0.0f, -0.5f, 0.0f);
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjOption", meta=(InlineEditConditionToggle))
+    bool bOverride_Density = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjOption", meta=(EditCondition="bOverride_Density"))
+    double Density = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjOption", meta=(InlineEditConditionToggle))
+    bool bOverride_Viscosity = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjOption", meta=(EditCondition="bOverride_Viscosity"))
+    double Viscosity = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjOption", meta=(InlineEditConditionToggle))
+    bool bOverride_OMargin = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjOption", meta=(EditCondition="bOverride_OMargin"))
+    double OMargin = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjOption", meta=(InlineEditConditionToggle))
+    bool bOverride_Integrator = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjOption", meta=(EditCondition="bOverride_Integrator"))
+    EMjIntegrator Integrator = EMjIntegrator::Euler;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjOption", meta=(InlineEditConditionToggle))
+    bool bOverride_Cone = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjOption", meta=(EditCondition="bOverride_Cone"))
+    EMjCone Cone = EMjCone::Pyramidal;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjOption", meta=(InlineEditConditionToggle))
+    bool bOverride_Jacobian = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjOption", meta=(EditCondition="bOverride_Jacobian"))
+    int32 Jacobian = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjOption", meta=(InlineEditConditionToggle))
+    bool bOverride_Solver = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjOption", meta=(EditCondition="bOverride_Solver"))
+    EMjSolver Solver = EMjSolver::Newton;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjOption", meta=(InlineEditConditionToggle))
+    bool bOverride_Iterations = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjOption", meta=(EditCondition="bOverride_Iterations"))
+    int32 Iterations = 100;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjOption", meta=(InlineEditConditionToggle))
+    bool bOverride_LsIterations = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjOption", meta=(EditCondition="bOverride_LsIterations"))
+    int32 LsIterations = 50;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjOption", meta=(InlineEditConditionToggle))
+    bool bOverride_NoslipIterations = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjOption", meta=(EditCondition="bOverride_NoslipIterations"))
+    int32 NoslipIterations = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjOption", meta=(InlineEditConditionToggle))
+    bool bOverride_CCD_Iterations = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjOption", meta=(EditCondition="bOverride_CCD_Iterations"))
+    int32 CCD_Iterations = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjOption", meta=(InlineEditConditionToggle))
+    bool bOverride_DisableFlags = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjOption", meta=(EditCondition="bOverride_DisableFlags"))
+    int32 DisableFlags = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjOption", meta=(InlineEditConditionToggle))
+    bool bOverride_EnableFlags = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjOption", meta=(EditCondition="bOverride_EnableFlags"))
+    int32 EnableFlags = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjOption", meta=(InlineEditConditionToggle))
+    bool bOverride_DisableActuator = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjOption", meta=(EditCondition="bOverride_DisableActuator"))
+    int32 DisableActuator = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjOption", meta=(InlineEditConditionToggle))
+    bool bOverride_SdfInitPoints = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjOption", meta=(EditCondition="bOverride_SdfInitPoints"))
+    int32 SdfInitPoints = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjOption", meta=(InlineEditConditionToggle))
+    bool bOverride_SdfIterations = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjOption", meta=(EditCondition="bOverride_SdfIterations"))
+    int32 SdfIterations = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|MjOption", meta=(InlineEditConditionToggle))
+    bool bOverride_MemoryMB = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjOption", meta=(EditCondition="bOverride_MemoryMB", ClampMin="1"))
+    int32 MemoryMB = 100;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjOption|Collision", meta=(ToolTip="Enable multi-contact convex collision detection. Useful for flat surfaces (e.g. mesh-mesh) where single-point contact causes sliding or wobbling."))
+    bool bEnableMultiCCD = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjOption|Sleep")
+    bool bEnableSleep = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|MjOption|Sleep", meta=(EditCondition="bEnableSleep"))
+    float SleepTolerance = 1e-4f;
+
+    /** @brief Writes every mirror field plus URLab extras into the runtime spec. Used by articulations to set child spec options. */
+    void ApplyToSpec(mjSpec* Spec) const;
+
+    /** @brief Applies only override-toggled mirror fields to a compiled model (post-compile override path). */
+    void ApplyOverridesToModel(mjModel* Model) const;
+};
diff --git a/Source/URLab/Public/MuJoCo/Generated/MjStatistic.h b/Source/URLab/Public/MuJoCo/Generated/MjStatistic.h
new file mode 100644
index 0000000..224ee44
--- /dev/null
+++ b/Source/URLab/Public/MuJoCo/Generated/MjStatistic.h
@@ -0,0 +1,81 @@
+// Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+//
+// 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.
+//
+// --- LEGAL DISCLAIMER ---
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
+// trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
+//
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
+// CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
+
+#pragma once
+
+#include "CoreMinimal.h"
+#include "MjStatistic.generated.h"
+
+// Mirror of MuJoCo's ``mjStatistic`` (MJSTATISTIC_FIELDS). Codegen-owned —
+// hand-edits get clobbered on next regen. Add fields by editing
+// codegen_rules.json[synthetic_categories.MjStatistic].
+USTRUCT(BlueprintType)
+struct URLAB_API FMjStatistic
+{
+    GENERATED_BODY()
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Statistic", meta=(InlineEditConditionToggle))
+    bool bOverride_Meaninertia = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Statistic", meta=(EditCondition="bOverride_Meaninertia"))
+    double Meaninertia = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Statistic", meta=(InlineEditConditionToggle))
+    bool bOverride_Meanmass = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Statistic", meta=(EditCondition="bOverride_Meanmass"))
+    double Meanmass = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Statistic", meta=(InlineEditConditionToggle))
+    bool bOverride_Meansize = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Statistic", meta=(EditCondition="bOverride_Meansize"))
+    double Meansize = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Statistic", meta=(InlineEditConditionToggle))
+    bool bOverride_Extent = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Statistic", meta=(EditCondition="bOverride_Extent"))
+    double Extent = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Statistic", meta=(InlineEditConditionToggle))
+    bool bOverride_Center = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Statistic", meta=(EditCondition="bOverride_Center"))
+    FVector Center = FVector::ZeroVector;
+
+    /** @brief Mirror every field into a runtime C struct. */
+    template <typename TDst>
+    void ApplyTo(TDst& Dst) const
+    {
+    Dst.meaninertia = static_cast<decltype(Dst.meaninertia)>(Meaninertia);
+    Dst.meanmass = static_cast<decltype(Dst.meanmass)>(Meanmass);
+    Dst.meansize = static_cast<decltype(Dst.meansize)>(Meansize);
+    Dst.extent = static_cast<decltype(Dst.extent)>(Extent);
+    Dst.center[0] = static_cast<decltype(Dst.center[0])>(Center.X);
+    Dst.center[1] = static_cast<decltype(Dst.center[1])>(Center.Y);
+    Dst.center[2] = static_cast<decltype(Dst.center[2])>(Center.Z);
+    }
+};
diff --git a/Source/URLab/Public/MuJoCo/Generated/MjVisualGlobal.h b/Source/URLab/Public/MuJoCo/Generated/MjVisualGlobal.h
new file mode 100644
index 0000000..86c783a
--- /dev/null
+++ b/Source/URLab/Public/MuJoCo/Generated/MjVisualGlobal.h
@@ -0,0 +1,135 @@
+// Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+//
+// 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.
+//
+// --- LEGAL DISCLAIMER ---
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
+// trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
+//
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
+// CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
+
+#pragma once
+
+#include "CoreMinimal.h"
+#include "MjVisualGlobal.generated.h"
+
+// Mirror of MuJoCo's ``mjVisualGlobal`` (MJVISUAL_GLOBAL_FIELDS). Codegen-owned —
+// hand-edits get clobbered on next regen. Add fields by editing
+// codegen_rules.json[synthetic_categories.MjVisualGlobal].
+USTRUCT(BlueprintType)
+struct URLAB_API FMjVisualGlobal
+{
+    GENERATED_BODY()
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Global", meta=(InlineEditConditionToggle))
+    bool bOverride_Cameraid = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Global", meta=(EditCondition="bOverride_Cameraid"))
+    int32 Cameraid = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Global", meta=(InlineEditConditionToggle))
+    bool bOverride_Orthographic = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Global", meta=(EditCondition="bOverride_Orthographic"))
+    int32 Orthographic = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Global", meta=(InlineEditConditionToggle))
+    bool bOverride_Fovy = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Global", meta=(EditCondition="bOverride_Fovy"))
+    float Fovy = 45.0f;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Global", meta=(InlineEditConditionToggle))
+    bool bOverride_Ipd = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Global", meta=(EditCondition="bOverride_Ipd"))
+    float Ipd = 0.068f;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Global", meta=(InlineEditConditionToggle))
+    bool bOverride_Azimuth = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Global", meta=(EditCondition="bOverride_Azimuth"))
+    float Azimuth = 0.0f;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Global", meta=(InlineEditConditionToggle))
+    bool bOverride_Elevation = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Global", meta=(EditCondition="bOverride_Elevation"))
+    float Elevation = 0.0f;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Global", meta=(InlineEditConditionToggle))
+    bool bOverride_Linewidth = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Global", meta=(EditCondition="bOverride_Linewidth"))
+    float Linewidth = 0.0f;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Global", meta=(InlineEditConditionToggle))
+    bool bOverride_Glow = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Global", meta=(EditCondition="bOverride_Glow"))
+    float Glow = 0.0f;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Global", meta=(InlineEditConditionToggle))
+    bool bOverride_Realtime = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Global", meta=(EditCondition="bOverride_Realtime"))
+    float Realtime = 1.0f;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Global", meta=(InlineEditConditionToggle))
+    bool bOverride_Offwidth = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Global", meta=(EditCondition="bOverride_Offwidth"))
+    int32 Offwidth = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Global", meta=(InlineEditConditionToggle))
+    bool bOverride_Offheight = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Global", meta=(EditCondition="bOverride_Offheight"))
+    int32 Offheight = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Global", meta=(InlineEditConditionToggle))
+    bool bOverride_Ellipsoidinertia = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Global", meta=(EditCondition="bOverride_Ellipsoidinertia"))
+    int32 Ellipsoidinertia = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Global", meta=(InlineEditConditionToggle))
+    bool bOverride_Bvactive = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Global", meta=(EditCondition="bOverride_Bvactive"))
+    int32 Bvactive = 0;
+
+    /** @brief Mirror every field into a runtime C struct. */
+    template <typename TDst>
+    void ApplyTo(TDst& Dst) const
+    {
+    Dst.cameraid = static_cast<decltype(Dst.cameraid)>(Cameraid);
+    Dst.orthographic = static_cast<decltype(Dst.orthographic)>(Orthographic);
+    Dst.fovy = static_cast<decltype(Dst.fovy)>(Fovy);
+    Dst.ipd = static_cast<decltype(Dst.ipd)>(Ipd);
+    Dst.azimuth = static_cast<decltype(Dst.azimuth)>(Azimuth);
+    Dst.elevation = static_cast<decltype(Dst.elevation)>(Elevation);
+    Dst.linewidth = static_cast<decltype(Dst.linewidth)>(Linewidth);
+    Dst.glow = static_cast<decltype(Dst.glow)>(Glow);
+    Dst.realtime = static_cast<decltype(Dst.realtime)>(Realtime);
+    Dst.offwidth = static_cast<decltype(Dst.offwidth)>(Offwidth);
+    Dst.offheight = static_cast<decltype(Dst.offheight)>(Offheight);
+    Dst.ellipsoidinertia = static_cast<decltype(Dst.ellipsoidinertia)>(Ellipsoidinertia);
+    Dst.bvactive = static_cast<decltype(Dst.bvactive)>(Bvactive);
+    }
+};
diff --git a/Source/URLab/Public/MuJoCo/Generated/MjVisualHeadlight.h b/Source/URLab/Public/MuJoCo/Generated/MjVisualHeadlight.h
new file mode 100644
index 0000000..d5dcb5f
--- /dev/null
+++ b/Source/URLab/Public/MuJoCo/Generated/MjVisualHeadlight.h
@@ -0,0 +1,78 @@
+// Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+//
+// 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.
+//
+// --- LEGAL DISCLAIMER ---
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
+// trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
+//
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
+// CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
+
+#pragma once
+
+#include "CoreMinimal.h"
+#include "MjVisualHeadlight.generated.h"
+
+// Mirror of MuJoCo's ``mjVisualHeadlight`` (MJVISUAL_HEADLIGHT_FIELDS). Codegen-owned —
+// hand-edits get clobbered on next regen. Add fields by editing
+// codegen_rules.json[synthetic_categories.MjVisualHeadlight].
+USTRUCT(BlueprintType)
+struct URLAB_API FMjVisualHeadlight
+{
+    GENERATED_BODY()
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Headlight", meta=(InlineEditConditionToggle))
+    bool bOverride_Ambient = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Headlight", meta=(EditCondition="bOverride_Ambient"))
+    FVector Ambient = FVector(0.1f, 0.1f, 0.1f);
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Headlight", meta=(InlineEditConditionToggle))
+    bool bOverride_Diffuse = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Headlight", meta=(EditCondition="bOverride_Diffuse"))
+    FVector Diffuse = FVector(0.4f, 0.4f, 0.4f);
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Headlight", meta=(InlineEditConditionToggle))
+    bool bOverride_Specular = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Headlight", meta=(EditCondition="bOverride_Specular"))
+    FVector Specular = FVector(0.5f, 0.5f, 0.5f);
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Headlight", meta=(InlineEditConditionToggle))
+    bool bOverride_Active = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Headlight", meta=(EditCondition="bOverride_Active"))
+    int32 Active = 1;
+
+    /** @brief Mirror every field into a runtime C struct. */
+    template <typename TDst>
+    void ApplyTo(TDst& Dst) const
+    {
+    Dst.ambient[0] = static_cast<decltype(Dst.ambient[0])>(Ambient.X);
+    Dst.ambient[1] = static_cast<decltype(Dst.ambient[1])>(Ambient.Y);
+    Dst.ambient[2] = static_cast<decltype(Dst.ambient[2])>(Ambient.Z);
+    Dst.diffuse[0] = static_cast<decltype(Dst.diffuse[0])>(Diffuse.X);
+    Dst.diffuse[1] = static_cast<decltype(Dst.diffuse[1])>(Diffuse.Y);
+    Dst.diffuse[2] = static_cast<decltype(Dst.diffuse[2])>(Diffuse.Z);
+    Dst.specular[0] = static_cast<decltype(Dst.specular[0])>(Specular.X);
+    Dst.specular[1] = static_cast<decltype(Dst.specular[1])>(Specular.Y);
+    Dst.specular[2] = static_cast<decltype(Dst.specular[2])>(Specular.Z);
+    Dst.active = static_cast<decltype(Dst.active)>(Active);
+    }
+};
diff --git a/Source/URLab/Public/MuJoCo/Generated/MjVisualMap.h b/Source/URLab/Public/MuJoCo/Generated/MjVisualMap.h
new file mode 100644
index 0000000..8b35c7e
--- /dev/null
+++ b/Source/URLab/Public/MuJoCo/Generated/MjVisualMap.h
@@ -0,0 +1,135 @@
+// Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+//
+// 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.
+//
+// --- LEGAL DISCLAIMER ---
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
+// trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
+//
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
+// CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
+
+#pragma once
+
+#include "CoreMinimal.h"
+#include "MjVisualMap.generated.h"
+
+// Mirror of MuJoCo's ``mjVisualMap`` (MJVISUAL_MAP_FIELDS). Codegen-owned —
+// hand-edits get clobbered on next regen. Add fields by editing
+// codegen_rules.json[synthetic_categories.MjVisualMap].
+USTRUCT(BlueprintType)
+struct URLAB_API FMjVisualMap
+{
+    GENERATED_BODY()
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Map", meta=(InlineEditConditionToggle))
+    bool bOverride_Stiffness = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Map", meta=(EditCondition="bOverride_Stiffness"))
+    double Stiffness = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Map", meta=(InlineEditConditionToggle))
+    bool bOverride_Stiffnessrot = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Map", meta=(EditCondition="bOverride_Stiffnessrot"))
+    double Stiffnessrot = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Map", meta=(InlineEditConditionToggle))
+    bool bOverride_Force = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Map", meta=(EditCondition="bOverride_Force"))
+    double Force = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Map", meta=(InlineEditConditionToggle))
+    bool bOverride_Torque = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Map", meta=(EditCondition="bOverride_Torque"))
+    double Torque = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Map", meta=(InlineEditConditionToggle))
+    bool bOverride_Alpha = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Map", meta=(EditCondition="bOverride_Alpha"))
+    double Alpha = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Map", meta=(InlineEditConditionToggle))
+    bool bOverride_Fogstart = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Map", meta=(EditCondition="bOverride_Fogstart"))
+    double Fogstart = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Map", meta=(InlineEditConditionToggle))
+    bool bOverride_Fogend = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Map", meta=(EditCondition="bOverride_Fogend"))
+    double Fogend = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Map", meta=(InlineEditConditionToggle))
+    bool bOverride_Znear = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Map", meta=(EditCondition="bOverride_Znear"))
+    double Znear = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Map", meta=(InlineEditConditionToggle))
+    bool bOverride_Zfar = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Map", meta=(EditCondition="bOverride_Zfar"))
+    double Zfar = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Map", meta=(InlineEditConditionToggle))
+    bool bOverride_Haze = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Map", meta=(EditCondition="bOverride_Haze"))
+    double Haze = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Map", meta=(InlineEditConditionToggle))
+    bool bOverride_Shadowclip = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Map", meta=(EditCondition="bOverride_Shadowclip"))
+    double Shadowclip = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Map", meta=(InlineEditConditionToggle))
+    bool bOverride_Shadowscale = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Map", meta=(EditCondition="bOverride_Shadowscale"))
+    double Shadowscale = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Map", meta=(InlineEditConditionToggle))
+    bool bOverride_Actuatortendon = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Map", meta=(EditCondition="bOverride_Actuatortendon"))
+    double Actuatortendon = 0.0;
+
+    /** @brief Mirror every field into a runtime C struct. */
+    template <typename TDst>
+    void ApplyTo(TDst& Dst) const
+    {
+    Dst.stiffness = static_cast<decltype(Dst.stiffness)>(Stiffness);
+    Dst.stiffnessrot = static_cast<decltype(Dst.stiffnessrot)>(Stiffnessrot);
+    Dst.force = static_cast<decltype(Dst.force)>(Force);
+    Dst.torque = static_cast<decltype(Dst.torque)>(Torque);
+    Dst.alpha = static_cast<decltype(Dst.alpha)>(Alpha);
+    Dst.fogstart = static_cast<decltype(Dst.fogstart)>(Fogstart);
+    Dst.fogend = static_cast<decltype(Dst.fogend)>(Fogend);
+    Dst.znear = static_cast<decltype(Dst.znear)>(Znear);
+    Dst.zfar = static_cast<decltype(Dst.zfar)>(Zfar);
+    Dst.haze = static_cast<decltype(Dst.haze)>(Haze);
+    Dst.shadowclip = static_cast<decltype(Dst.shadowclip)>(Shadowclip);
+    Dst.shadowscale = static_cast<decltype(Dst.shadowscale)>(Shadowscale);
+    Dst.actuatortendon = static_cast<decltype(Dst.actuatortendon)>(Actuatortendon);
+    }
+};
diff --git a/Source/URLab/Public/MuJoCo/Generated/MjVisualQuality.h b/Source/URLab/Public/MuJoCo/Generated/MjVisualQuality.h
new file mode 100644
index 0000000..fb0cec6
--- /dev/null
+++ b/Source/URLab/Public/MuJoCo/Generated/MjVisualQuality.h
@@ -0,0 +1,79 @@
+// Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+//
+// 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.
+//
+// --- LEGAL DISCLAIMER ---
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
+// trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
+//
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
+// CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
+
+#pragma once
+
+#include "CoreMinimal.h"
+#include "MjVisualQuality.generated.h"
+
+// Mirror of MuJoCo's ``mjVisualQuality`` (MJVISUAL_QUALITY_FIELDS). Codegen-owned —
+// hand-edits get clobbered on next regen. Add fields by editing
+// codegen_rules.json[synthetic_categories.MjVisualQuality].
+USTRUCT(BlueprintType)
+struct URLAB_API FMjVisualQuality
+{
+    GENERATED_BODY()
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Quality", meta=(InlineEditConditionToggle))
+    bool bOverride_Shadowsize = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Quality", meta=(EditCondition="bOverride_Shadowsize"))
+    int32 Shadowsize = 1024;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Quality", meta=(InlineEditConditionToggle))
+    bool bOverride_Offsamples = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Quality", meta=(EditCondition="bOverride_Offsamples"))
+    int32 Offsamples = 4;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Quality", meta=(InlineEditConditionToggle))
+    bool bOverride_Numslices = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Quality", meta=(EditCondition="bOverride_Numslices"))
+    int32 Numslices = 28;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Quality", meta=(InlineEditConditionToggle))
+    bool bOverride_Numstacks = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Quality", meta=(EditCondition="bOverride_Numstacks"))
+    int32 Numstacks = 16;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Quality", meta=(InlineEditConditionToggle))
+    bool bOverride_Numquads = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Quality", meta=(EditCondition="bOverride_Numquads"))
+    int32 Numquads = 4;
+
+    /** @brief Mirror every field into a runtime C struct. */
+    template <typename TDst>
+    void ApplyTo(TDst& Dst) const
+    {
+    Dst.shadowsize = static_cast<decltype(Dst.shadowsize)>(Shadowsize);
+    Dst.offsamples = static_cast<decltype(Dst.offsamples)>(Offsamples);
+    Dst.numslices = static_cast<decltype(Dst.numslices)>(Numslices);
+    Dst.numstacks = static_cast<decltype(Dst.numstacks)>(Numstacks);
+    Dst.numquads = static_cast<decltype(Dst.numquads)>(Numquads);
+    }
+};
diff --git a/Source/URLab/Public/MuJoCo/Generated/MjVisualRgba.h b/Source/URLab/Public/MuJoCo/Generated/MjVisualRgba.h
new file mode 100644
index 0000000..e0469f9
--- /dev/null
+++ b/Source/URLab/Public/MuJoCo/Generated/MjVisualRgba.h
@@ -0,0 +1,294 @@
+// Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+//
+// 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.
+//
+// --- LEGAL DISCLAIMER ---
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
+// trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
+//
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
+// CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
+
+#pragma once
+
+#include "CoreMinimal.h"
+#include "MjVisualRgba.generated.h"
+
+// Mirror of MuJoCo's ``mjVisualRgba`` (MJVISUAL_RGBA_FIELDS). Codegen-owned —
+// hand-edits get clobbered on next regen. Add fields by editing
+// codegen_rules.json[synthetic_categories.MjVisualRgba].
+USTRUCT(BlueprintType)
+struct URLAB_API FMjVisualRgba
+{
+    GENERATED_BODY()
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Rgba", meta=(InlineEditConditionToggle))
+    bool bOverride_Fog = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Rgba", meta=(EditCondition="bOverride_Fog"))
+    FLinearColor Fog = FLinearColor::White;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Rgba", meta=(InlineEditConditionToggle))
+    bool bOverride_Haze = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Rgba", meta=(EditCondition="bOverride_Haze"))
+    FLinearColor Haze = FLinearColor::White;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Rgba", meta=(InlineEditConditionToggle))
+    bool bOverride_Force = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Rgba", meta=(EditCondition="bOverride_Force"))
+    FLinearColor Force = FLinearColor::White;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Rgba", meta=(InlineEditConditionToggle))
+    bool bOverride_Inertia = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Rgba", meta=(EditCondition="bOverride_Inertia"))
+    FLinearColor Inertia = FLinearColor::White;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Rgba", meta=(InlineEditConditionToggle))
+    bool bOverride_Joint = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Rgba", meta=(EditCondition="bOverride_Joint"))
+    FLinearColor Joint = FLinearColor::White;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Rgba", meta=(InlineEditConditionToggle))
+    bool bOverride_Actuator = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Rgba", meta=(EditCondition="bOverride_Actuator"))
+    FLinearColor Actuator = FLinearColor::White;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Rgba", meta=(InlineEditConditionToggle))
+    bool bOverride_Actuatornegative = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Rgba", meta=(EditCondition="bOverride_Actuatornegative"))
+    FLinearColor Actuatornegative = FLinearColor::White;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Rgba", meta=(InlineEditConditionToggle))
+    bool bOverride_Actuatorpositive = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Rgba", meta=(EditCondition="bOverride_Actuatorpositive"))
+    FLinearColor Actuatorpositive = FLinearColor::White;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Rgba", meta=(InlineEditConditionToggle))
+    bool bOverride_Com = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Rgba", meta=(EditCondition="bOverride_Com"))
+    FLinearColor Com = FLinearColor::White;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Rgba", meta=(InlineEditConditionToggle))
+    bool bOverride_Camera = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Rgba", meta=(EditCondition="bOverride_Camera"))
+    FLinearColor Camera = FLinearColor::White;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Rgba", meta=(InlineEditConditionToggle))
+    bool bOverride_Light = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Rgba", meta=(EditCondition="bOverride_Light"))
+    FLinearColor Light = FLinearColor::White;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Rgba", meta=(InlineEditConditionToggle))
+    bool bOverride_Selectpoint = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Rgba", meta=(EditCondition="bOverride_Selectpoint"))
+    FLinearColor Selectpoint = FLinearColor::White;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Rgba", meta=(InlineEditConditionToggle))
+    bool bOverride_Connect = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Rgba", meta=(EditCondition="bOverride_Connect"))
+    FLinearColor Connect = FLinearColor::White;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Rgba", meta=(InlineEditConditionToggle))
+    bool bOverride_Contactpoint = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Rgba", meta=(EditCondition="bOverride_Contactpoint"))
+    FLinearColor Contactpoint = FLinearColor::White;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Rgba", meta=(InlineEditConditionToggle))
+    bool bOverride_Contactforce = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Rgba", meta=(EditCondition="bOverride_Contactforce"))
+    FLinearColor Contactforce = FLinearColor::White;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Rgba", meta=(InlineEditConditionToggle))
+    bool bOverride_Contactfriction = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Rgba", meta=(EditCondition="bOverride_Contactfriction"))
+    FLinearColor Contactfriction = FLinearColor::White;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Rgba", meta=(InlineEditConditionToggle))
+    bool bOverride_Contacttorque = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Rgba", meta=(EditCondition="bOverride_Contacttorque"))
+    FLinearColor Contacttorque = FLinearColor::White;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Rgba", meta=(InlineEditConditionToggle))
+    bool bOverride_Contactgap = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Rgba", meta=(EditCondition="bOverride_Contactgap"))
+    FLinearColor Contactgap = FLinearColor::White;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Rgba", meta=(InlineEditConditionToggle))
+    bool bOverride_Rangefinder = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Rgba", meta=(EditCondition="bOverride_Rangefinder"))
+    FLinearColor Rangefinder = FLinearColor::White;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Rgba", meta=(InlineEditConditionToggle))
+    bool bOverride_Constraint = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Rgba", meta=(EditCondition="bOverride_Constraint"))
+    FLinearColor Constraint = FLinearColor::White;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Rgba", meta=(InlineEditConditionToggle))
+    bool bOverride_Slidercrank = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Rgba", meta=(EditCondition="bOverride_Slidercrank"))
+    FLinearColor Slidercrank = FLinearColor::White;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Rgba", meta=(InlineEditConditionToggle))
+    bool bOverride_Crankbroken = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Rgba", meta=(EditCondition="bOverride_Crankbroken"))
+    FLinearColor Crankbroken = FLinearColor::White;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Rgba", meta=(InlineEditConditionToggle))
+    bool bOverride_Frustum = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Rgba", meta=(EditCondition="bOverride_Frustum"))
+    FLinearColor Frustum = FLinearColor::White;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Rgba", meta=(InlineEditConditionToggle))
+    bool bOverride_Bv = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Rgba", meta=(EditCondition="bOverride_Bv"))
+    FLinearColor Bv = FLinearColor::White;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Rgba", meta=(InlineEditConditionToggle))
+    bool bOverride_Bvactive = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Rgba", meta=(EditCondition="bOverride_Bvactive"))
+    FLinearColor Bvactive = FLinearColor::White;
+
+    /** @brief Mirror every field into a runtime C struct. */
+    template <typename TDst>
+    void ApplyTo(TDst& Dst) const
+    {
+    Dst.fog[0] = static_cast<decltype(Dst.fog[0])>(Fog.R);
+    Dst.fog[1] = static_cast<decltype(Dst.fog[1])>(Fog.G);
+    Dst.fog[2] = static_cast<decltype(Dst.fog[2])>(Fog.B);
+    Dst.fog[3] = static_cast<decltype(Dst.fog[3])>(Fog.A);
+    Dst.haze[0] = static_cast<decltype(Dst.haze[0])>(Haze.R);
+    Dst.haze[1] = static_cast<decltype(Dst.haze[1])>(Haze.G);
+    Dst.haze[2] = static_cast<decltype(Dst.haze[2])>(Haze.B);
+    Dst.haze[3] = static_cast<decltype(Dst.haze[3])>(Haze.A);
+    Dst.force[0] = static_cast<decltype(Dst.force[0])>(Force.R);
+    Dst.force[1] = static_cast<decltype(Dst.force[1])>(Force.G);
+    Dst.force[2] = static_cast<decltype(Dst.force[2])>(Force.B);
+    Dst.force[3] = static_cast<decltype(Dst.force[3])>(Force.A);
+    Dst.inertia[0] = static_cast<decltype(Dst.inertia[0])>(Inertia.R);
+    Dst.inertia[1] = static_cast<decltype(Dst.inertia[1])>(Inertia.G);
+    Dst.inertia[2] = static_cast<decltype(Dst.inertia[2])>(Inertia.B);
+    Dst.inertia[3] = static_cast<decltype(Dst.inertia[3])>(Inertia.A);
+    Dst.joint[0] = static_cast<decltype(Dst.joint[0])>(Joint.R);
+    Dst.joint[1] = static_cast<decltype(Dst.joint[1])>(Joint.G);
+    Dst.joint[2] = static_cast<decltype(Dst.joint[2])>(Joint.B);
+    Dst.joint[3] = static_cast<decltype(Dst.joint[3])>(Joint.A);
+    Dst.actuator[0] = static_cast<decltype(Dst.actuator[0])>(Actuator.R);
+    Dst.actuator[1] = static_cast<decltype(Dst.actuator[1])>(Actuator.G);
+    Dst.actuator[2] = static_cast<decltype(Dst.actuator[2])>(Actuator.B);
+    Dst.actuator[3] = static_cast<decltype(Dst.actuator[3])>(Actuator.A);
+    Dst.actuatornegative[0] = static_cast<decltype(Dst.actuatornegative[0])>(Actuatornegative.R);
+    Dst.actuatornegative[1] = static_cast<decltype(Dst.actuatornegative[1])>(Actuatornegative.G);
+    Dst.actuatornegative[2] = static_cast<decltype(Dst.actuatornegative[2])>(Actuatornegative.B);
+    Dst.actuatornegative[3] = static_cast<decltype(Dst.actuatornegative[3])>(Actuatornegative.A);
+    Dst.actuatorpositive[0] = static_cast<decltype(Dst.actuatorpositive[0])>(Actuatorpositive.R);
+    Dst.actuatorpositive[1] = static_cast<decltype(Dst.actuatorpositive[1])>(Actuatorpositive.G);
+    Dst.actuatorpositive[2] = static_cast<decltype(Dst.actuatorpositive[2])>(Actuatorpositive.B);
+    Dst.actuatorpositive[3] = static_cast<decltype(Dst.actuatorpositive[3])>(Actuatorpositive.A);
+    Dst.com[0] = static_cast<decltype(Dst.com[0])>(Com.R);
+    Dst.com[1] = static_cast<decltype(Dst.com[1])>(Com.G);
+    Dst.com[2] = static_cast<decltype(Dst.com[2])>(Com.B);
+    Dst.com[3] = static_cast<decltype(Dst.com[3])>(Com.A);
+    Dst.camera[0] = static_cast<decltype(Dst.camera[0])>(Camera.R);
+    Dst.camera[1] = static_cast<decltype(Dst.camera[1])>(Camera.G);
+    Dst.camera[2] = static_cast<decltype(Dst.camera[2])>(Camera.B);
+    Dst.camera[3] = static_cast<decltype(Dst.camera[3])>(Camera.A);
+    Dst.light[0] = static_cast<decltype(Dst.light[0])>(Light.R);
+    Dst.light[1] = static_cast<decltype(Dst.light[1])>(Light.G);
+    Dst.light[2] = static_cast<decltype(Dst.light[2])>(Light.B);
+    Dst.light[3] = static_cast<decltype(Dst.light[3])>(Light.A);
+    Dst.selectpoint[0] = static_cast<decltype(Dst.selectpoint[0])>(Selectpoint.R);
+    Dst.selectpoint[1] = static_cast<decltype(Dst.selectpoint[1])>(Selectpoint.G);
+    Dst.selectpoint[2] = static_cast<decltype(Dst.selectpoint[2])>(Selectpoint.B);
+    Dst.selectpoint[3] = static_cast<decltype(Dst.selectpoint[3])>(Selectpoint.A);
+    Dst.connect[0] = static_cast<decltype(Dst.connect[0])>(Connect.R);
+    Dst.connect[1] = static_cast<decltype(Dst.connect[1])>(Connect.G);
+    Dst.connect[2] = static_cast<decltype(Dst.connect[2])>(Connect.B);
+    Dst.connect[3] = static_cast<decltype(Dst.connect[3])>(Connect.A);
+    Dst.contactpoint[0] = static_cast<decltype(Dst.contactpoint[0])>(Contactpoint.R);
+    Dst.contactpoint[1] = static_cast<decltype(Dst.contactpoint[1])>(Contactpoint.G);
+    Dst.contactpoint[2] = static_cast<decltype(Dst.contactpoint[2])>(Contactpoint.B);
+    Dst.contactpoint[3] = static_cast<decltype(Dst.contactpoint[3])>(Contactpoint.A);
+    Dst.contactforce[0] = static_cast<decltype(Dst.contactforce[0])>(Contactforce.R);
+    Dst.contactforce[1] = static_cast<decltype(Dst.contactforce[1])>(Contactforce.G);
+    Dst.contactforce[2] = static_cast<decltype(Dst.contactforce[2])>(Contactforce.B);
+    Dst.contactforce[3] = static_cast<decltype(Dst.contactforce[3])>(Contactforce.A);
+    Dst.contactfriction[0] = static_cast<decltype(Dst.contactfriction[0])>(Contactfriction.R);
+    Dst.contactfriction[1] = static_cast<decltype(Dst.contactfriction[1])>(Contactfriction.G);
+    Dst.contactfriction[2] = static_cast<decltype(Dst.contactfriction[2])>(Contactfriction.B);
+    Dst.contactfriction[3] = static_cast<decltype(Dst.contactfriction[3])>(Contactfriction.A);
+    Dst.contacttorque[0] = static_cast<decltype(Dst.contacttorque[0])>(Contacttorque.R);
+    Dst.contacttorque[1] = static_cast<decltype(Dst.contacttorque[1])>(Contacttorque.G);
+    Dst.contacttorque[2] = static_cast<decltype(Dst.contacttorque[2])>(Contacttorque.B);
+    Dst.contacttorque[3] = static_cast<decltype(Dst.contacttorque[3])>(Contacttorque.A);
+    Dst.contactgap[0] = static_cast<decltype(Dst.contactgap[0])>(Contactgap.R);
+    Dst.contactgap[1] = static_cast<decltype(Dst.contactgap[1])>(Contactgap.G);
+    Dst.contactgap[2] = static_cast<decltype(Dst.contactgap[2])>(Contactgap.B);
+    Dst.contactgap[3] = static_cast<decltype(Dst.contactgap[3])>(Contactgap.A);
+    Dst.rangefinder[0] = static_cast<decltype(Dst.rangefinder[0])>(Rangefinder.R);
+    Dst.rangefinder[1] = static_cast<decltype(Dst.rangefinder[1])>(Rangefinder.G);
+    Dst.rangefinder[2] = static_cast<decltype(Dst.rangefinder[2])>(Rangefinder.B);
+    Dst.rangefinder[3] = static_cast<decltype(Dst.rangefinder[3])>(Rangefinder.A);
+    Dst.constraint[0] = static_cast<decltype(Dst.constraint[0])>(Constraint.R);
+    Dst.constraint[1] = static_cast<decltype(Dst.constraint[1])>(Constraint.G);
+    Dst.constraint[2] = static_cast<decltype(Dst.constraint[2])>(Constraint.B);
+    Dst.constraint[3] = static_cast<decltype(Dst.constraint[3])>(Constraint.A);
+    Dst.slidercrank[0] = static_cast<decltype(Dst.slidercrank[0])>(Slidercrank.R);
+    Dst.slidercrank[1] = static_cast<decltype(Dst.slidercrank[1])>(Slidercrank.G);
+    Dst.slidercrank[2] = static_cast<decltype(Dst.slidercrank[2])>(Slidercrank.B);
+    Dst.slidercrank[3] = static_cast<decltype(Dst.slidercrank[3])>(Slidercrank.A);
+    Dst.crankbroken[0] = static_cast<decltype(Dst.crankbroken[0])>(Crankbroken.R);
+    Dst.crankbroken[1] = static_cast<decltype(Dst.crankbroken[1])>(Crankbroken.G);
+    Dst.crankbroken[2] = static_cast<decltype(Dst.crankbroken[2])>(Crankbroken.B);
+    Dst.crankbroken[3] = static_cast<decltype(Dst.crankbroken[3])>(Crankbroken.A);
+    Dst.frustum[0] = static_cast<decltype(Dst.frustum[0])>(Frustum.R);
+    Dst.frustum[1] = static_cast<decltype(Dst.frustum[1])>(Frustum.G);
+    Dst.frustum[2] = static_cast<decltype(Dst.frustum[2])>(Frustum.B);
+    Dst.frustum[3] = static_cast<decltype(Dst.frustum[3])>(Frustum.A);
+    Dst.bv[0] = static_cast<decltype(Dst.bv[0])>(Bv.R);
+    Dst.bv[1] = static_cast<decltype(Dst.bv[1])>(Bv.G);
+    Dst.bv[2] = static_cast<decltype(Dst.bv[2])>(Bv.B);
+    Dst.bv[3] = static_cast<decltype(Dst.bv[3])>(Bv.A);
+    Dst.bvactive[0] = static_cast<decltype(Dst.bvactive[0])>(Bvactive.R);
+    Dst.bvactive[1] = static_cast<decltype(Dst.bvactive[1])>(Bvactive.G);
+    Dst.bvactive[2] = static_cast<decltype(Dst.bvactive[2])>(Bvactive.B);
+    Dst.bvactive[3] = static_cast<decltype(Dst.bvactive[3])>(Bvactive.A);
+    }
+};
diff --git a/Source/URLab/Public/MuJoCo/Generated/MjVisualScale.h b/Source/URLab/Public/MuJoCo/Generated/MjVisualScale.h
new file mode 100644
index 0000000..bf1c9d9
--- /dev/null
+++ b/Source/URLab/Public/MuJoCo/Generated/MjVisualScale.h
@@ -0,0 +1,163 @@
+// Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+//
+// 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.
+//
+// --- LEGAL DISCLAIMER ---
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
+// trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
+//
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
+// CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
+
+#pragma once
+
+#include "CoreMinimal.h"
+#include "MjVisualScale.generated.h"
+
+// Mirror of MuJoCo's ``mjVisualScale`` (MJVISUAL_SCALE_FIELDS). Codegen-owned —
+// hand-edits get clobbered on next regen. Add fields by editing
+// codegen_rules.json[synthetic_categories.MjVisualScale].
+USTRUCT(BlueprintType)
+struct URLAB_API FMjVisualScale
+{
+    GENERATED_BODY()
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Scale", meta=(InlineEditConditionToggle))
+    bool bOverride_Forcewidth = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Scale", meta=(EditCondition="bOverride_Forcewidth"))
+    double Forcewidth = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Scale", meta=(InlineEditConditionToggle))
+    bool bOverride_Contactwidth = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Scale", meta=(EditCondition="bOverride_Contactwidth"))
+    double Contactwidth = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Scale", meta=(InlineEditConditionToggle))
+    bool bOverride_Contactheight = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Scale", meta=(EditCondition="bOverride_Contactheight"))
+    double Contactheight = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Scale", meta=(InlineEditConditionToggle))
+    bool bOverride_Connect = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Scale", meta=(EditCondition="bOverride_Connect"))
+    double Connect = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Scale", meta=(InlineEditConditionToggle))
+    bool bOverride_Com = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Scale", meta=(EditCondition="bOverride_Com"))
+    double Com = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Scale", meta=(InlineEditConditionToggle))
+    bool bOverride_Camera = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Scale", meta=(EditCondition="bOverride_Camera"))
+    double Camera = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Scale", meta=(InlineEditConditionToggle))
+    bool bOverride_Light = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Scale", meta=(EditCondition="bOverride_Light"))
+    double Light = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Scale", meta=(InlineEditConditionToggle))
+    bool bOverride_Selectpoint = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Scale", meta=(EditCondition="bOverride_Selectpoint"))
+    double Selectpoint = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Scale", meta=(InlineEditConditionToggle))
+    bool bOverride_Jointlength = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Scale", meta=(EditCondition="bOverride_Jointlength"))
+    double Jointlength = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Scale", meta=(InlineEditConditionToggle))
+    bool bOverride_Jointwidth = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Scale", meta=(EditCondition="bOverride_Jointwidth"))
+    double Jointwidth = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Scale", meta=(InlineEditConditionToggle))
+    bool bOverride_Actuatorlength = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Scale", meta=(EditCondition="bOverride_Actuatorlength"))
+    double Actuatorlength = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Scale", meta=(InlineEditConditionToggle))
+    bool bOverride_Actuatorwidth = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Scale", meta=(EditCondition="bOverride_Actuatorwidth"))
+    double Actuatorwidth = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Scale", meta=(InlineEditConditionToggle))
+    bool bOverride_Framelength = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Scale", meta=(EditCondition="bOverride_Framelength"))
+    double Framelength = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Scale", meta=(InlineEditConditionToggle))
+    bool bOverride_Framewidth = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Scale", meta=(EditCondition="bOverride_Framewidth"))
+    double Framewidth = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Scale", meta=(InlineEditConditionToggle))
+    bool bOverride_Constraint = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Scale", meta=(EditCondition="bOverride_Constraint"))
+    double Constraint = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Scale", meta=(InlineEditConditionToggle))
+    bool bOverride_Slidercrank = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Scale", meta=(EditCondition="bOverride_Slidercrank"))
+    double Slidercrank = 0.0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Visual|Scale", meta=(InlineEditConditionToggle))
+    bool bOverride_Frustum = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Visual|Scale", meta=(EditCondition="bOverride_Frustum"))
+    double Frustum = 0.0;
+
+    /** @brief Mirror every field into a runtime C struct. */
+    template <typename TDst>
+    void ApplyTo(TDst& Dst) const
+    {
+    Dst.forcewidth = static_cast<decltype(Dst.forcewidth)>(Forcewidth);
+    Dst.contactwidth = static_cast<decltype(Dst.contactwidth)>(Contactwidth);
+    Dst.contactheight = static_cast<decltype(Dst.contactheight)>(Contactheight);
+    Dst.connect = static_cast<decltype(Dst.connect)>(Connect);
+    Dst.com = static_cast<decltype(Dst.com)>(Com);
+    Dst.camera = static_cast<decltype(Dst.camera)>(Camera);
+    Dst.light = static_cast<decltype(Dst.light)>(Light);
+    Dst.selectpoint = static_cast<decltype(Dst.selectpoint)>(Selectpoint);
+    Dst.jointlength = static_cast<decltype(Dst.jointlength)>(Jointlength);
+    Dst.jointwidth = static_cast<decltype(Dst.jointwidth)>(Jointwidth);
+    Dst.actuatorlength = static_cast<decltype(Dst.actuatorlength)>(Actuatorlength);
+    Dst.actuatorwidth = static_cast<decltype(Dst.actuatorwidth)>(Actuatorwidth);
+    Dst.framelength = static_cast<decltype(Dst.framelength)>(Framelength);
+    Dst.framewidth = static_cast<decltype(Dst.framewidth)>(Framewidth);
+    Dst.constraint = static_cast<decltype(Dst.constraint)>(Constraint);
+    Dst.slidercrank = static_cast<decltype(Dst.slidercrank)>(Slidercrank);
+    Dst.frustum = static_cast<decltype(Dst.frustum)>(Frustum);
+    }
+};
diff --git a/Source/URLab/Public/MuJoCo/Generated/MjsCompilerOptions.h b/Source/URLab/Public/MuJoCo/Generated/MjsCompilerOptions.h
new file mode 100644
index 0000000..6670e01
--- /dev/null
+++ b/Source/URLab/Public/MuJoCo/Generated/MjsCompilerOptions.h
@@ -0,0 +1,149 @@
+// Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+//
+// 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.
+//
+// --- LEGAL DISCLAIMER ---
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
+// trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
+//
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
+// CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
+
+#pragma once
+
+#include "CoreMinimal.h"
+#include "MjsCompilerOptions.generated.h"
+
+// Mirror of MuJoCo's ``mjsCompiler`` (MJSCOMPILER_FIELDS). Codegen-owned —
+// hand-edits get clobbered on next regen. Add fields by editing
+// codegen_rules.json[synthetic_categories.MjsCompilerOptions].
+USTRUCT(BlueprintType)
+struct URLAB_API FMjsCompilerOptions
+{
+    GENERATED_BODY()
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Spec|Compiler", meta=(InlineEditConditionToggle))
+    bool bOverride_Autolimits = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Spec|Compiler", meta=(EditCondition="bOverride_Autolimits"))
+    uint8 Autolimits = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Spec|Compiler", meta=(InlineEditConditionToggle))
+    bool bOverride_Boundmass = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Spec|Compiler", meta=(EditCondition="bOverride_Boundmass"))
+    float Boundmass = 0.0f;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Spec|Compiler", meta=(InlineEditConditionToggle))
+    bool bOverride_Boundinertia = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Spec|Compiler", meta=(EditCondition="bOverride_Boundinertia"))
+    float Boundinertia = 0.0f;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Spec|Compiler", meta=(InlineEditConditionToggle))
+    bool bOverride_Settotalmass = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Spec|Compiler", meta=(EditCondition="bOverride_Settotalmass"))
+    float Settotalmass = 0.0f;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Spec|Compiler", meta=(InlineEditConditionToggle))
+    bool bOverride_Balanceinertia = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Spec|Compiler", meta=(EditCondition="bOverride_Balanceinertia"))
+    uint8 Balanceinertia = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Spec|Compiler", meta=(InlineEditConditionToggle))
+    bool bOverride_Fitaabb = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Spec|Compiler", meta=(EditCondition="bOverride_Fitaabb"))
+    uint8 Fitaabb = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Spec|Compiler", meta=(InlineEditConditionToggle))
+    bool bOverride_Degree = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Spec|Compiler", meta=(EditCondition="bOverride_Degree"))
+    uint8 Degree = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Spec|Compiler", meta=(InlineEditConditionToggle))
+    bool bOverride_Eulerseq = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Spec|Compiler", meta=(EditCondition="bOverride_Eulerseq"))
+    FString Eulerseq = TEXT("xyz");
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Spec|Compiler", meta=(InlineEditConditionToggle))
+    bool bOverride_Discardvisual = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Spec|Compiler", meta=(EditCondition="bOverride_Discardvisual"))
+    uint8 Discardvisual = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Spec|Compiler", meta=(InlineEditConditionToggle))
+    bool bOverride_Usethread = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Spec|Compiler", meta=(EditCondition="bOverride_Usethread"))
+    uint8 Usethread = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Spec|Compiler", meta=(InlineEditConditionToggle))
+    bool bOverride_Fusestatic = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Spec|Compiler", meta=(EditCondition="bOverride_Fusestatic"))
+    uint8 Fusestatic = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Spec|Compiler", meta=(InlineEditConditionToggle))
+    bool bOverride_Inertiafromgeom = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Spec|Compiler", meta=(EditCondition="bOverride_Inertiafromgeom"))
+    int32 Inertiafromgeom = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Spec|Compiler", meta=(InlineEditConditionToggle))
+    bool bOverride_Inertiagrouprange = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Spec|Compiler", meta=(EditCondition="bOverride_Inertiagrouprange"))
+    TArray<float> Inertiagrouprange = {};
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Spec|Compiler", meta=(InlineEditConditionToggle))
+    bool bOverride_Saveinertial = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Spec|Compiler", meta=(EditCondition="bOverride_Saveinertial"))
+    uint8 Saveinertial = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Spec|Compiler", meta=(InlineEditConditionToggle))
+    bool bOverride_Alignfree = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Spec|Compiler", meta=(EditCondition="bOverride_Alignfree"))
+    int32 Alignfree = 0;
+
+    /** @brief Mirror every field into a runtime C struct. */
+    template <typename TDst>
+    void ApplyTo(TDst& Dst) const
+    {
+    Dst.autolimits = static_cast<decltype(Dst.autolimits)>(Autolimits);
+    Dst.boundmass = static_cast<decltype(Dst.boundmass)>(Boundmass);
+    Dst.boundinertia = static_cast<decltype(Dst.boundinertia)>(Boundinertia);
+    Dst.settotalmass = static_cast<decltype(Dst.settotalmass)>(Settotalmass);
+    Dst.balanceinertia = static_cast<decltype(Dst.balanceinertia)>(Balanceinertia);
+    Dst.fitaabb = static_cast<decltype(Dst.fitaabb)>(Fitaabb);
+    Dst.degree = static_cast<decltype(Dst.degree)>(Degree);
+    Dst.discardvisual = static_cast<decltype(Dst.discardvisual)>(Discardvisual);
+    Dst.usethread = static_cast<decltype(Dst.usethread)>(Usethread);
+    Dst.fusestatic = static_cast<decltype(Dst.fusestatic)>(Fusestatic);
+    Dst.inertiafromgeom = static_cast<decltype(Dst.inertiafromgeom)>(Inertiafromgeom);
+    for (int32 I = 0; I < Inertiagrouprange.Num() && I < 2; ++I)
+        Dst.inertiagrouprange[I] = static_cast<decltype(Dst.inertiagrouprange[0])>(Inertiagrouprange[I]);
+    Dst.saveinertial = static_cast<decltype(Dst.saveinertial)>(Saveinertial);
+    Dst.alignfree = static_cast<decltype(Dst.alignfree)>(Alignfree);
+    }
+};
diff --git a/Source/URLab/Public/MuJoCo/Generated/MjsSpec.h b/Source/URLab/Public/MuJoCo/Generated/MjsSpec.h
new file mode 100644
index 0000000..7b2b61b
--- /dev/null
+++ b/Source/URLab/Public/MuJoCo/Generated/MjsSpec.h
@@ -0,0 +1,149 @@
+// Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+//
+// 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.
+//
+// --- LEGAL DISCLAIMER ---
+// UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with,
+// endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are
+// trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
+//
+// This plugin incorporates third-party software: MuJoCo (Apache 2.0),
+// CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
+//
+// AUTOGENERATED by Scripts/codegen/generate_ue_components.py
+// Do not edit by hand. Re-run the generator to regenerate.
+
+#pragma once
+
+#include "CoreMinimal.h"
+#include "MjsSpec.generated.h"
+
+// Mirror of MuJoCo's ``mjSpec`` (MJSPEC_FIELDS). Codegen-owned —
+// hand-edits get clobbered on next regen. Add fields by editing
+// codegen_rules.json[synthetic_categories.MjsSpec].
+USTRUCT(BlueprintType)
+struct URLAB_API FMjsSpec
+{
+    GENERATED_BODY()
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Spec", meta=(InlineEditConditionToggle))
+    bool bOverride_Nemax = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Spec", meta=(EditCondition="bOverride_Nemax"))
+    int32 Nemax = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Spec", meta=(InlineEditConditionToggle))
+    bool bOverride_Nuserdata = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Spec", meta=(EditCondition="bOverride_Nuserdata"))
+    int32 Nuserdata = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Spec", meta=(InlineEditConditionToggle))
+    bool bOverride_NuserBody = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Spec", meta=(EditCondition="bOverride_NuserBody"))
+    int32 NuserBody = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Spec", meta=(InlineEditConditionToggle))
+    bool bOverride_NuserJnt = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Spec", meta=(EditCondition="bOverride_NuserJnt"))
+    int32 NuserJnt = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Spec", meta=(InlineEditConditionToggle))
+    bool bOverride_NuserGeom = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Spec", meta=(EditCondition="bOverride_NuserGeom"))
+    int32 NuserGeom = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Spec", meta=(InlineEditConditionToggle))
+    bool bOverride_NuserSite = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Spec", meta=(EditCondition="bOverride_NuserSite"))
+    int32 NuserSite = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Spec", meta=(InlineEditConditionToggle))
+    bool bOverride_NuserCam = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Spec", meta=(EditCondition="bOverride_NuserCam"))
+    int32 NuserCam = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Spec", meta=(InlineEditConditionToggle))
+    bool bOverride_NuserTendon = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Spec", meta=(EditCondition="bOverride_NuserTendon"))
+    int32 NuserTendon = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Spec", meta=(InlineEditConditionToggle))
+    bool bOverride_NuserActuator = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Spec", meta=(EditCondition="bOverride_NuserActuator"))
+    int32 NuserActuator = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Spec", meta=(InlineEditConditionToggle))
+    bool bOverride_NuserSensor = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Spec", meta=(EditCondition="bOverride_NuserSensor"))
+    int32 NuserSensor = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Spec", meta=(InlineEditConditionToggle))
+    bool bOverride_Nkey = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Spec", meta=(EditCondition="bOverride_Nkey"))
+    int32 Nkey = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Spec", meta=(InlineEditConditionToggle))
+    bool bOverride_Njmax = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Spec", meta=(EditCondition="bOverride_Njmax"))
+    int32 Njmax = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Spec", meta=(InlineEditConditionToggle))
+    bool bOverride_Nconmax = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Spec", meta=(EditCondition="bOverride_Nconmax"))
+    int32 Nconmax = 0;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Spec", meta=(InlineEditConditionToggle))
+    bool bOverride_Nstack = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Spec", meta=(EditCondition="bOverride_Nstack"))
+    float Nstack = 0.0f;
+
+    UPROPERTY(EditAnywhere, Category = "MuJoCo|Spec", meta=(InlineEditConditionToggle))
+    bool bOverride_HasImplicitPluginElem = false;
+
+    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Spec", meta=(EditCondition="bOverride_HasImplicitPluginElem"))
+    uint8 HasImplicitPluginElem = 0;
+
+    /** @brief Mirror every field into a runtime C struct. */
+    template <typename TDst>
+    void ApplyTo(TDst& Dst) const
+    {
+    Dst.nemax = static_cast<decltype(Dst.nemax)>(Nemax);
+    Dst.nuserdata = static_cast<decltype(Dst.nuserdata)>(Nuserdata);
+    Dst.nuser_body = static_cast<decltype(Dst.nuser_body)>(NuserBody);
+    Dst.nuser_jnt = static_cast<decltype(Dst.nuser_jnt)>(NuserJnt);
+    Dst.nuser_geom = static_cast<decltype(Dst.nuser_geom)>(NuserGeom);
+    Dst.nuser_site = static_cast<decltype(Dst.nuser_site)>(NuserSite);
+    Dst.nuser_cam = static_cast<decltype(Dst.nuser_cam)>(NuserCam);
+    Dst.nuser_tendon = static_cast<decltype(Dst.nuser_tendon)>(NuserTendon);
+    Dst.nuser_actuator = static_cast<decltype(Dst.nuser_actuator)>(NuserActuator);
+    Dst.nuser_sensor = static_cast<decltype(Dst.nuser_sensor)>(NuserSensor);
+    Dst.nkey = static_cast<decltype(Dst.nkey)>(Nkey);
+    Dst.njmax = static_cast<decltype(Dst.njmax)>(Njmax);
+    Dst.nconmax = static_cast<decltype(Dst.nconmax)>(Nconmax);
+    Dst.nstack = static_cast<decltype(Dst.nstack)>(Nstack);
+    Dst.hasImplicitPluginElem = static_cast<decltype(Dst.hasImplicitPluginElem)>(HasImplicitPluginElem);
+    }
+};
diff --git a/Source/URLab/Public/MuJoCo/Generated/README.md b/Source/URLab/Public/MuJoCo/Generated/README.md
new file mode 100644
index 0000000..609a60d
--- /dev/null
+++ b/Source/URLab/Public/MuJoCo/Generated/README.md
@@ -0,0 +1,57 @@
+# Codegen-owned files (do NOT edit by hand)
+
+Everything in this directory is regenerated by
+`Scripts/codegen/generate_ue_components.py`. Hand-edits get clobbered
+on the next regen. The `--check` mode of the generator runs as a build
+gate (`Scripts/build_and_test.ps1`) so drift between source rules and
+on-disk output fails the build.
+
+## Files in this directory
+
+| File                       | Driver                                                          | Owner                                                          |
+|----------------------------|-----------------------------------------------------------------|----------------------------------------------------------------|
+| `MjOptionEnums.h`          | `codegen_rules.json[generated_enums.MjOptionEnums]`             | `_phase_generated_enums`                                       |
+| `MjStatistic.h`            | `codegen_rules.json[synthetic_categories.MjStatistic]`          | `_phase_synthetic_categories`                                  |
+| `MjOptionGenerated.h/.cpp` | `codegen_rules.json[synthetic_categories.MjOption]`             | `_phase_synthetic_categories` (with `emit_apply_methods: true`) |
+| `MjVisualGlobal.h`         | `codegen_rules.json[synthetic_categories.MjVisualGlobal]`       | `_phase_synthetic_categories`                                  |
+| `MjVisualHeadlight.h`      | `codegen_rules.json[synthetic_categories.MjVisualHeadlight]`    | `_phase_synthetic_categories`                                  |
+| `MjArticulationRegistry.h` | `codegen_rules.json[categories]` (walks every category)         | `_phase_articulation_registry`                                 |
+
+Hand-written companion files (NOT codegen-owned, edit freely):
+
+| File                              | Why hand-written                                                                              |
+|-----------------------------------|-----------------------------------------------------------------------------------------------|
+| `MjOptionGeneratedExtras.cpp`     | URLab pseudo-fields (MemoryMB → mjSpec.memory, enableflags bit-banging for MultiCCD / Sleep). The codegen-emitted `.cpp` calls into `ApplyMjOptionExtras` defined here. |
+
+## Codegen-managed regions in non-Generated files
+
+These hand-written .cpp files have CODEGEN_*_START / CODEGEN_*_END
+markers. The codegen owns content between the markers; everything else
+is hand-written.
+
+| Marker pair                            | File                                                                    |
+|----------------------------------------|-------------------------------------------------------------------------|
+| `CODEGEN_IMPORT_*` / `CODEGEN_EXPORT_*`| Most files under `Source/URLab/Private/MuJoCo/Components/`              |
+| `CODEGEN_SENSOR_TYPE_SWITCH_*`         | `Source/URLab/Private/MuJoCo/Components/Sensors/MjSensor.cpp`           |
+| `CODEGEN_SENSOR_TAG_TO_TYPE_*`         | `Source/URLab/Private/MuJoCo/Components/Sensors/MjSensor.cpp`           |
+| `CODEGEN_VIEW_*_FIELDS_*` / `CODEGEN_VIEW_*_BIND_*` | `Source/URLab/Public/MuJoCo/Utils/MjBind.h`                             |
+
+## How to refresh after editing rules
+
+```
+python Scripts/codegen/generate_ue_components.py
+```
+
+To verify drift before committing:
+
+```
+python Scripts/codegen/generate_ue_components.py --check
+```
+
+To make drift-diagnostics fatal:
+
+```
+python Scripts/codegen/generate_ue_components.py --strict
+```
+
+For deeper background, see `docs/guides/codegen_contributors.md`.
diff --git a/Source/URLab/Public/MuJoCo/Utils/MjBind.h b/Source/URLab/Public/MuJoCo/Utils/MjBind.h
index 20e75a7..47d4d79 100644
--- a/Source/URLab/Public/MuJoCo/Utils/MjBind.h
+++ b/Source/URLab/Public/MuJoCo/Utils/MjBind.h
@@ -56,31 +56,35 @@ struct GeomView {
     int id;
     const char* name;
 
-    // --- Config ---
-    int type;
-    mjtNum* size;
-    mjtNum* pos_offset;
-    mjtNum* quat_offset;
-    float* rgba;
-    int mat_id;
-    int body_id;
-    int dataid;
-    mjtNum* friction;
-    mjtNum* solref;
-    mjtNum* solimp;
-    mjtNum* solmix;
-    mjtNum* margin;
-    mjtNum* gap;
-    int contype;
-    int conaffinity;
-    int priority;
-    mjtNum* fluid_coef;
-    mjtNum* user;
-
-    // --- State ---
-    mjtNum* xpos;
-    mjtNum* xmat;
-
+    // --- CODEGEN_VIEW_GeomView_FIELDS_START ---
+    int geom_type;
+    int geom_contype;
+    int geom_conaffinity;
+    int geom_condim;
+    int geom_bodyid;
+    int geom_dataid;
+    int geom_matid;
+    int geom_group;
+    int geom_priority;
+    int geom_plugin;
+    mjtByte* geom_sameframe;
+    mjtNum* geom_solmix;
+    mjtNum* geom_solref;
+    mjtNum* geom_solimp;
+    mjtNum* geom_size;
+    mjtNum* geom_aabb;
+    mjtNum* geom_rbound;
+    mjtNum* geom_pos;
+    mjtNum* geom_quat;
+    mjtNum* geom_friction;
+    mjtNum* geom_margin;
+    mjtNum* geom_gap;
+    mjtNum* geom_fluid;
+    mjtNum* geom_user;
+    float* geom_rgba;
+    mjtNum* geom_xpos;
+    mjtNum* geom_xmat;
+    // --- CODEGEN_VIEW_GeomView_FIELDS_END ---
 
     GeomView() : _m(nullptr), _d(nullptr), id(-1) {}
 
@@ -88,56 +92,63 @@ struct GeomView {
         check(id >= 0 && id < m->ngeom);
         name = (m->name_geomadr[id] >= 0) ? m->names + m->name_geomadr[id] : nullptr;
 
-        type        = m->geom_type[id];
-        size        = m->geom_size + (id * 3);
-        pos_offset  = m->geom_pos + (id * 3);
-        quat_offset = m->geom_quat + (id * 4);
-        rgba        = m->geom_rgba + (id * 4);
-        mat_id      = m->geom_matid[id];
-        body_id     = m->geom_bodyid[id];
-        friction    = m->geom_friction + (id * 3);
-        solref      = m->geom_solref + (id * mjNREF);
-        solimp      = m->geom_solimp + (id * mjNIMP);
-        solmix      = m->geom_solmix + id;
-        margin      = m->geom_margin + id;
-        gap         = m->geom_gap + id;
-        contype     = m->geom_contype[id];
-        conaffinity = m->geom_conaffinity[id];
-        priority    = m->geom_priority[id];
-        fluid_coef  = m->geom_fluid + (id * mjNFLUID);
-        user        = m->geom_user + (id * m->nuser_geom);
-        dataid     = m->geom_dataid[id];
-
-        xpos = d->geom_xpos + (id * 3);
-        xmat = d->geom_xmat + (id * 9);
+    // --- CODEGEN_VIEW_GeomView_BIND_START ---
+    geom_type = m->geom_type[id];
+    geom_contype = m->geom_contype[id];
+    geom_conaffinity = m->geom_conaffinity[id];
+    geom_condim = m->geom_condim[id];
+    geom_bodyid = m->geom_bodyid[id];
+    geom_dataid = m->geom_dataid[id];
+    geom_matid = m->geom_matid[id];
+    geom_group = m->geom_group[id];
+    geom_priority = m->geom_priority[id];
+    geom_plugin = m->geom_plugin[id];
+    geom_sameframe = m->geom_sameframe + id * 1;
+    geom_solmix = m->geom_solmix + id * 1;
+    geom_solref = m->geom_solref + id * mjNREF;
+    geom_solimp = m->geom_solimp + id * mjNIMP;
+    geom_size = m->geom_size + id * 3;
+    geom_aabb = m->geom_aabb + id * 6;
+    geom_rbound = m->geom_rbound + id * 1;
+    geom_pos = m->geom_pos + id * 3;
+    geom_quat = m->geom_quat + id * 4;
+    geom_friction = m->geom_friction + id * 3;
+    geom_margin = m->geom_margin + id * 1;
+    geom_gap = m->geom_gap + id * 1;
+    geom_fluid = m->geom_fluid + id * mjNFLUID;
+    geom_user = m->geom_user + id * m->nuser_geom;
+    geom_rgba = m->geom_rgba + id * 4;
+    geom_xpos = d->geom_xpos + id * 3;
+    geom_xmat = d->geom_xmat + id * 9;
+    // --- CODEGEN_VIEW_GeomView_BIND_END ---
     }
 
     /** @brief Sets the friction coefficient (tangential) for this geom. */
     void SetFriction(float Value) {
-        if (friction) friction[0] = (mjtNum)Value;
+        if (geom_friction) geom_friction[0] = (mjtNum)Value;
     }
 
     /** @brief Sets the contact solver reference parameters (time constant, damping ratio). */
     void SetSolRef(float TimeConst, float DampRatio) {
-        if (solref) {
-            solref[0] = (mjtNum)TimeConst;
-            solref[1] = (mjtNum)DampRatio;
+        if (geom_solref) {
+            geom_solref[0] = (mjtNum)TimeConst;
+            geom_solref[1] = (mjtNum)DampRatio;
         }
     }
 
     /** @brief Sets the contact solver impedance parameters. */
     void SetSolImp(float Dmin, float Dmax, float Width) {
-        if (solimp) {
-            solimp[0] = (mjtNum)Dmin;
-            solimp[1] = (mjtNum)Dmax;
-            solimp[2] = (mjtNum)Width;
+        if (geom_solimp) {
+            geom_solimp[0] = (mjtNum)Dmin;
+            geom_solimp[1] = (mjtNum)Dmax;
+            geom_solimp[2] = (mjtNum)Width;
         }
     }
 
     FString ToString() const {
         FString Info = FString::Printf(TEXT("=== Geom ID: %d (%s) ===\n"), id, name ? *MjUtils::MjToString(name) : TEXT("None"));
-        Info += FString::Printf(TEXT("    Type: %d | Size: %s\n"), type, *FormatVec3(size));
-        Info += FString::Printf(TEXT("    World Pos: %s\n"), *FormatVec3(xpos));
+        Info += FString::Printf(TEXT("    Type: %d | Size: %s\n"), geom_type, *FormatVec3(geom_size));
+        Info += FString::Printf(TEXT("    World Pos: %s\n"), *FormatVec3(geom_xpos));
         return Info;
     }
 };
@@ -153,65 +164,98 @@ struct JointView {
     mjData* _d;
     int id;
     const char* name;
-    int type;
-
-    // --- Config ---
-    mjtNum* pos_offset;
-    mjtNum* axis_local;
-    mjtNum* stiffness;
-    mjtNum* stiffnesspoly;   // high-order stiffness coefficients (mjNPOLY values)
-    mjtNum* range;
-    mjtNum* margin;
-    mjtNum* solref_limit;
-    mjtNum* solimp_limit;
-    mjtNum* damping;
-    mjtNum* dampingpoly;     // high-order damping coefficients (mjNPOLY values)
-    mjtNum* armature;
-    mjtNum* frictionloss;
-    mjtNum* solref_friction;
-    mjtNum* solimp_friction;
-    mjtNum* user;
-
-    // --- State ---
+
+    // --- CODEGEN_VIEW_JointView_FIELDS_START ---
+    int jnt_type;
+    int jnt_qposadr;
+    int jnt_dofadr;
+    int jnt_bodyid;
+    int jnt_actuatorid;
+    int jnt_group;
+    mjtByte* jnt_limited;
+    mjtByte* jnt_actfrclimited;
+    mjtByte* jnt_actgravcomp;
+    mjtNum* jnt_solref;
+    mjtNum* jnt_solimp;
+    mjtNum* jnt_pos;
+    mjtNum* jnt_axis;
+    mjtNum* jnt_stiffness;
+    mjtNum* jnt_stiffnesspoly;
+    mjtNum* jnt_range;
+    mjtNum* jnt_actfrcrange;
+    mjtNum* jnt_margin;
+    mjtNum* jnt_user;
+    int dof_bodyid;
+    int dof_jntid;
+    int dof_parentid;
+    int dof_treeid;
+    int dof_Madr;
+    int dof_simplenum;
+    mjtNum* dof_solref;
+    mjtNum* dof_solimp;
+    mjtNum* dof_frictionloss;
+    mjtNum* dof_armature;
+    mjtNum* dof_damping;
+    mjtNum* dof_dampingpoly;
+    mjtNum* dof_invweight0;
+    mjtNum* dof_M0;
+    mjtNum* dof_length;
     mjtNum* qpos;
     mjtNum* qvel;
     mjtNum* qacc;
     mjtNum* xanchor;
     mjtNum* xaxis;
-
+    // --- CODEGEN_VIEW_JointView_FIELDS_END ---
 
     JointView() : _m(nullptr), _d(nullptr), id(-1) {}
 
     JointView(const mjModel* m, mjData* d, int id_in) : _m(m), _d(d), id(id_in) {
         check(id >= 0 && id < m->njnt);
         name = (m->name_jntadr[id] >= 0) ? m->names + m->name_jntadr[id] : nullptr;
-        type = m->jnt_type[id];
-
-        int qpos_adr = m->jnt_qposadr[id];
-        int dof_adr  = m->jnt_dofadr[id];
-
-        pos_offset  = m->jnt_pos + (id * 3);
-        axis_local  = m->jnt_axis + (id * 3);
-        stiffness       = m->jnt_stiffness + id;
-        stiffnesspoly   = m->jnt_stiffnesspoly + (id * mjNPOLY);
-        range           = m->jnt_range + (id * 2);
-        margin      = m->jnt_margin + id;
-        solref_limit= m->jnt_solref + (id * mjNREF);
-        solimp_limit= m->jnt_solimp + (id * mjNIMP);
-        user        = m->jnt_user + (id * m->nuser_jnt);
-
-        damping         = m->dof_damping + dof_adr;
-        dampingpoly     = m->dof_dampingpoly + (dof_adr * mjNPOLY);
-        armature        = m->dof_armature + dof_adr;
-        frictionloss    = m->dof_frictionloss + dof_adr;
-        solref_friction = m->dof_solref + (dof_adr * mjNREF);
-        solimp_friction = m->dof_solimp + (dof_adr * mjNIMP);
-
-        qpos    = d->qpos + qpos_adr;
-        qvel    = d->qvel + dof_adr;
-        qacc    = d->qacc + dof_adr;
-        xanchor = d->xanchor + (id * 3);
-        xaxis   = d->xaxis + (id * 3);
+
+    // --- CODEGEN_VIEW_JointView_BIND_START ---
+    const int qpos_adr = m->jnt_qposadr[id];
+    const int dof_adr = m->jnt_dofadr[id];
+    jnt_type = m->jnt_type[id];
+    jnt_qposadr = m->jnt_qposadr[id];
+    jnt_dofadr = m->jnt_dofadr[id];
+    jnt_bodyid = m->jnt_bodyid[id];
+    jnt_actuatorid = m->jnt_actuatorid[id];
+    jnt_group = m->jnt_group[id];
+    jnt_limited = m->jnt_limited + id * 1;
+    jnt_actfrclimited = m->jnt_actfrclimited + id * 1;
+    jnt_actgravcomp = m->jnt_actgravcomp + id * 1;
+    jnt_solref = m->jnt_solref + id * mjNREF;
+    jnt_solimp = m->jnt_solimp + id * mjNIMP;
+    jnt_pos = m->jnt_pos + id * 3;
+    jnt_axis = m->jnt_axis + id * 3;
+    jnt_stiffness = m->jnt_stiffness + id * 1;
+    jnt_stiffnesspoly = m->jnt_stiffnesspoly + id * mjNPOLY;
+    jnt_range = m->jnt_range + id * 2;
+    jnt_actfrcrange = m->jnt_actfrcrange + id * 2;
+    jnt_margin = m->jnt_margin + id * 1;
+    jnt_user = m->jnt_user + id * m->nuser_jnt;
+    dof_bodyid = m->dof_bodyid[dof_adr];
+    dof_jntid = m->dof_jntid[dof_adr];
+    dof_parentid = m->dof_parentid[dof_adr];
+    dof_treeid = m->dof_treeid[dof_adr];
+    dof_Madr = m->dof_Madr[dof_adr];
+    dof_simplenum = m->dof_simplenum[dof_adr];
+    dof_solref = m->dof_solref + dof_adr * mjNREF;
+    dof_solimp = m->dof_solimp + dof_adr * mjNIMP;
+    dof_frictionloss = m->dof_frictionloss + dof_adr * 1;
+    dof_armature = m->dof_armature + dof_adr * 1;
+    dof_damping = m->dof_damping + dof_adr * 1;
+    dof_dampingpoly = m->dof_dampingpoly + dof_adr * mjNPOLY;
+    dof_invweight0 = m->dof_invweight0 + dof_adr * 1;
+    dof_M0 = m->dof_M0 + dof_adr * 1;
+    dof_length = m->dof_length + dof_adr * 1;
+    qpos = d->qpos + qpos_adr * 1;
+    qvel = d->qvel + dof_adr * 1;
+    qacc = d->qacc + dof_adr * 1;
+    xanchor = d->xanchor + id * 3;
+    xaxis = d->xaxis + id * 3;
+    // --- CODEGEN_VIEW_JointView_BIND_END ---
     }
 
     /** @brief Gets the current joint position (radians for hinges, meters for sliders). */
@@ -225,7 +269,7 @@ struct JointView {
     }
 
     FString ToString() const {
-        return FString::Printf(TEXT("=== Joint ID: %d (%s) Type: %d ===\n"), id, name ? *MjUtils::MjToString(name) : TEXT("None"), type);
+        return FString::Printf(TEXT("=== Joint ID: %d (%s) Type: %d ===\n"), id, name ? *MjUtils::MjToString(name) : TEXT("None"), jnt_type);
     }
 };
 
@@ -241,30 +285,45 @@ struct ActuatorView {
     int id;
     const char* name;
 
-    // --- Config (mjModel) ---
-    int trntype;
-    int dyntype;
-    int gaintype;
-    int biastype;
-    mjtNum* gear;
-    mjtNum* cranklength;
-    mjtNum* acc0;
-    mjtNum* length0;
-    mjtNum* lengthrange;
-    mjtNum* ctrlrange;
-    mjtNum* forcerange;
-    mjtNum* actrange;
-    mjtNum* gainprm;
-    mjtNum* biasprm;
-    mjtNum* dynprm;
-    
-    // --- State (mjData) ---
+    // --- CODEGEN_VIEW_ActuatorView_FIELDS_START ---
+    int actuator_trntype;
+    int actuator_dyntype;
+    int actuator_gaintype;
+    int actuator_biastype;
+    int* actuator_trnid;
+    mjtNum* actuator_damping;
+    mjtNum* actuator_dampingpoly;
+    mjtNum* actuator_armature;
+    int actuator_actadr;
+    int actuator_actnum;
+    int actuator_group;
+    int* actuator_history;
+    int actuator_historyadr;
+    mjtNum* actuator_delay;
+    mjtByte* actuator_ctrllimited;
+    mjtByte* actuator_forcelimited;
+    mjtByte* actuator_actlimited;
+    mjtNum* actuator_dynprm;
+    mjtNum* actuator_gainprm;
+    mjtNum* actuator_biasprm;
+    mjtByte* actuator_actearly;
+    mjtNum* actuator_ctrlrange;
+    mjtNum* actuator_forcerange;
+    mjtNum* actuator_actrange;
+    mjtNum* actuator_gear;
+    mjtNum* actuator_cranklength;
+    mjtNum* actuator_acc0;
+    mjtNum* actuator_length0;
+    mjtNum* actuator_lengthrange;
+    mjtNum* actuator_user;
+    int actuator_plugin;
     mjtNum* ctrl;
-    mjtNum* force;
-    mjtNum* length;
-    mjtNum* moment; // output: actuator moment
-    mjtNum* velocity;
-    mjtNum* act;    // internal state (activation)
+    mjtNum* actuator_force;
+    mjtNum* actuator_length;
+    mjtNum* actuator_velocity;
+    mjtNum* actuator_moment;
+    mjtNum* act;
+    // --- CODEGEN_VIEW_ActuatorView_FIELDS_END ---
 
     ActuatorView() : _m(nullptr), _d(nullptr), id(-1) {}
 
@@ -272,36 +331,45 @@ struct ActuatorView {
         check(id >= 0 && id < m->nu);
         name = (m->name_actuatoradr[id] >= 0) ? m->names + m->name_actuatoradr[id] : nullptr;
 
-        // Config
-        trntype = m->actuator_trntype[id];
-        dyntype = m->actuator_dyntype[id];
-        gaintype = m->actuator_gaintype[id];
-        biastype = m->actuator_biastype[id];
-        
-        gear = m->actuator_gear + (id * 6);
-        cranklength = m->actuator_cranklength + id;
-        acc0 = m->actuator_acc0 + id;
-        length0 = m->actuator_length0 + id;
-        lengthrange = m->actuator_lengthrange + (id * 2);
-        ctrlrange = m->actuator_ctrlrange + (id * 2);
-        forcerange = m->actuator_forcerange + (id * 2);
-        actrange = m->actuator_actrange + (id * 2);
-        
-        gainprm = m->actuator_gainprm + (id * mjNGAIN);
-        biasprm = m->actuator_biasprm + (id * mjNBIAS);
-        dynprm = m->actuator_dynprm + (id * mjNDYN);
-
-        // State
-        ctrl = d->ctrl + id;
-        force = d->actuator_force + id;
-        length = d->actuator_length + id;
-        moment = d->actuator_moment + (id * m->nv); // nactuator x nv matrix, row for this actuator
-        velocity = d->actuator_velocity + id;
-        int act_adr = m->actuator_actadr[id];
-        if (act_adr >= 0)
-            act = d->act + act_adr;
-        else
-            act = nullptr;
+    // --- CODEGEN_VIEW_ActuatorView_BIND_START ---
+    actuator_trntype = m->actuator_trntype[id];
+    actuator_dyntype = m->actuator_dyntype[id];
+    actuator_gaintype = m->actuator_gaintype[id];
+    actuator_biastype = m->actuator_biastype[id];
+    actuator_trnid = m->actuator_trnid + id * 2;
+    actuator_damping = m->actuator_damping + id * 1;
+    actuator_dampingpoly = m->actuator_dampingpoly + id * mjNPOLY;
+    actuator_armature = m->actuator_armature + id * 1;
+    actuator_actadr = m->actuator_actadr[id];
+    actuator_actnum = m->actuator_actnum[id];
+    actuator_group = m->actuator_group[id];
+    actuator_history = m->actuator_history + id * 2;
+    actuator_historyadr = m->actuator_historyadr[id];
+    actuator_delay = m->actuator_delay + id * 1;
+    actuator_ctrllimited = m->actuator_ctrllimited + id * 1;
+    actuator_forcelimited = m->actuator_forcelimited + id * 1;
+    actuator_actlimited = m->actuator_actlimited + id * 1;
+    actuator_dynprm = m->actuator_dynprm + id * mjNDYN;
+    actuator_gainprm = m->actuator_gainprm + id * mjNGAIN;
+    actuator_biasprm = m->actuator_biasprm + id * mjNBIAS;
+    actuator_actearly = m->actuator_actearly + id * 1;
+    actuator_ctrlrange = m->actuator_ctrlrange + id * 2;
+    actuator_forcerange = m->actuator_forcerange + id * 2;
+    actuator_actrange = m->actuator_actrange + id * 2;
+    actuator_gear = m->actuator_gear + id * 6;
+    actuator_cranklength = m->actuator_cranklength + id * 1;
+    actuator_acc0 = m->actuator_acc0 + id * 1;
+    actuator_length0 = m->actuator_length0 + id * 1;
+    actuator_lengthrange = m->actuator_lengthrange + id * 2;
+    actuator_user = m->actuator_user + id * m->nuser_actuator;
+    actuator_plugin = m->actuator_plugin[id];
+    ctrl = d->ctrl + id * 1;
+    actuator_force = d->actuator_force + id * 1;
+    actuator_length = d->actuator_length + id * 1;
+    actuator_velocity = d->actuator_velocity + id * 1;
+    actuator_moment = d->actuator_moment + id * m->nv;
+    act = (m->actuator_actadr[id] >= 0) ? d->act + m->actuator_actadr[id] : nullptr;
+    // --- CODEGEN_VIEW_ActuatorView_BIND_END ---
     }
 };
 
@@ -317,63 +385,93 @@ struct TendonView {
     int id;
     const char* name;
 
-    // --- Config (mjModel) ---
-    mjtNum* stiffness;
-    mjtNum* stiffnesspoly;   // high-order stiffness coefficients (mjNPOLY values)
-    mjtNum* damping;
-    mjtNum* dampingpoly;     // high-order damping coefficients (mjNPOLY values)
-    mjtNum* frictionloss;
-    mjtNum* armature;
-    mjtNum* range;
-    mjtNum* margin;
-    mjtNum* solref_limit;
-    mjtNum* solimp_limit;
-    mjtNum* solref_friction;
-    mjtNum* solimp_friction;
-
-    // --- State (mjData) ---
-    mjtNum* length;     // ten_length[id]
-    mjtNum* velocity;   // ten_velocity[id]
-
-    TendonView() : _m(nullptr), _d(nullptr), id(-1),
-        name(nullptr), stiffness(nullptr), stiffnesspoly(nullptr),
-        damping(nullptr), dampingpoly(nullptr),
-        frictionloss(nullptr), armature(nullptr), range(nullptr),
-        margin(nullptr), solref_limit(nullptr), solimp_limit(nullptr),
-        solref_friction(nullptr), solimp_friction(nullptr),
-        length(nullptr), velocity(nullptr) {}
+    // --- CODEGEN_VIEW_TendonView_FIELDS_START ---
+    int tendon_adr;
+    int tendon_num;
+    int tendon_matid;
+    int tendon_actuatorid;
+    int tendon_group;
+    int tendon_treenum;
+    int* tendon_treeid;
+    int ten_J_rownnz;
+    int ten_J_rowadr;
+    int ten_J_colind;
+    mjtByte* tendon_limited;
+    mjtByte* tendon_actfrclimited;
+    mjtNum* tendon_width;
+    mjtNum* tendon_solref_lim;
+    mjtNum* tendon_solimp_lim;
+    mjtNum* tendon_solref_fri;
+    mjtNum* tendon_solimp_fri;
+    mjtNum* tendon_range;
+    mjtNum* tendon_actfrcrange;
+    mjtNum* tendon_margin;
+    mjtNum* tendon_stiffness;
+    mjtNum* tendon_stiffnesspoly;
+    mjtNum* tendon_damping;
+    mjtNum* tendon_dampingpoly;
+    mjtNum* tendon_armature;
+    mjtNum* tendon_frictionloss;
+    mjtNum* tendon_lengthspring;
+    mjtNum* tendon_length0;
+    mjtNum* tendon_invweight0;
+    mjtNum* tendon_user;
+    float* tendon_rgba;
+    mjtNum* ten_length;
+    mjtNum* ten_velocity;
+    // --- CODEGEN_VIEW_TendonView_FIELDS_END ---
+
+    TendonView() : _m(nullptr), _d(nullptr), id(-1), name(nullptr) {}
 
     TendonView(const mjModel* m, mjData* d, int id_in) : _m(m), _d(d), id(id_in) {
         check(id >= 0 && id < m->ntendon);
         name = (m->name_tendonadr[id] >= 0) ? m->names + m->name_tendonadr[id] : nullptr;
 
-        // Config
-        stiffness       = m->tendon_stiffness + id;
-        stiffnesspoly   = m->tendon_stiffnesspoly + (id * mjNPOLY);
-        damping         = m->tendon_damping + id;
-        dampingpoly     = m->tendon_dampingpoly + (id * mjNPOLY);
-        frictionloss    = m->tendon_frictionloss + id;
-        armature        = m->tendon_armature + id;
-        range           = m->tendon_range + (id * 2);
-        margin          = m->tendon_margin + id;
-        solref_limit    = m->tendon_solref_lim + (id * mjNREF);
-        solimp_limit    = m->tendon_solimp_lim + (id * mjNIMP);
-        solref_friction = m->tendon_solref_fri + (id * mjNREF);
-        solimp_friction = m->tendon_solimp_fri + (id * mjNIMP);
-
-        // State
-        length   = d->ten_length   + id;
-        velocity = d->ten_velocity + id;
+    // --- CODEGEN_VIEW_TendonView_BIND_START ---
+    tendon_adr = m->tendon_adr[id];
+    tendon_num = m->tendon_num[id];
+    tendon_matid = m->tendon_matid[id];
+    tendon_actuatorid = m->tendon_actuatorid[id];
+    tendon_group = m->tendon_group[id];
+    tendon_treenum = m->tendon_treenum[id];
+    tendon_treeid = m->tendon_treeid + id * 2;
+    ten_J_rownnz = m->ten_J_rownnz[id];
+    ten_J_rowadr = m->ten_J_rowadr[id];
+    ten_J_colind = m->ten_J_colind[id];
+    tendon_limited = m->tendon_limited + id * 1;
+    tendon_actfrclimited = m->tendon_actfrclimited + id * 1;
+    tendon_width = m->tendon_width + id * 1;
+    tendon_solref_lim = m->tendon_solref_lim + id * mjNREF;
+    tendon_solimp_lim = m->tendon_solimp_lim + id * mjNIMP;
+    tendon_solref_fri = m->tendon_solref_fri + id * mjNREF;
+    tendon_solimp_fri = m->tendon_solimp_fri + id * mjNIMP;
+    tendon_range = m->tendon_range + id * 2;
+    tendon_actfrcrange = m->tendon_actfrcrange + id * 2;
+    tendon_margin = m->tendon_margin + id * 1;
+    tendon_stiffness = m->tendon_stiffness + id * 1;
+    tendon_stiffnesspoly = m->tendon_stiffnesspoly + id * mjNPOLY;
+    tendon_damping = m->tendon_damping + id * 1;
+    tendon_dampingpoly = m->tendon_dampingpoly + id * mjNPOLY;
+    tendon_armature = m->tendon_armature + id * 1;
+    tendon_frictionloss = m->tendon_frictionloss + id * 1;
+    tendon_lengthspring = m->tendon_lengthspring + id * 2;
+    tendon_length0 = m->tendon_length0 + id * 1;
+    tendon_invweight0 = m->tendon_invweight0 + id * 1;
+    tendon_user = m->tendon_user + id * m->nuser_tendon;
+    tendon_rgba = m->tendon_rgba + id * 4;
+    ten_length = d->ten_length + id * 1;
+    ten_velocity = d->ten_velocity + id * 1;
+    // --- CODEGEN_VIEW_TendonView_BIND_END ---
     }
 
     /** @brief Gets the current tendon length (meters). */
     float GetLength() const {
-        return length ? (float)*length : 0.0f;
+        return ten_length ? (float)*ten_length : 0.0f;
     }
 
     /** @brief Gets the current tendon velocity (m/s). */
     float GetVelocity() const {
-        return velocity ? (float)*velocity : 0.0f;
+        return ten_velocity ? (float)*ten_velocity : 0.0f;
     }
 };
 
@@ -389,19 +487,27 @@ struct SensorView {
     int id;
     const char* name;
 
-    // --- Config ---
-    int type;
-    int objtype;
-    int objid;
-    int reftype;
-    int refid;
-    int dim;
-    int adr;
-    mjtNum* cutoff;
-    mjtNum* noise;
-
-    // --- State ---
-    mjtNum* data; // Pointer to start of this sensor's data in sensordata
+    // --- CODEGEN_VIEW_SensorView_FIELDS_START ---
+    int sensor_type;
+    int sensor_datatype;
+    int sensor_needstage;
+    int sensor_objtype;
+    int sensor_objid;
+    int sensor_reftype;
+    int sensor_refid;
+    int* sensor_intprm;
+    int sensor_dim;
+    int sensor_adr;
+    mjtNum* sensor_cutoff;
+    mjtNum* sensor_noise;
+    int* sensor_history;
+    int sensor_historyadr;
+    mjtNum* sensor_delay;
+    mjtNum* sensor_interval;
+    mjtNum* sensor_user;
+    int sensor_plugin;
+    mjtNum* sensordata;
+    // --- CODEGEN_VIEW_SensorView_FIELDS_END ---
 
     SensorView() : _m(nullptr), _d(nullptr), id(-1) {}
 
@@ -409,21 +515,27 @@ struct SensorView {
         check(id >= 0 && id < m->nsensor);
         name = (m->name_sensoradr[id] >= 0) ? m->names + m->name_sensoradr[id] : nullptr;
 
-        type = m->sensor_type[id];
-        objtype = m->sensor_objtype[id];
-        objid = m->sensor_objid[id];
-        reftype = m->sensor_reftype[id];
-        refid = m->sensor_refid[id];
-        dim = m->sensor_dim[id];
-        adr = m->sensor_adr[id];
-        cutoff = m->sensor_cutoff + id;
-        noise = m->sensor_noise + id;
-
-        // State
-        if (adr >= 0 && adr < m->nsensordata)
-             data = d->sensordata + adr;
-        else
-             data = nullptr;
+    // --- CODEGEN_VIEW_SensorView_BIND_START ---
+    sensor_type = m->sensor_type[id];
+    sensor_datatype = m->sensor_datatype[id];
+    sensor_needstage = m->sensor_needstage[id];
+    sensor_objtype = m->sensor_objtype[id];
+    sensor_objid = m->sensor_objid[id];
+    sensor_reftype = m->sensor_reftype[id];
+    sensor_refid = m->sensor_refid[id];
+    sensor_intprm = m->sensor_intprm + id * mjNSENS;
+    sensor_dim = m->sensor_dim[id];
+    sensor_adr = m->sensor_adr[id];
+    sensor_cutoff = m->sensor_cutoff + id * 1;
+    sensor_noise = m->sensor_noise + id * 1;
+    sensor_history = m->sensor_history + id * 2;
+    sensor_historyadr = m->sensor_historyadr[id];
+    sensor_delay = m->sensor_delay + id * 1;
+    sensor_interval = m->sensor_interval + id * 2;
+    sensor_user = m->sensor_user + id * m->nuser_sensor;
+    sensor_plugin = m->sensor_plugin[id];
+    sensordata = (sensor_adr >= 0 && sensor_adr < m->nsensordata) ? d->sensordata + sensor_adr : nullptr;
+    // --- CODEGEN_VIEW_SensorView_BIND_END ---
     }
 };
 /**
@@ -438,52 +550,51 @@ struct SiteView {
     int id;
     const char* name;
 
-    // --- Config (mjModel) ---
-    int type;
-    mjtNum* size;
-    mjtNum* pos_offset;
-    mjtNum* quat_offset;
-    float* rgba;
-    int body_id;
-    int group;
-    mjtNum* user;
-
-    // --- State (mjData) ---
-    mjtNum* xpos;
-    mjtNum* xmat;
-
-    SiteView() : _m(nullptr), _d(nullptr), id(-1), name(nullptr),
-        type(0), size(nullptr), pos_offset(nullptr), quat_offset(nullptr),
-        rgba(nullptr), body_id(-1), group(0), user(nullptr),
-        xpos(nullptr), xmat(nullptr) {}
+    // --- CODEGEN_VIEW_SiteView_FIELDS_START ---
+    int site_type;
+    int site_bodyid;
+    int site_matid;
+    int site_group;
+    mjtByte* site_sameframe;
+    mjtNum* site_size;
+    mjtNum* site_pos;
+    mjtNum* site_quat;
+    mjtNum* site_user;
+    float* site_rgba;
+    mjtNum* site_xpos;
+    mjtNum* site_xmat;
+    // --- CODEGEN_VIEW_SiteView_FIELDS_END ---
+
+    SiteView() : _m(nullptr), _d(nullptr), id(-1), name(nullptr) {}
 
     SiteView(const mjModel* m, mjData* d, int id_in) : _m(m), _d(d), id(id_in) {
         check(id >= 0 && id < m->nsite);
         name = (m->name_siteadr[id] >= 0) ? m->names + m->name_siteadr[id] : nullptr;
 
-        // Config
-        type        = m->site_type[id];
-        size        = m->site_size + (id * 3);
-        pos_offset  = m->site_pos + (id * 3);
-        quat_offset = m->site_quat + (id * 4);
-        rgba        = m->site_rgba + (id * 4);
-        body_id     = m->site_bodyid[id];
-        group       = m->site_group[id];
-        user        = m->site_user + (id * m->nuser_site);
-
-        // State
-        xpos = d->site_xpos + (id * 3);
-        xmat = d->site_xmat + (id * 9);
+    // --- CODEGEN_VIEW_SiteView_BIND_START ---
+    site_type = m->site_type[id];
+    site_bodyid = m->site_bodyid[id];
+    site_matid = m->site_matid[id];
+    site_group = m->site_group[id];
+    site_sameframe = m->site_sameframe + id * 1;
+    site_size = m->site_size + id * 3;
+    site_pos = m->site_pos + id * 3;
+    site_quat = m->site_quat + id * 4;
+    site_user = m->site_user + id * m->nuser_site;
+    site_rgba = m->site_rgba + id * 4;
+    site_xpos = d->site_xpos + id * 3;
+    site_xmat = d->site_xmat + id * 9;
+    // --- CODEGEN_VIEW_SiteView_BIND_END ---
     }
 
     FVector GetWorldPosition() const {
-        return MjUtils::MjToUEPosition(xpos);
+        return MjUtils::MjToUEPosition(site_xpos);
     }
 
     FString ToString() const {
         FString Info = FString::Printf(TEXT("=== Site ID: %d (%s) ===\n"), id, name ? *MjUtils::MjToString(name) : TEXT("None"));
-        Info += FString::Printf(TEXT("    Type: %d | Size: %s\n"), type, *FormatVec3(size));
-        Info += FString::Printf(TEXT("    World Pos: %s\n"), *FormatVec3(xpos));
+        Info += FString::Printf(TEXT("    Type: %d | Size: %s\n"), site_type, *FormatVec3(site_size));
+        Info += FString::Printf(TEXT("    World Pos: %s\n"), *FormatVec3(site_xpos));
         return Info;
     }
 };
@@ -504,57 +615,99 @@ struct BodyView {
     // --- Identification ---
     const char* name;
 
-    // --- Configuration (mjModel) ---
-    mjtNum* mass;
-    mjtNum* inertia;
-    mjtNum* ipos;
-    mjtNum* iquat;
-    mjtNum* gravcomp;
-    mjtNum* pos_offset;
-    mjtNum* quat_offset;
-    mjtNum* user;
-    int mocap_id;
-
-    // --- State (mjData) ---
+    // --- CODEGEN_VIEW_BodyView_FIELDS_START ---
+    int body_parentid;
+    int body_rootid;
+    int body_weldid;
+    int body_mocapid;
+    int body_jntnum;
+    int body_jntadr;
+    int body_dofnum;
+    int body_dofadr;
+    int body_treeid;
+    int body_geomnum;
+    int body_geomadr;
+    mjtByte* body_simple;
+    mjtByte* body_sameframe;
+    mjtNum* body_pos;
+    mjtNum* body_quat;
+    mjtNum* body_ipos;
+    mjtNum* body_iquat;
+    mjtNum* body_mass;
+    mjtNum* body_subtreemass;
+    mjtNum* body_inertia;
+    mjtNum* body_invweight0;
+    mjtNum* body_gravcomp;
+    mjtNum* body_margin;
+    mjtNum* body_user;
+    int body_plugin;
+    int body_contype;
+    int body_conaffinity;
+    int body_bvhadr;
+    int body_bvhnum;
+    int tree_bodyadr;
+    int tree_bodynum;
+    int tree_dofadr;
+    int tree_dofnum;
+    int tree_sleep_policy;
     mjtNum* xpos;
     mjtNum* xquat;
-    mjtNum* xipos;
-    mjtNum* ximat;
+    mjtNum* xmat;
     mjtNum* cvel;
-    mjtNum* cacc;
+    mjtNum* cinert;
     mjtNum* xfrc_applied;
-    
+    // --- CODEGEN_VIEW_BodyView_FIELDS_END ---
+
     // Initialize critical members to safe defaults so a failed bind doesn't cause a crash later
-    BodyView() : _m(nullptr), _d(nullptr), id(-1), name(nullptr),
-        mass(nullptr), inertia(nullptr), ipos(nullptr), iquat(nullptr), gravcomp(nullptr),
-        pos_offset(nullptr), quat_offset(nullptr), user(nullptr), mocap_id(-1),
-        xpos(nullptr), xquat(nullptr), xipos(nullptr), ximat(nullptr),
-        cvel(nullptr), cacc(nullptr), xfrc_applied(nullptr) {}
+    BodyView() : _m(nullptr), _d(nullptr), id(-1), name(nullptr) {}
 
     BodyView(const mjModel* m, mjData* d, int id_in) : _m(m), _d(d), id(id_in) {
         check(id >= 0 && id < m->nbody);
         // Name resolution
         name = (m->name_bodyadr[id] >= 0) ? m->names + m->name_bodyadr[id] : nullptr;
 
-        // Model (Const params)
-        mass        = m->body_mass + id;
-        inertia     = m->body_inertia + (id * 3);
-        ipos        = m->body_ipos + (id * 3);
-        iquat       = m->body_iquat + (id * 4);
-        gravcomp    = m->body_gravcomp + id;
-        pos_offset  = m->body_pos + (id * 3);
-        quat_offset = m->body_quat + (id * 4);
-        user        = m->body_user + (id * m->nuser_body);
-        mocap_id    = m->body_mocapid[id];
-
-        // Data (Dynamic state)
-        xpos         = d->xpos + (id * 3);
-        xquat        = d->xquat + (id * 4);
-        xipos        = d->xipos + (id * 3);
-        ximat        = d->ximat + (id * 9);
-        cvel         = d->cvel + (id * 6);
-        cacc         = d->cacc + (id * 6);
-        xfrc_applied = d->xfrc_applied + (id * 6);
+    // --- CODEGEN_VIEW_BodyView_BIND_START ---
+    body_parentid = m->body_parentid[id];
+    body_rootid = m->body_rootid[id];
+    body_weldid = m->body_weldid[id];
+    body_mocapid = m->body_mocapid[id];
+    body_jntnum = m->body_jntnum[id];
+    body_jntadr = m->body_jntadr[id];
+    body_dofnum = m->body_dofnum[id];
+    body_dofadr = m->body_dofadr[id];
+    body_treeid = m->body_treeid[id];
+    body_geomnum = m->body_geomnum[id];
+    body_geomadr = m->body_geomadr[id];
+    body_simple = m->body_simple + id * 1;
+    body_sameframe = m->body_sameframe + id * 1;
+    body_pos = m->body_pos + id * 3;
+    body_quat = m->body_quat + id * 4;
+    body_ipos = m->body_ipos + id * 3;
+    body_iquat = m->body_iquat + id * 4;
+    body_mass = m->body_mass + id * 1;
+    body_subtreemass = m->body_subtreemass + id * 1;
+    body_inertia = m->body_inertia + id * 3;
+    body_invweight0 = m->body_invweight0 + id * 2;
+    body_gravcomp = m->body_gravcomp + id * 1;
+    body_margin = m->body_margin + id * 1;
+    body_user = m->body_user + id * m->nuser_body;
+    body_plugin = m->body_plugin[id];
+    body_contype = m->body_contype[id];
+    body_conaffinity = m->body_conaffinity[id];
+    body_bvhadr = m->body_bvhadr[id];
+    body_bvhnum = m->body_bvhnum[id];
+    tree_bodyadr = m->tree_bodyadr[id];
+    tree_bodynum = m->tree_bodynum[id];
+    tree_dofadr = m->tree_dofadr[id];
+    tree_dofnum = m->tree_dofnum[id];
+    tree_sleep_policy = m->tree_sleep_policy[id];
+    xpos = d->xpos + id * 3;
+    xquat = d->xquat + id * 4;
+    xmat = d->xmat + id * 9;
+    cvel = d->cvel + id * 6;
+    cinert = d->cinert + id * 10;
+    xfrc_applied = d->xfrc_applied + id * 6;
+    // --- CODEGEN_VIEW_BodyView_BIND_END ---
     }
 
     FVector GetWorldPosition() const {
@@ -578,7 +731,7 @@ struct BodyView {
     /** @brief Applies a world-space wrench (Force and Torque) to this body. */
     void ApplyWrench(FVector UEForce, FVector UETorque) {
         if (!xfrc_applied) return;
-        
+
         // Force: Convert and add to indices 3, 4, 5
         mjtNum MjForce[3];
         MjUtils::UEToMjPosition(UEForce, MjForce);
@@ -587,11 +740,9 @@ struct BodyView {
         xfrc_applied[5] += MjForce[2];
 
         // Torque: Convert and add to indices 0, 1, 2
-        // Note: Torque doesn't require the 0.01 scale factor (cm->m) in the same way, 
+        // Note: Torque doesn't require the 0.01 scale factor (cm->m) in the same way,
         // but it does require the Y-flip and coordinate mapping.
-        // UEToMjRotation handles the flip/mapping part if treated as a vector.
-        // However, torque is often just a vector. 
-        // Looking at UMjBody::ApplyForce, it uses Torque.X, -Torque.Y, Torque.Z.
+        // UMjBody::ApplyForce uses Torque.X, -Torque.Y, Torque.Z.
         xfrc_applied[0] += (mjtNum)UETorque.X;
         xfrc_applied[1] += (mjtNum)-UETorque.Y;
         xfrc_applied[2] += (mjtNum)UETorque.Z;
@@ -604,10 +755,10 @@ struct BodyView {
 
     FString ToString() const {
         FString Info = FString::Printf(TEXT("=== Body ID: %d (%s) ===\n"), id, name ? *MjUtils::MjToString(name) : TEXT("None"));
-        
+
         Info += TEXT("  [Config]\n");
-        Info += FString::Printf(TEXT("    Mass: %.4f\n"), *mass);
-        Info += FString::Printf(TEXT("    Rel Pos: %s\n"), *FormatVec3(pos_offset));
+        Info += FString::Printf(TEXT("    Mass: %.4f\n"), *body_mass);
+        Info += FString::Printf(TEXT("    Rel Pos: %s\n"), *FormatVec3(body_pos));
 
         Info += TEXT("  [State]\n");
         Info += FString::Printf(TEXT("    World Pos: %s\n"), *FormatVec3(xpos));
@@ -677,8 +828,8 @@ inline void LogBodyHierarchy(const BodyView& RootBody, int IndentLevel = 0) {
         FString GeomLog = FString::Printf(TEXT("%s  - (Geom ID: %d) Type: %d | Size: %s"), 
             *IndentString, 
             Geom.id, 
-            Geom.type, 
-            *FormatVec3(Geom.size)); // Uses the FormatVec3 helper defined previously
+            Geom.geom_type,
+            *FormatVec3(Geom.geom_size)); // Uses the FormatVec3 helper defined previously
         
         UE_LOG(LogURLabBind, Verbose, TEXT("%s"), *GeomLog);
     }
diff --git a/Source/URLab/Public/MuJoCo/Utils/MjOrientationUtils.h b/Source/URLab/Public/MuJoCo/Utils/MjOrientationUtils.h
index 7b9e808..bbdcc06 100644
--- a/Source/URLab/Public/MuJoCo/Utils/MjOrientationUtils.h
+++ b/Source/URLab/Public/MuJoCo/Utils/MjOrientationUtils.h
@@ -114,17 +114,4 @@ class URLAB_API MjOrientationUtils
 
     /** Convert zaxis (3 values) to quaternion via minimal rotation from (0,0,1). */
     static void ZAxisToQuat(const double ZAxis[3], double OutQuat[4]);
-
-private:
-    /** Create a quaternion for rotation around a single axis (0=x, 1=y, 2=z) by AngleRad. */
-    static void ElementalRotQuat(int AxisIndex, double AngleRad, double OutQuat[4]);
-
-    /** Multiply two quaternions: Result = A * B (Hamilton product). All in w,x,y,z order. */
-    static void QuatMul(const double A[4], const double B[4], double Result[4]);
-
-    /** Normalize a quaternion in-place. */
-    static void QuatNormalize(double Q[4]);
-
-    /** Convert a 3x3 rotation matrix (row-major) to quaternion (w,x,y,z). */
-    static void RotMatToQuat(const double R[9], double OutQuat[4]);
 };
diff --git a/Source/URLab/Public/MuJoCo/Utils/MjUtils.h b/Source/URLab/Public/MuJoCo/Utils/MjUtils.h
index 8b1b853..003b5a6 100644
--- a/Source/URLab/Public/MuJoCo/Utils/MjUtils.h
+++ b/Source/URLab/Public/MuJoCo/Utils/MjUtils.h
@@ -176,3 +176,68 @@ class URLAB_API MjUtils
      */
     static FString PrettifyName(const FString& Name, const FString& PrefixToStrip = TEXT(""));
 };
+
+// ---------------------------------------------------------------------------
+// Free inline helpers for the most common MuJoCo-side write patterns.
+// These collapse the `if (!X.IsEmpty()) mjs_setString(Field, TCHAR_TO_UTF8(*X));`
+// boilerplate that appears in ~13 places across URLab components.
+// ---------------------------------------------------------------------------
+
+/**
+ * @brief Write a UE FString to an mjString* field, but only if non-empty.
+ *
+ * Replaces the gated pattern:
+ *   if (!Name.IsEmpty()) mjs_setString(Element->name, TCHAR_TO_UTF8(*Name));
+ * with:
+ *   MjSetString(Element->name, Name);
+ *
+ * @param Field  The mjString* field (e.g. Element->childclass).
+ * @param Value  The UE FString to write. No-op if empty.
+ */
+inline void MjSetString(mjString* Field, const FString& Value)
+{
+    if (!Value.IsEmpty())
+    {
+        mjs_setString(Field, TCHAR_TO_UTF8(*Value));
+    }
+}
+
+/**
+ * @brief Unconditionally write a UE FString to an mjString* field.
+ *
+ * Use when the caller has already verified the value should be written
+ * (e.g. registered mesh asset name, attached body identifier). Does NOT
+ * skip empty strings — the caller's responsibility.
+ *
+ * @param Field  The mjString* field.
+ * @param Value  The UE FString to write.
+ */
+inline void MjSetStringRaw(mjString* Field, const FString& Value)
+{
+    mjs_setString(Field, TCHAR_TO_UTF8(*Value));
+}
+
+/**
+ * @brief Overwrite an mjDoubleVec* with the contents of a TArray<float>.
+ *
+ * The mjs spec exposes mjsKey::qpos / qvel / ... as ``mjDoubleVec*``
+ * (a std::vector<double>* in disguise). The standard write pattern is
+ * ``clear() + push_back per element``; this helper packages that.
+ * Used both by codegen-emitted exports and by URLab's hand-written
+ * freejoint-padding path in MjKeyframe.
+ *
+ * @param Dest  The mjDoubleVec* destination. No-op if null.
+ * @param Src   The float source array. Each element is widened to double.
+ */
+inline void MjSetDoubleVec(mjDoubleVec* Dest, const TArray<float>& Src)
+{
+    if (!Dest)
+    {
+        return;
+    }
+    Dest->clear();
+    for (float V : Src)
+    {
+        Dest->push_back(static_cast<double>(V));
+    }
+}
diff --git a/Source/URLab/Public/MuJoCo/Utils/URLabAxisConv.h b/Source/URLab/Public/MuJoCo/Utils/URLabAxisConv.h
new file mode 100644
index 0000000..bbb482b
--- /dev/null
+++ b/Source/URLab/Public/MuJoCo/Utils/URLabAxisConv.h
@@ -0,0 +1,103 @@
+// Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+
+#pragma once
+
+#include "CoreMinimal.h"
+
+// ============================================================================
+// URLabAxisConv
+//
+// Centralised MuJoCo <-> Unreal handedness + units conversion helpers. The
+// codegen and the hand-rolled import/export paths converge here so a future
+// audit only has to scrub one file (instead of grepping for inlined
+// expressions like ``Out.Y = -In.Y;`` and ``X / 100.0`` scattered across
+// every component .cpp).
+//
+// Gating:
+//   URLAB_USE_AXIS_CONV_HELPERS=1 routes codegen emission through these
+//   helpers. Defaults to 0 (codegen stays inline) so existing serialized
+//   data + compiled artifacts stay byte-identical until the migration flips.
+//
+// Conventions (mirroring the existing MjUtils:: shape):
+//   * Position: MuJoCo metres -> UE centimetres, Y-axis sign flipped.
+//   * Direction: same handedness flip but NO scale.
+//   * Rotation: wxyz -> xyzw + X/Z sign flip for RHS -> LHS.
+//   * Per-element apply mode (UE -> MuJoCo) inverts each rule.
+// ============================================================================
+
+#ifndef URLAB_USE_AXIS_CONV_HELPERS
+    #define URLAB_USE_AXIS_CONV_HELPERS 0
+#endif
+
+namespace URLabAxisConv
+{
+    /** @brief MuJoCo position (metres, RHS) -> UE FVector (centimetres, LHS). */
+    FORCEINLINE FVector MjPositionToUe(const double InM[3])
+    {
+        return FVector(
+            static_cast<FVector::FReal>( InM[0] * 100.0),
+            static_cast<FVector::FReal>(-InM[1] * 100.0),
+            static_cast<FVector::FReal>( InM[2] * 100.0));
+    }
+
+    /** @brief UE FVector position (cm, LHS) -> MuJoCo metres (RHS). */
+    FORCEINLINE void UePositionToMj(const FVector& In, double OutM[3])
+    {
+        OutM[0] =  static_cast<double>(In.X) / 100.0;
+        OutM[1] = -static_cast<double>(In.Y) / 100.0;
+        OutM[2] =  static_cast<double>(In.Z) / 100.0;
+    }
+
+    /** @brief MuJoCo direction (RHS, no scale) -> UE FVector (LHS, no scale). */
+    FORCEINLINE FVector MjDirectionToUe(const double In[3])
+    {
+        return FVector(
+            static_cast<FVector::FReal>( In[0]),
+            static_cast<FVector::FReal>(-In[1]),
+            static_cast<FVector::FReal>( In[2]));
+    }
+
+    /** @brief UE direction -> MuJoCo direction (LHS -> RHS, no scale). */
+    FORCEINLINE void UeDirectionToMj(const FVector& In, double Out[3])
+    {
+        Out[0] =  static_cast<double>(In.X);
+        Out[1] = -static_cast<double>(In.Y);
+        Out[2] =  static_cast<double>(In.Z);
+    }
+
+    /** @brief MuJoCo quaternion (wxyz, RHS) -> UE FQuat (xyzw, LHS). */
+    FORCEINLINE FQuat MjQuatToUe(const double InWXYZ[4])
+    {
+        // wxyz -> xyzw + X/Z negate so the resulting LHS quat matches
+        // the converted MjPositionToUe vectors.
+        return FQuat(
+            static_cast<FQuat::FReal>(-InWXYZ[1]),
+            static_cast<FQuat::FReal>( InWXYZ[2]),
+            static_cast<FQuat::FReal>(-InWXYZ[3]),
+            static_cast<FQuat::FReal>( InWXYZ[0])).GetNormalized();
+    }
+
+    /** @brief UE FQuat (LHS) -> MuJoCo quaternion (wxyz, RHS). */
+    FORCEINLINE void UeQuatToMj(const FQuat& In, double OutWXYZ[4])
+    {
+        OutWXYZ[0] =  static_cast<double>(In.W);
+        OutWXYZ[1] = -static_cast<double>(In.X);
+        OutWXYZ[2] =  static_cast<double>(In.Y);
+        OutWXYZ[3] = -static_cast<double>(In.Z);
+    }
+
+    /** @brief Scalar-array Y-negate apply (in-place). Used by direction
+     *  attrs like joint.axis where the codegen handles cm scaling
+     *  separately (vec3_convert = "y_negate" rule path). */
+    FORCEINLINE void YNegate(FVector& InOut) { InOut.Y = -InOut.Y; }
+
+    /** @brief Per-element scale for fixed-size MJ array fields. The
+     *  codegen array-export loop reads through this so a future units
+     *  change (e.g. radians -> degrees on a specific subtype) doesn't
+     *  need a per-call-site edit. */
+    FORCEINLINE double ScaleMToCm(double InM) { return InM * 100.0; }
+    FORCEINLINE double ScaleCmToM(double InCm) { return InCm * 0.01; }
+} // namespace URLabAxisConv
diff --git a/Source/URLab/Public/MuJoCo/Utils/URLabLocaleSafeFloat.h b/Source/URLab/Public/MuJoCo/Utils/URLabLocaleSafeFloat.h
new file mode 100644
index 0000000..0b1b508
--- /dev/null
+++ b/Source/URLab/Public/MuJoCo/Utils/URLabLocaleSafeFloat.h
@@ -0,0 +1,76 @@
+// Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+
+#pragma once
+
+#include "CoreMinimal.h"
+
+// ============================================================================
+// URLabLocaleSafeFloat
+//
+// FString::Printf("%f", X) respects the user's C locale and on a system
+// running e.g. de_DE.UTF-8 it writes "1,5" instead of "1.5". MuJoCo's
+// MJCF parser rejects comma-decimal floats; round-trip through Unreal
+// on such a host shipped XML the simulator can't reload.
+//
+// These helpers emit a stable decimal-point representation regardless
+// of locale. The codegen-emitted xml_passthrough body and the hand-
+// rolled MJCF exporters route through them so the resulting XML
+// parses on any host.
+//
+// Gating:
+//   URLAB_USE_LOCALE_SAFE_FLOAT=1 routes codegen + hand-rolled emit
+//   paths through these helpers. Defaults to 0 (no-op) so the existing
+//   format strings stay untouched until the migration flips.
+// ============================================================================
+
+#ifndef URLAB_USE_LOCALE_SAFE_FLOAT
+    #define URLAB_USE_LOCALE_SAFE_FLOAT 0
+#endif
+
+namespace URLabLocaleSafeFloat
+{
+    /** @brief Format ``InValue`` with ``Precision`` fractional digits
+     *  using ``.`` as the decimal separator regardless of the host
+     *  C locale. Returns an FString suitable for concatenation into
+     *  MJCF attr strings. */
+    FORCEINLINE FString Format(double InValue, int32 Precision = 6)
+    {
+        // FString::SanitizeFloat respects locale; build the formatter
+        // ourselves: integer part + "." + fractional part. Negative
+        // numbers carry their sign through the integer part.
+        if (FMath::IsNaN(InValue))
+        {
+            return TEXT("nan");
+        }
+        if (!FMath::IsFinite(InValue))
+        {
+            return InValue < 0.0 ? TEXT("-inf") : TEXT("inf");
+        }
+        const bool bNegative = InValue < 0.0;
+        const double Abs = FMath::Abs(InValue);
+        const int64 IntPart = static_cast<int64>(Abs);
+        double FracPart = Abs - static_cast<double>(IntPart);
+        // Round the fractional part to ``Precision`` digits before
+        // emitting so trailing 0.999999... doesn't print 1 too many.
+        const double Scale = FMath::Pow(10.0, static_cast<double>(Precision));
+        FracPart = FMath::RoundToDouble(FracPart * Scale) / Scale;
+        const int64 FracDigits = static_cast<int64>(FracPart * Scale);
+        return FString::Printf(
+            TEXT("%s%lld.%0*lld"),
+            bNegative ? TEXT("-") : TEXT(""),
+            IntPart,
+            Precision,
+            FracDigits);
+    }
+
+    /** @brief Append a single attr-key=value pair using locale-safe
+     *  float emission. Used by the codegen xml_passthrough body
+     *  emitters when the rule sets ``locale_safe: true`` on the attr. */
+    FORCEINLINE FString Attr(const TCHAR* Name, double InValue, int32 Precision = 6)
+    {
+        return FString::Printf(TEXT(" %s=\"%s\""), Name, *Format(InValue, Precision));
+    }
+} // namespace URLabLocaleSafeFloat
diff --git a/Source/URLab/RobotBuilder.py b/Source/URLab/RobotBuilder.py
deleted file mode 100644
index 294c599..0000000
--- a/Source/URLab/RobotBuilder.py
+++ /dev/null
@@ -1,78 +0,0 @@
-# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
-#
-# 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.
-#
-# --- LEGAL DISCLAIMER ---
-# UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, 
-# endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are 
-# trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere.
-#
-# This plugin incorporates third-party software: MuJoCo (Apache 2.0), 
-# CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
-
-
-import unreal
-# Load the Blueprint asset
-bp_asset = unreal.load_asset('/Game/NewBlueprint.NewBlueprint')
-# Function to get the currently selected asset in the Content Browser
-def get_selected_assets_in_content_browser():
-    selected_assets = unreal.EditorUtilityLibrary.get_selected_assets()
-    if not selected_assets:
-        print("No asset selected in the Content Browser!")
-        return None
-
-    for asset in selected_assets:
-        print(f"Selected Asset: {asset.get_path_name()}")
-        return asset  # Return the first selected asset for simplicity
-selected_bp =   get_selected_assets_in_content_browser()
-print(selected_bp)
-if selected_bp:
-
-    print(f"You selected: {selected_bp}")
-    bp_asset = selected_bp
-# Get the SubobjectDataSubsystem and gather subobject data for the Blueprint
-subsystem: unreal.SubobjectDataSubsystem = unreal.get_engine_subsystem(unreal.SubobjectDataSubsystem)
-root_data_handle: unreal.SubobjectDataHandle = subsystem.k2_gather_subobject_data_for_blueprint(context=bp_asset)
-
-objects = []
-# Gather all components in the Blueprint
-for handle in root_data_handle:
-    subobject = subsystem.k2_find_subobject_data_from_handle(handle)
-    objects.append(unreal.SubobjectDataBlueprintFunctionLibrary.get_object(subobject))
-
-# Remove duplicate references
-objects = list(dict.fromkeys(objects))
-
-# Iterate over objects and check if they are StaticMeshComponents
-for obj in objects:
-    if "StaticMeshComponent" in str(obj):
-        static_mesh = obj.get_editor_property("static_mesh")
-        print(f"StaticMeshComponent: {obj}, StaticMesh: {static_mesh}")
-
-        if str(static_mesh) == "None":
-            print(f"StaticMeshComponent: {obj} has no static_mesh")
-        else:
-            # Clean up the component name
-            component_name = obj.get_name()
-            clean_name = component_name.replace("_GEN_VARIABLE", "")
-            
-            # Retrieve existing tags
-            existing_tags = obj.get_editor_property("component_tags")
-
-            if clean_name not in existing_tags:
-                updated_tags =  [clean_name]
-                obj.set_editor_property("component_tags", updated_tags)
-
-                print(f"Added tag '{clean_name}' to StaticMeshComponent: {obj}")
-            else:
-                print(f"Tag '{clean_name}' already exists for StaticMeshComponent: {obj}")
diff --git a/Source/URLabEditor/Private/MjComponentDetailCustomizations.cpp b/Source/URLabEditor/Private/MjComponentDetailCustomizations.cpp
index 318ebfd..5563ce6 100644
--- a/Source/URLabEditor/Private/MjComponentDetailCustomizations.cpp
+++ b/Source/URLabEditor/Private/MjComponentDetailCustomizations.cpp
@@ -21,167 +21,18 @@
 // CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details.
 
 #include "MjComponentDetailCustomizations.h"
+
 #include "DetailLayoutBuilder.h"
 #include "DetailCategoryBuilder.h"
 #include "DetailWidgetRow.h"
 #include "Widgets/Input/SButton.h"
 #include "Widgets/Text/STextBlock.h"
 
-#include "MuJoCo/Components/Actuators/MjActuator.h"
-#include "MuJoCo/Components/Sensors/MjSensor.h"
-#include "MuJoCo/Components/Physics/MjContactPair.h"
-#include "MuJoCo/Components/Physics/MjContactExclude.h"
-#include "MuJoCo/Components/Constraints/MjEquality.h"
-#include "MuJoCo/Components/Bodies/MjBody.h"
-#include "MuJoCo/Components/Defaults/MjDefault.h"
-#include "MuJoCo/Components/Joints/MjJoint.h"
 #include "MuJoCo/Components/Geometry/MjGeom.h"
-#include "MuJoCo/Components/Geometry/MjSite.h"
-#include "MuJoCo/Components/Tendons/MjTendon.h"
-
-// Detail customizations that hide internal properties from the editor UI.
-
-// ============================================================================
-// Actuator — hide DefaultClass pointer
-// ============================================================================
-
-TSharedRef<IDetailCustomization> FMjActuatorDetailCustomization::MakeInstance()
-{
-    return MakeShareable(new FMjActuatorDetailCustomization);
-}
-
-void FMjActuatorDetailCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)
-{
-    DetailBuilder.HideProperty(DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UMjActuator, DefaultClass)));
-}
-
-// ============================================================================
-// Sensor — hide DefaultClass pointer
-// ============================================================================
-
-TSharedRef<IDetailCustomization> FMjSensorDetailCustomization::MakeInstance()
-{
-    return MakeShareable(new FMjSensorDetailCustomization);
-}
-
-void FMjSensorDetailCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)
-{
-    DetailBuilder.HideProperty(DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UMjSensor, DefaultClass)));
-}
-
-// ============================================================================
-// Contact Pair — hide Name
-// ============================================================================
-
-TSharedRef<IDetailCustomization> FMjContactPairDetailCustomization::MakeInstance()
-{
-    return MakeShareable(new FMjContactPairDetailCustomization);
-}
-
-void FMjContactPairDetailCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)
-{
-    DetailBuilder.HideProperty(DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UMjContactPair, Name)));
-}
-
-// ============================================================================
-// Contact Exclude — hide Name
-// ============================================================================
-
-TSharedRef<IDetailCustomization> FMjContactExcludeDetailCustomization::MakeInstance()
-{
-    return MakeShareable(new FMjContactExcludeDetailCustomization);
-}
-
-void FMjContactExcludeDetailCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)
-{
-    DetailBuilder.HideProperty(DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UMjContactExclude, Name)));
-}
-
-// ============================================================================
-// Equality
-// ============================================================================
-
-TSharedRef<IDetailCustomization> FMjEqualityDetailCustomization::MakeInstance()
-{
-    return MakeShareable(new FMjEqualityDetailCustomization);
-}
-
-void FMjEqualityDetailCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)
-{
-}
 
 // ============================================================================
-// Joint — hide DefaultClass pointer
-// ============================================================================
-
-TSharedRef<IDetailCustomization> FMjJointDetailCustomization::MakeInstance()
-{
-    return MakeShareable(new FMjJointDetailCustomization);
-}
-
-void FMjJointDetailCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)
-{
-    DetailBuilder.HideProperty(DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UMjJoint, DefaultClass)));
-}
-
-// ============================================================================
-// Body
-// ============================================================================
-
-TSharedRef<IDetailCustomization> FMjBodyDetailCustomization::MakeInstance()
-{
-    return MakeShareable(new FMjBodyDetailCustomization);
-}
-
-void FMjBodyDetailCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)
-{
-}
-
-// ============================================================================
-// Default — hide ClassName and ParentClassName (synced from SCS hierarchy)
-// ============================================================================
-
-TSharedRef<IDetailCustomization> FMjDefaultDetailCustomization::MakeInstance()
-{
-    return MakeShareable(new FMjDefaultDetailCustomization);
-}
-
-void FMjDefaultDetailCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)
-{
-    DetailBuilder.HideProperty(DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UMjDefault, ClassName)));
-    DetailBuilder.HideProperty(DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UMjDefault, ParentClassName)));
-}
-
-// ============================================================================
-// Site — hide DefaultClass pointer
-// ============================================================================
-
-TSharedRef<IDetailCustomization> FMjSiteDetailCustomization::MakeInstance()
-{
-    return MakeShareable(new FMjSiteDetailCustomization);
-}
-
-void FMjSiteDetailCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)
-{
-    DetailBuilder.HideProperty(DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UMjSite, DefaultClass)));
-}
-
-// ============================================================================
-// Tendon — hide DefaultClass pointer
-// ============================================================================
-
-TSharedRef<IDetailCustomization> FMjTendonDetailCustomization::MakeInstance()
-{
-    return MakeShareable(new FMjTendonDetailCustomization);
-}
-
-void FMjTendonDetailCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)
-{
-    DetailBuilder.HideProperty(DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UMjTendon, DefaultClass)));
-}
-
-// ============================================================================
-// Geom — hide DefaultClass pointer + CoACD decomposition buttons
+// Geom — adds CoACD decomposition buttons (the only non-hiding logic).
+// DefaultClass hiding now lives on the UPROPERTY itself.
 // ============================================================================
 
 TSharedRef<IDetailCustomization> FMjGeomDetailCustomization::MakeInstance()
@@ -198,9 +49,7 @@ void FMjGeomDetailCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBu
     TWeakObjectPtr<UMjGeom> WeakGeom = Cast<UMjGeom>(Objects[0].Get());
     if (!WeakGeom.IsValid()) return;
 
-    DetailBuilder.HideProperty(DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UMjGeom, DefaultClass)));
-
-    // Decomposition buttons (only for mesh geoms)
+    // Decomposition buttons (only for mesh geoms).
     if (WeakGeom->Type != EMjGeomType::Mesh) return;
 
     IDetailCategoryBuilder& DecompCategory = DetailBuilder.EditCategory("MuJoCo|Geom|Decomposition");
diff --git a/Source/URLabEditor/Private/MujocoGenerationAction.cpp b/Source/URLabEditor/Private/MujocoGenerationAction.cpp
index ab1a61f..31466a4 100644
--- a/Source/URLabEditor/Private/MujocoGenerationAction.cpp
+++ b/Source/URLabEditor/Private/MujocoGenerationAction.cpp
@@ -243,7 +243,7 @@ void UMujocoGenerationAction::GenerateForBlueprintXml(UBlueprint* BP, const FStr
             AMjArticulation* CDO = Cast<AMjArticulation>(BP->GeneratedClass->GetDefaultObject());
             if (CDO)
             {
-                FMuJoCoOptions& Opts = CDO->SimOptions;
+                FMjOptionGenerated& Opts = CDO->SimOptions;
 
                 auto ParseVec3 = [](const FString& Raw, FVector& Out)
                 {
diff --git a/Source/URLabEditor/Private/Tests/MjBindingPathProbeTest.cpp b/Source/URLabEditor/Private/Tests/MjBindingPathProbeTest.cpp
new file mode 100644
index 0000000..081ee8c
--- /dev/null
+++ b/Source/URLabEditor/Private/Tests/MjBindingPathProbeTest.cpp
@@ -0,0 +1,266 @@
+// Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved.
+//
+// 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.
+
+// Empirical probe: does Path 1 (mjs_getId from m_SpecElement) work in
+// current MuJoCo, or does the v0.1-alpha-era comment in MjComponent.h
+// ("mjs_getId can return stale/garbage IDs after mjs_attach merges")
+// still hold?
+//
+// For each component, after Compile + PostSetup:
+//   - Path 1 id   = mjs_getId(m_SpecElement)
+//   - Path 2 id   = mj_name2id(model, type, m_prefix + name)
+//   - Bind result = component's view.id
+// Test logs all three via UE_LOG(Display) and decides Path 1 vs Path 2
+// success per component. Test always passes — its job is to report,
+// not to gate.
+
+#include "CoreMinimal.h"
+#include "Misc/AutomationTest.h"
+#include "Tests/AutomationEditorCommon.h"
+#include "Engine/World.h"
+#include "GameFramework/Actor.h"
+#include "MuJoCo/Components/Bodies/MjBody.h"
+#include "MuJoCo/Components/Bodies/MjWorldBody.h"
+#include "MuJoCo/Components/Geometry/MjGeom.h"
+#include "MuJoCo/Components/Joints/MjJoint.h"
+#include "MuJoCo/Core/AMjManager.h"
+#include "MuJoCo/Core/MjPhysicsEngine.h"
+#include "MuJoCo/Core/MjArticulation.h"
+#include <mujoco/mujoco.h>
+
+IMPLEMENT_SIMPLE_AUTOMATION_TEST(
+    FMjBindingPathProbeTest,
+    "URLab.MuJoCo.Binding.PathProbe",
+    EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter)
+
+namespace
+{
+    enum class EPathResult : uint8 { Success, Stale, Failed, Skipped };
+
+    struct FProbeResult
+    {
+        FString Component;
+        FString MjType;
+        int Path1Id;
+        int Path2Id;
+        int BindId;
+        EPathResult Path1Outcome;
+        EPathResult Path2Outcome;
+    };
+
+    const TCHAR* PathResultName(EPathResult R)
+    {
+        switch (R)
+        {
+        case EPathResult::Success: return TEXT("SUCCESS");
+        case EPathResult::Stale:   return TEXT("STALE_ID");
+        case EPathResult::Failed:  return TEXT("FAILED");
+        case EPathResult::Skipped: return TEXT("SKIPPED");
+        }
+        return TEXT("?");
+    }
+
+    EPathResult ClassifyPath1(int Id, int MaxCount, void* SpecElement)
+    {
+        if (!SpecElement) return EPathResult::Skipped;
+        if (Id < 0) return EPathResult::Failed;
+        if (Id >= MaxCount) return EPathResult::Stale;
+        return EPathResult::Success;
+    }
+
+    EPathResult ClassifyPath2(int Id)
+    {
+        return Id >= 0 ? EPathResult::Success : EPathResult::Failed;
+    }
+
+    int GetMjCountFor(const mjModel* m, mjtObj Type)
+    {
+        switch (Type)
+        {
+        case mjOBJ_BODY:     return m->nbody;
+        case mjOBJ_JOINT:    return m->njnt;
+        case mjOBJ_GEOM:     return m->ngeom;
+        case mjOBJ_SITE:     return m->nsite;
+        case mjOBJ_CAMERA:   return m->ncam;
+        case mjOBJ_LIGHT:    return m->nlight;
+        case mjOBJ_MESH:     return m->nmesh;
+        case mjOBJ_HFIELD:   return m->nhfield;
+        case mjOBJ_TEXTURE:  return m->ntex;
+        case mjOBJ_MATERIAL: return m->nmat;
+        case mjOBJ_PAIR:     return m->npair;
+        case mjOBJ_EXCLUDE:  return m->nexclude;
+        case mjOBJ_EQUALITY: return m->neq;
+        case mjOBJ_TENDON:   return m->ntendon;
+        case mjOBJ_ACTUATOR: return m->nu;
+        case mjOBJ_SENSOR:   return m->nsensor;
+        case mjOBJ_NUMERIC:  return m->nnumeric;
+        case mjOBJ_TEXT:     return m->ntext;
+        case mjOBJ_TUPLE:    return m->ntuple;
+        case mjOBJ_KEY:      return m->nkey;
+        case mjOBJ_PLUGIN:   return m->nplugin;
+        default:             return 0;
+        }
+    }
+
+    FProbeResult Probe(
+        const TCHAR* Label,
+        mjtObj Type,
+        UMjComponent* Comp,
+        const FString& Prefix,
+        const mjModel* Model,
+        int BindId)
+    {
+        FProbeResult R;
+        R.Component = Label;
+        R.MjType = FString::Printf(TEXT("%d"), (int)Type);
+        R.BindId = BindId;
+
+        const int MaxCount = GetMjCountFor(Model, Type);
+        mjsElement* SpecEl = Comp->GetSpecElementForDiagnostics();
+        R.Path1Id = SpecEl ? mjs_getId(SpecEl) : -1;
+        R.Path1Outcome = ClassifyPath1(R.Path1Id, MaxCount, SpecEl);
+
+        const FString NameToLookup = Comp->MjName.IsEmpty() ? Comp->GetName() : Comp->MjName;
+        const FString PrefixedName = Prefix + NameToLookup;
+        R.Path2Id = mj_name2id(Model, Type, TCHAR_TO_UTF8(*PrefixedName));
+        R.Path2Outcome = ClassifyPath2(R.Path2Id);
+
+        return R;
+    }
+}
+
+bool FMjBindingPathProbeTest::RunTest(const FString& Parameters)
+{
+    UWorld* World = UWorld::CreateWorld(EWorldType::Game, false);
+    if (!World) { AddError(TEXT("Failed to create temporary world.")); return false; }
+    FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Game);
+    WorldContext.SetCurrentWorld(World);
+
+    FActorSpawnParameters SpawnParams;
+    AMjArticulation* Robot = World->SpawnActor<AMjArticulation>(SpawnParams);
+    if (!Robot) { AddError(TEXT("Failed to spawn AMjArticulation.")); World->DestroyWorld(false); GEngine->DestroyWorldContext(World); return false; }
+
+    UMjWorldBody* WorldBody = NewObject<UMjWorldBody>(Robot, TEXT("WorldBody"));
+    Robot->SetRootComponent(WorldBody);
+    WorldBody->RegisterComponent();
+
+    UMjBody* RootBody = NewObject<UMjBody>(Robot, TEXT("RootBody"));
+    RootBody->RegisterComponent();
+    RootBody->AttachToComponent(WorldBody, FAttachmentTransformRules::KeepRelativeTransform);
+
+    UMjGeom* Geom = NewObject<UMjGeom>(Robot, TEXT("TestGeom"));
+    Geom->size = { 0.1f, 0.1f, 0.1f };
+    Geom->bOverride_size = true;
+    Geom->RegisterComponent();
+    Geom->AttachToComponent(RootBody, FAttachmentTransformRules::KeepRelativeTransform);
+
+    UMjJoint* Joint = NewObject<UMjJoint>(Robot, TEXT("TestJoint"));
+    Joint->RegisterComponent();
+    Joint->AttachToComponent(RootBody, FAttachmentTransformRules::KeepRelativeTransform);
+
+    AAMjManager* Manager = World->SpawnActor<AAMjManager>(SpawnParams);
+    if (!Manager) { AddError(TEXT("Failed to spawn AAMjManager.")); World->DestroyWorld(false); GEngine->DestroyWorldContext(World); return false; }
+
+    // mj_compile fires inside Manager->Compile() (PhysicsEngine::Compile line 221).
+    // mjs_getId on a spec element is only meaningful after that point.
+    Manager->Compile();
+
+    if (!Manager->IsInitialized())
+    {
+        AddError(TEXT("Manager->IsInitialized() returned false after Compile — mj_compile did not produce a valid model. Cannot probe."));
+        World->DestroyWorld(false); GEngine->DestroyWorldContext(World); return false;
+    }
+
+    const mjModel* Model = Manager->PhysicsEngine ? Manager->PhysicsEngine->GetModel() : nullptr;
+    if (!Model)
+    {
+        AddError(TEXT("PhysicsEngine->GetModel() returned null after Compile."));
+        World->DestroyWorld(false); GEngine->DestroyWorldContext(World); return false;
+    }
+    if (Robot->bAttachFailed)
+    {
+        AddError(TEXT("Articulation reports bAttachFailed — mjs_attach did not move child spec into root. Probe would be meaningless."));
+        World->DestroyWorld(false); GEngine->DestroyWorldContext(World); return false;
+    }
+    if (Model->nbody < 2 || Model->ngeom < 1 || Model->njnt < 1)
+    {
+        AddError(FString::Printf(
+            TEXT("Compiled model is missing expected entities (nbody=%d, ngeom=%d, njnt=%d). Expected nbody>=2, ngeom>=1, njnt>=1. Probe aborted."),
+            Model->nbody, Model->ngeom, Model->njnt));
+        World->DestroyWorld(false); GEngine->DestroyWorldContext(World); return false;
+    }
+
+    const FString Prefix = Robot->GetPrefixForDiagnostics();
+    UE_LOG(LogTemp, Display, TEXT("[PathProbe] Articulation prefix: '%s'"), *Prefix);
+    UE_LOG(LogTemp, Display, TEXT("[PathProbe] Model counts: nbody=%d  ngeom=%d  njnt=%d  (mj_compile completed)"),
+        Model->nbody, Model->ngeom, Model->njnt);
+
+    TArray<FProbeResult> Results;
+    Results.Add(Probe(TEXT("RootBody"), mjOBJ_BODY,  RootBody, Prefix, Model, RootBody->GetMjID()));
+    Results.Add(Probe(TEXT("TestGeom"), mjOBJ_GEOM,  Geom,     Prefix, Model, Geom->GetMjID()));
+    Results.Add(Probe(TEXT("TestJoint"),mjOBJ_JOINT, Joint,    Prefix, Model, Joint->GetMjID()));
+
+    int Path1Successes = 0, Path1Stales = 0, Path1Failures = 0, Path1Skipped = 0;
+    int Path2Successes = 0, Path2Failures = 0;
+    int Path1MatchesBind = 0;
+    int Path2MatchesBind = 0;
+
+    for (const FProbeResult& R : Results)
+    {
+        UE_LOG(LogTemp, Display,
+            TEXT("[PathProbe] [%s] BindId=%d  Path1Id=%d (%s)  Path2Id=%d (%s)  Path1==Bind:%d Path2==Bind:%d"),
+            *R.Component, R.BindId, R.Path1Id, PathResultName(R.Path1Outcome),
+            R.Path2Id, PathResultName(R.Path2Outcome),
+            (int)(R.Path1Id == R.BindId && R.BindId >= 0),
+            (int)(R.Path2Id == R.BindId && R.BindId >= 0));
+
+        switch (R.Path1Outcome)
+        {
+        case EPathResult::Success: ++Path1Successes; break;
+        case EPathResult::Stale:   ++Path1Stales; break;
+        case EPathResult::Failed:  ++Path1Failures; break;
+        case EPathResult::Skipped: ++Path1Skipped; break;
+        }
+        switch (R.Path2Outcome)
+        {
+        case EPathResult::Success: ++Path2Successes; break;
+        default: ++Path2Failures; break;
+        }
+        if (R.Path1Outcome == EPathResult::Success && R.Path1Id == R.BindId) ++Path1MatchesBind;
+        if (R.Path2Outcome == EPathResult::Success && R.Path2Id == R.BindId) ++Path2MatchesBind;
+    }
+
+    UE_LOG(LogTemp, Display, TEXT("[PathProbe] === SUMMARY (n=%d) ==="), Results.Num());
+    UE_LOG(LogTemp, Display, TEXT("[PathProbe] Path1: success=%d  stale=%d  failed=%d  skipped=%d"),
+        Path1Successes, Path1Stales, Path1Failures, Path1Skipped);
+    UE_LOG(LogTemp, Display, TEXT("[PathProbe] Path2: success=%d  failed=%d"),
+        Path2Successes, Path2Failures);
+    UE_LOG(LogTemp, Display, TEXT("[PathProbe] Path1==Bind: %d/%d   Path2==Bind: %d/%d"),
+        Path1MatchesBind, Results.Num(), Path2MatchesBind, Results.Num());
+
+    if (Path1Stales == 0 && Path1Failures == 0 && Path1Successes == Results.Num())
+    {
+        UE_LOG(LogTemp, Display, TEXT("[PathProbe] VERDICT: Path 1 is reliable in this scenario. The 'stale IDs after mjs_attach' comment may be obsolete."));
+    }
+    else
+    {
+        UE_LOG(LogTemp, Display, TEXT("[PathProbe] VERDICT: Path 1 failed on %d/%d components. The fallback IS load-bearing."),
+            Path1Stales + Path1Failures, Results.Num());
+    }
+
+    Manager->PhysicsEngine->bShouldStopTask = true;
+    World->DestroyWorld(false);
+    GEngine->DestroyWorldContext(World);
+    return true;
+}
diff --git a/Source/URLabEditor/Private/Tests/MjBindingTest.cpp b/Source/URLabEditor/Private/Tests/MjBindingTest.cpp
index eb689cd..1b425c0 100644
--- a/Source/URLabEditor/Private/Tests/MjBindingTest.cpp
+++ b/Source/URLabEditor/Private/Tests/MjBindingTest.cpp
@@ -105,7 +105,7 @@ bool FMjBindingIntegrationTest::RunTest(const FString& Parameters)
     {
         float InitialFriction = 0.5f;
         Geom->SetFriction(InitialFriction);
-        TestEqual(TEXT("Geom friction should be updated"), Geom->m_GeomView.friction[0], (double)InitialFriction);
+        TestEqual(TEXT("Geom friction should be updated"), Geom->m_GeomView.geom_friction[0], (double)InitialFriction);
     }
 
     // 9. Cleanup
diff --git a/Source/URLabEditor/Private/Tests/MjPhysicsTests.cpp b/Source/URLabEditor/Private/Tests/MjPhysicsTests.cpp
index b269b3f..21d7c8a 100644
--- a/Source/URLabEditor/Private/Tests/MjPhysicsTests.cpp
+++ b/Source/URLabEditor/Private/Tests/MjPhysicsTests.cpp
@@ -502,6 +502,7 @@ bool FMjPhysicsSleep_BodySleepPolicyAllowed::RunTest(const FString& Parameters)
     {
         Sess.Manager->PhysicsEngine->Options.bEnableSleep = true;
         Sess.Body->SleepPolicy = EMjBodySleepPolicy::Allowed;
+        Sess.Body->bOverride_SleepPolicy = true;  // v8: explicit toggle required to write per-body policy
     }))
     {
         AddError(S.LastError);
diff --git a/Source/URLabEditor/Private/Tests/MjSchemaForTests.h b/Source/URLabEditor/Private/Tests/MjSchemaForTests.h
index f0e5532..1f5ba10 100644
--- a/Source/URLabEditor/Private/Tests/MjSchemaForTests.h
+++ b/Source/URLabEditor/Private/Tests/MjSchemaForTests.h
@@ -47,7 +47,7 @@ namespace URLabCodegenSchemaForTests
 
     inline const TArray<FString>& JointProps()
     {
-        static const TArray<FString> P{TEXT("group"), TEXT("springdamper"), TEXT("limited"), TEXT("actuatorfrclimited"), TEXT("solreflimit"), TEXT("solimplimit"), TEXT("solreffriction"), TEXT("solimpfriction"), TEXT("stiffness"), TEXT("range"), TEXT("actuatorfrcrange"), TEXT("actuatorgravcomp"), TEXT("margin"), TEXT("ref"), TEXT("springref"), TEXT("armature"), TEXT("damping"), TEXT("frictionloss")};
+        static const TArray<FString> P{TEXT("Pos"), TEXT("group"), TEXT("Axis"), TEXT("springdamper"), TEXT("limited"), TEXT("ActFrcLimited"), TEXT("solreflimit"), TEXT("solimplimit"), TEXT("solreffriction"), TEXT("solimpfriction"), TEXT("stiffness"), TEXT("range"), TEXT("ActFrcRange"), TEXT("actuatorgravcomp"), TEXT("margin"), TEXT("ref"), TEXT("springref"), TEXT("armature"), TEXT("damping"), TEXT("frictionloss")};
         return P;
     }
 
@@ -59,7 +59,7 @@ namespace URLabCodegenSchemaForTests
 
     inline const TArray<FString>& TendonProps()
     {
-        static const TArray<FString> P{TEXT("group"), TEXT("limited"), TEXT("actuatorfrclimited"), TEXT("range"), TEXT("actuatorfrcrange"), TEXT("solreflimit"), TEXT("solimplimit"), TEXT("solreffriction"), TEXT("solimpfriction"), TEXT("frictionloss"), TEXT("springlength"), TEXT("width"), TEXT("material"), TEXT("margin"), TEXT("stiffness"), TEXT("damping"), TEXT("armature"), TEXT("rgba")};
+        static const TArray<FString> P{TEXT("group"), TEXT("limited"), TEXT("ActFrcLimited"), TEXT("range"), TEXT("ActFrcRange"), TEXT("solreflimit"), TEXT("solimplimit"), TEXT("solreffriction"), TEXT("solimpfriction"), TEXT("frictionloss"), TEXT("springlength"), TEXT("width"), TEXT("material"), TEXT("margin"), TEXT("stiffness"), TEXT("damping"), TEXT("armature"), TEXT("rgba")};
         return P;
     }
 
diff --git a/Source/URLabEditor/Private/URLabEditor.cpp b/Source/URLabEditor/Private/URLabEditor.cpp
index f435b5c..3a5f0f1 100644
--- a/Source/URLabEditor/Private/URLabEditor.cpp
+++ b/Source/URLabEditor/Private/URLabEditor.cpp
@@ -58,7 +58,11 @@ void FURLabEditorModule::StartupModule()
     FMjEditorStyle::Initialize();
 
     FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
-    // Register geom customization on all subclasses
+    // Only Geom has non-hiding logic (the CoACD decomposition buttons).
+    // The other 9 components' simple "HideProperty(DefaultClass)" /
+    // "HideProperty(Name)" customizations were replaced by
+    // meta=(EditCondition="false", EditConditionHides) on the UPROPERTY
+    // decls themselves — see UMjActuator::DefaultClass et al.
     {
         TArray<UClass*> GeomClasses;
         GetDerivedClasses(UMjGeom::StaticClass(), GeomClasses, true);
@@ -70,63 +74,6 @@ void FURLabEditorModule::StartupModule()
                 FOnGetDetailCustomizationInstance::CreateStatic(&FMjGeomDetailCustomization::MakeInstance));
         }
     }
-    // Register actuator customization on all subclasses
-    {
-        TArray<UClass*> ActuatorClasses;
-        GetDerivedClasses(UMjActuator::StaticClass(), ActuatorClasses, true);
-        ActuatorClasses.Add(UMjActuator::StaticClass());
-        for (UClass* Class : ActuatorClasses)
-        {
-            PropertyModule.RegisterCustomClassLayout(
-                Class->GetFName(),
-                FOnGetDetailCustomizationInstance::CreateStatic(&FMjActuatorDetailCustomization::MakeInstance));
-        }
-    }
-    // Register sensor customization on all subclasses
-    {
-        TArray<UClass*> SensorClasses;
-        GetDerivedClasses(UMjSensor::StaticClass(), SensorClasses, true);
-        SensorClasses.Add(UMjSensor::StaticClass());
-        for (UClass* Class : SensorClasses)
-        {
-            PropertyModule.RegisterCustomClassLayout(
-                Class->GetFName(),
-                FOnGetDetailCustomizationInstance::CreateStatic(&FMjSensorDetailCustomization::MakeInstance));
-        }
-    }
-    PropertyModule.RegisterCustomClassLayout(
-        UMjContactPair::StaticClass()->GetFName(),
-        FOnGetDetailCustomizationInstance::CreateStatic(&FMjContactPairDetailCustomization::MakeInstance));
-    PropertyModule.RegisterCustomClassLayout(
-        UMjContactExclude::StaticClass()->GetFName(),
-        FOnGetDetailCustomizationInstance::CreateStatic(&FMjContactExcludeDetailCustomization::MakeInstance));
-    PropertyModule.RegisterCustomClassLayout(
-        UMjEquality::StaticClass()->GetFName(),
-        FOnGetDetailCustomizationInstance::CreateStatic(&FMjEqualityDetailCustomization::MakeInstance));
-    // Register joint customization on all subclasses
-    {
-        TArray<UClass*> JointClasses;
-        GetDerivedClasses(UMjJoint::StaticClass(), JointClasses, true);
-        JointClasses.Add(UMjJoint::StaticClass());
-        for (UClass* Class : JointClasses)
-        {
-            PropertyModule.RegisterCustomClassLayout(
-                Class->GetFName(),
-                FOnGetDetailCustomizationInstance::CreateStatic(&FMjJointDetailCustomization::MakeInstance));
-        }
-    }
-    PropertyModule.RegisterCustomClassLayout(
-        UMjBody::StaticClass()->GetFName(),
-        FOnGetDetailCustomizationInstance::CreateStatic(&FMjBodyDetailCustomization::MakeInstance));
-    PropertyModule.RegisterCustomClassLayout(
-        UMjDefault::StaticClass()->GetFName(),
-        FOnGetDetailCustomizationInstance::CreateStatic(&FMjDefaultDetailCustomization::MakeInstance));
-    PropertyModule.RegisterCustomClassLayout(
-        UMjSite::StaticClass()->GetFName(),
-        FOnGetDetailCustomizationInstance::CreateStatic(&FMjSiteDetailCustomization::MakeInstance));
-    PropertyModule.RegisterCustomClassLayout(
-        UMjTendon::StaticClass()->GetFName(),
-        FOnGetDetailCustomizationInstance::CreateStatic(&FMjTendonDetailCustomization::MakeInstance));
     PropertyModule.NotifyCustomizationModuleChanged();
 
     // Register viewport actor context menu extender
@@ -184,16 +131,6 @@ void FURLabEditorModule::ShutdownModule()
             }
         };
         UnregisterWithSubclasses(UMjGeom::StaticClass());
-        UnregisterWithSubclasses(UMjActuator::StaticClass());
-        UnregisterWithSubclasses(UMjSensor::StaticClass());
-        UnregisterWithSubclasses(UMjJoint::StaticClass());
-        PropertyModule.UnregisterCustomClassLayout(UMjContactPair::StaticClass()->GetFName());
-        PropertyModule.UnregisterCustomClassLayout(UMjContactExclude::StaticClass()->GetFName());
-        PropertyModule.UnregisterCustomClassLayout(UMjEquality::StaticClass()->GetFName());
-        PropertyModule.UnregisterCustomClassLayout(UMjBody::StaticClass()->GetFName());
-        PropertyModule.UnregisterCustomClassLayout(UMjDefault::StaticClass()->GetFName());
-        PropertyModule.UnregisterCustomClassLayout(UMjSite::StaticClass()->GetFName());
-        PropertyModule.UnregisterCustomClassLayout(UMjTendon::StaticClass()->GetFName());
     }
 
     if (FModuleManager::Get().IsModuleLoaded("LevelEditor"))
diff --git a/Source/URLabEditor/Public/MjComponentDetailCustomizations.h b/Source/URLabEditor/Public/MjComponentDetailCustomizations.h
index 03e8684..4e0e99b 100644
--- a/Source/URLabEditor/Public/MjComponentDetailCustomizations.h
+++ b/Source/URLabEditor/Public/MjComponentDetailCustomizations.h
@@ -24,87 +24,18 @@
 
 #include "IDetailCustomization.h"
 
-/** Detail customization for UMjActuator — hides internal properties. */
-class FMjActuatorDetailCustomization : public IDetailCustomization
-{
-public:
-    static TSharedRef<IDetailCustomization> MakeInstance();
-    virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override;
-};
-
-/** Detail customization for UMjSensor — hides internal properties. */
-class FMjSensorDetailCustomization : public IDetailCustomization
-{
-public:
-    static TSharedRef<IDetailCustomization> MakeInstance();
-    virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override;
-};
-
-/** Detail customization for UMjContactPair — hides internal properties. */
-class FMjContactPairDetailCustomization : public IDetailCustomization
-{
-public:
-    static TSharedRef<IDetailCustomization> MakeInstance();
-    virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override;
-};
-
-/** Detail customization for UMjContactExclude — hides internal properties. */
-class FMjContactExcludeDetailCustomization : public IDetailCustomization
-{
-public:
-    static TSharedRef<IDetailCustomization> MakeInstance();
-    virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override;
-};
-
-/** Detail customization for UMjEquality. */
-class FMjEqualityDetailCustomization : public IDetailCustomization
-{
-public:
-    static TSharedRef<IDetailCustomization> MakeInstance();
-    virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override;
-};
-
-/** Detail customization for UMjJoint — hides string properties. */
-class FMjJointDetailCustomization : public IDetailCustomization
-{
-public:
-    static TSharedRef<IDetailCustomization> MakeInstance();
-    virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override;
-};
-
-/** Detail customization for UMjBody. */
-class FMjBodyDetailCustomization : public IDetailCustomization
-{
-public:
-    static TSharedRef<IDetailCustomization> MakeInstance();
-    virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override;
-};
-
-/** Detail customization for UMjDefault — hides string properties synced at compile time. */
-class FMjDefaultDetailCustomization : public IDetailCustomization
-{
-public:
-    static TSharedRef<IDetailCustomization> MakeInstance();
-    virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override;
-};
-
-/** Detail customization for UMjSite — hides string properties. */
-class FMjSiteDetailCustomization : public IDetailCustomization
-{
-public:
-    static TSharedRef<IDetailCustomization> MakeInstance();
-    virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override;
-};
-
-/** Detail customization for UMjTendon — hides string properties. */
-class FMjTendonDetailCustomization : public IDetailCustomization
-{
-public:
-    static TSharedRef<IDetailCustomization> MakeInstance();
-    virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override;
-};
-
-/** Detail customization for UMjGeom — hides internal properties and adds CoACD decomposition buttons. */
+/**
+ * @class FMjGeomDetailCustomization
+ * @brief Adds CoACD decomposition buttons to the UMjGeom Details panel.
+ *
+ * All other URLab components used to have parallel customizations that
+ * existed only to ``HideProperty`` on a single internal UPROPERTY
+ * (DefaultClass / ClassName / Name etc). Those were removed in the
+ * MjComponentDetailCustomizations cleanup — the same hiding now lives on
+ * the UPROPERTY decls themselves via
+ * ``meta=(EditCondition="false", EditConditionHides)``. This file
+ * keeps the one customization with real Slate-button logic.
+ */
 class FMjGeomDetailCustomization : public IDetailCustomization
 {
 public:
diff --git a/docs/guides/codegen_contributors.md b/docs/guides/codegen_contributors.md
new file mode 100644
index 0000000..6b81bb7
--- /dev/null
+++ b/docs/guides/codegen_contributors.md
@@ -0,0 +1,108 @@
+# Codegen contributor guide
+
+How to change URLab's MuJoCo wrappers without breaking the codegen
+pipeline. Read this first when you're about to edit anything under
+`Source/URLab/Public/MuJoCo/Components/` or `Source/URLab/Public/MuJoCo/Generated/`.
+
+## The three snapshots
+
+| Snapshot                                | Built by                                         | What it captures                                                  |
+|-----------------------------------------|--------------------------------------------------|-------------------------------------------------------------------|
+| `Scripts/mjcf_schema_snapshot.json`     | `Scripts/codegen/build_mjcf_schema_snapshot.py`  | MJCF schema (`MJCF[]` table) + per-sensor objtype/reftype scrape  |
+| `Scripts/mjspec_snapshot.json`          | `Scripts/codegen/build_mjspec_snapshot.py`       | `mjsX` struct fields, `mjs_setTo*` signatures, `mjt*` enum values |
+| `Scripts/mjxmacro_snapshot.json`        | `Scripts/codegen/build_mjxmacro_snapshot.py`     | `MJOPTION_FIELDS`, `MJSTATISTIC_FIELDS`, `MJVISUAL_*_FIELDS`, mjModel/mjData pointer tables |
+| `Scripts/introspect_snapshot.json`      | `Scripts/codegen/build_introspect_snapshot.py`   | clang-AST scrape of mjspec.h / mjmodel.h / mujoco.h: function sigs, enum decls with doc comments, struct decls, `#define` constants |
+
+Run `python Scripts/codegen/regen_all.py` to refresh all of them after
+a MuJoCo bump.
+
+## The generator
+
+`Scripts/codegen/generate_ue_components.py` reads:
+
+1. The four snapshots above
+2. `Scripts/codegen/codegen_rules.json` (URLab-side decisions: which fields to expose, how to type-map them, which categories use which layout)
+
+…and emits:
+
+- `.h` + `.cpp` for every category subclass (single-uclass / multi-uclass / no-subclasses layouts)
+- Marker-region content for each CODEGEN_*_START/END pair in hand-written files
+- Full-file banner-mode artifacts under `Source/URLab/Public/MuJoCo/Generated/`
+
+CLI flags:
+
+| Flag        | Effect                                                                     |
+|-------------|----------------------------------------------------------------------------|
+| `--check`   | Non-zero exit if regen would change anything. Used by the build script gate. |
+| `--dry-run` | Print diffs, write nothing                                                 |
+| `--strict`  | Exit code 2 if any drift diagnostic fires                                  |
+
+## The build gate
+
+`Scripts/build_and_test.ps1` runs `--check` before invoking UBT. If
+codegen output drifted from the committed source, the build fails with
+exit code 4 before wasting compile time. Re-run the generator without
+`--check`, commit the output, then re-run the script.
+
+Bypass for known-incompatible local edits:
+
+```
+$env:URLAB_SKIP_CODEGEN_DRIFT = '1'  # NOT YET WIRED — manually skip by editing the script
+```
+
+## Editing rules
+
+`codegen_rules.json` is **URLab decisions only** — anything derivable
+from a snapshot should NOT be hand-listed. Common rule sections:
+
+- `type_mappings`: attr name → UE type (`"size": "TArray<float>"`)
+- `element_rules.<elem>`:  per-element exclude/rename/meta config
+- `element_rules.<elem>.xml_enum_attrs`: XML-string → UE-enum mapping
+- `canonicalizations`: how multi-attr clusters collapse into UPROPERTYs
+- `categories.<cat>.subtypes`: list of UE classes per category
+- `synthetic_categories`: whole-file banner-mode targets (mjOption, mjVisual, etc.)
+- `generated_enums`: banner-mode .h files holding multiple UENUMs
+- `intentionally_unmodeled_elements` / `intentionally_unmodeled_mjs_fields`: silence the drift diagnostic for known-handled-elsewhere items
+
+When MuJoCo adds a new field / enum value / sensor type, the drift
+diagnostic should surface it. Run `--strict` to make those fatal in CI.
+
+## Marker pairs in hand-written files
+
+Most `Source/URLab/Private/MuJoCo/Components/*.cpp` files have these
+two pairs:
+
+```
+// --- CODEGEN_IMPORT_START ---
+// (codegen-emitted XML → UPROPERTY assignments)
+// --- CODEGEN_IMPORT_END ---
+
+// --- CODEGEN_EXPORT_START ---
+// (codegen-emitted UPROPERTY → mjs spec field writes)
+// --- CODEGEN_EXPORT_END ---
+```
+
+Inside the markers is codegen-owned. Outside is hand-written. Do not
+move logic across the boundary in a single commit — split into a
+"prepare hand-side" commit and a "regen" commit so the diff is
+reviewable.
+
+Special markers for `MjSensor.cpp`:
+
+```
+// --- CODEGEN_SENSOR_TYPE_SWITCH_START ---  (case bodies in ExportTo)
+// --- CODEGEN_SENSOR_TAG_TO_TYPE_START ---  (TagToType TMap entries)
+```
+
+## Plan v8 references
+
+- `docs/plan_codegen_introspect.md` — full plan (v9 status at top)
+- `docs/plan_codegen_deviations.md` — D1–D10 deviation log
+- `Source/URLab/Public/MuJoCo/Generated/README.md` — per-file index of generated artifacts
+
+## Common mistakes
+
+1. **Edit between CODEGEN markers and forget to update the rule** → next regen reverts your change. Fix: edit the rule + run regen.
+2. **Add a field to `mjsX` upstream after a MuJoCo bump** → drift diagnostic surfaces it. Either add to a `canonicalization`, `xml_enum_attrs`, `attr_to_mjs_field`, etc., or add to `intentionally_unmodeled_mjs_fields` with a one-line reason.
+3. **Hand-write a `bOverride_X` pair for an xml_enum_attr** → conflicts with the codegen-emitted decl when `emit_property_decl: true` is set. Pick one source of truth.
+4. **Forget to run regen before opening a PR** → build gate fails with exit 4. CI will catch it.
