(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,
vec3_convert: Optional[str] = None,
array_cap: Optional[int] = None)
| 375 | |
| 376 | |
| 377 | def _emit_export_line(attr: str, prop_name: str, ue_type: str, |
| 378 | mjs_fields: set | None = None, |
| 379 | slot_sentinel: str | None = None, |
| 380 | attr_to_mjs_field: Dict[str, str] | None = None, |
| 381 | consumed_by_setto: set | None = None, |
| 382 | vec3_convert: Optional[str] = None, |
| 383 | array_cap: Optional[int] = None) -> str: |
| 384 | toggle = override_toggle_name(prop_name) |
| 385 | # If we have an mjs field set and the attr can't be resolved to one of |
| 386 | # them, this attr isn't directly assignable on the mjs struct (e.g. |
| 387 | # actuator subtype params like `kp` live in gainprm[], not Element->kp). |
| 388 | # Emit either: |
| 389 | # - nothing (silent) when the attr is consumed by an mjs_setTo* call |
| 390 | # above us in the same CODEGEN_EXPORT block (no need for noise) |
| 391 | # - a one-line "(skipped)" marker otherwise (genuinely unhandled, |
| 392 | # reviewers should know) |
| 393 | if mjs_fields: |
| 394 | field = _resolve_mjs_field(attr, mjs_fields, attr_to_mjs_field) |
| 395 | if field == attr and attr not in mjs_fields and not ( |
| 396 | attr_to_mjs_field and attr in attr_to_mjs_field |
| 397 | ): |
| 398 | if consumed_by_setto and attr in consumed_by_setto: |
| 399 | return "" |
| 400 | return f' // (skipped: mjs has no field for "{attr}"; hand-roll via mjs_setTo<Type>)\n' |
| 401 | else: |
| 402 | field = attr_to_mjs_field.get(attr, attr) if attr_to_mjs_field else attr |
| 403 | if ue_type.startswith("TArray<float") or ue_type.startswith("TArray<int") or ue_type.startswith("TArray<double"): |
| 404 | # When the destination C field is a fixed-size array, cap the |
| 405 | # loop count against the C array dim so a future MuJoCo shrink |
| 406 | # (gainprm[10] -> gainprm[6]) doesn't memory-overrun. Dynamic |
| 407 | # vec destinations (mjFloatVec*, mjDoubleVec*) pass array_cap=None |
| 408 | # and use the raw prop.Num() — the dynamic vec resizes itself. |
| 409 | upper = f"FMath::Min({prop_name}.Num(), {array_cap})" if array_cap else f"{prop_name}.Num()" |
| 410 | if slot_sentinel is not None: |
| 411 | # Per-slot sentinel guard: skip slots whose value equals the |
| 412 | # sentinel (used for `size` so fromto-only imports don't |
| 413 | # clobber the inherited default radius / earlier slots). |
| 414 | return _guarded_export(toggle, |
| 415 | f'{{ for (int32 i = 0; i < {upper}; ++i)' |
| 416 | f' {{ if ({prop_name}[i] != {slot_sentinel}) Element->{field}[i] = {prop_name}[i]; }} }}', |
| 417 | ) |
| 418 | return _guarded_export(toggle, |
| 419 | f'{{ for (int32 i = 0; i < {upper}; ++i)' |
| 420 | f' Element->{field}[i] = {prop_name}[i]; }}', |
| 421 | ) |
| 422 | if ue_type == "FVector": |
| 423 | if vec3_convert == "y_negate": |
| 424 | return _guarded_export(toggle, |
| 425 | f'{{ Element->{field}[0] = {prop_name}.X;' |
| 426 | f' Element->{field}[1] = -{prop_name}.Y;' |
| 427 | f' Element->{field}[2] = {prop_name}.Z; }}', |
| 428 | ) |
| 429 | return _guarded_export(toggle, |
| 430 | f'{{ Element->{field}[0] = {prop_name}.X;' |
| 431 | f' Element->{field}[1] = {prop_name}.Y;' |
| 432 | f' Element->{field}[2] = {prop_name}.Z; }}', |
| 433 | ) |
| 434 | if ue_type == "FLinearColor": |
no test coverage detected