Skip to main content

rusteal_codegen/
type_map.rs

1// UE property type → Rust type / C++ FFI type mapping.
2
3use crate::schema::ParamInfo;
4
5/// Classification of a function parameter.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum ParamDirection {
8    /// Input parameter.
9    In,
10    /// Output parameter (non-const out).
11    Out,
12    /// Input + output (reference param).
13    InOut,
14    /// Return value.
15    Return,
16}
17
18/// Mapped type information for code generation.
19#[derive(Debug, Clone)]
20pub struct MappedType {
21    /// Rust type in function signatures (e.g., "bool", "i32", "UObjectRef<AActor>").
22    pub rust_type: String,
23    /// Rust type for the FFI boundary (e.g., "bool", "i32", "UObjectHandle").
24    pub rust_ffi_type: String,
25    /// C++ type for the wrapper function (e.g., "bool", "int32", "UObject*").
26    pub cpp_type: String,
27    /// PropertyApi method name for getters (e.g., "get_bool", "get_i32").
28    pub property_getter: String,
29    /// PropertyApi method name for setters.
30    pub property_setter: String,
31    /// How to convert from Rust safe type to FFI type in function call.
32    pub rust_to_ffi: ConversionKind,
33    /// How to convert from FFI type to Rust safe type in return.
34    pub ffi_to_rust: ConversionKind,
35    /// Whether this is a supported type for Phase 3.
36    pub supported: bool,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum ConversionKind {
41    /// No conversion needed (primitives).
42    Identity,
43    /// Integer type cast (e.g., u32 ↔ i32).
44    IntCast,
45    /// Wrap in UObjectRef::from_raw / extract with .raw().
46    ObjectRef,
47    /// String: UTF-8 ptr+len on FFI, String on Rust side.
48    StringUtf8,
49    /// Enum: i64 on FFI, enum type on Rust side.
50    EnumCast,
51    /// Struct: opaque pointer on FFI.
52    StructOpaque,
53    /// FName: FNameHandle on FFI.
54    FName,
55    /// TArray container property — returns UeArray<T> handle.
56    ContainerArray,
57    /// TMap container property — returns UeMap<K, V> handle.
58    ContainerMap,
59    /// TSet container property — returns UeSet<T> handle.
60    ContainerSet,
61    /// Unicast delegate property.
62    Delegate,
63    /// Multicast delegate property (inline or sparse).
64    MulticastDelegate,
65}
66
67/// Supported UE property types.
68const SUPPORTED_TYPES: &[&str] = &[
69    "BoolProperty",
70    "Int8Property",
71    "ByteProperty",
72    "Int16Property",
73    "UInt16Property",
74    "IntProperty",
75    "UInt32Property",
76    "Int64Property",
77    "UInt64Property",
78    "FloatProperty",
79    "DoubleProperty",
80    "StrProperty",
81    "NameProperty",
82    "TextProperty",
83    "EnumProperty",
84    "ObjectProperty",
85    "ClassProperty",
86    "StructProperty",
87    "ArrayProperty",
88    "MapProperty",
89    "SetProperty",
90    "SoftObjectProperty",
91    "WeakObjectProperty",
92    "InterfaceProperty",
93    "DelegateProperty",
94    "MulticastInlineDelegateProperty",
95    "MulticastSparseDelegateProperty",
96];
97
98/// Check if a property type is supported in Phase 3.
99pub fn is_supported_type(prop_type: &str) -> bool {
100    SUPPORTED_TYPES.contains(&prop_type)
101}
102
103/// Map a UE property type string to its Rust/C++ type information.
104pub fn map_property_type(
105    prop_type: &str,
106    class_name: Option<&str>,
107    struct_name: Option<&str>,
108    enum_name: Option<&str>,
109    enum_underlying_type: Option<&str>,
110    meta_class_name: Option<&str>,
111    interface_name: Option<&str>,
112) -> MappedType {
113    match prop_type {
114        "BoolProperty" => MappedType {
115            rust_type: "bool".into(),
116            rust_ffi_type: "bool".into(),
117            cpp_type: "bool".into(),
118            property_getter: "get_bool".into(),
119            property_setter: "set_bool".into(),
120            rust_to_ffi: ConversionKind::Identity,
121            ffi_to_rust: ConversionKind::Identity,
122            supported: true,
123        },
124        "Int8Property" => int_type("i8", "int8"),
125        "ByteProperty" => {
126            // ByteProperty can be a plain uint8 or an enum
127            if let Some(en) = enum_name {
128                enum_type(en, enum_underlying_type.unwrap_or("uint8"))
129            } else {
130                int_type("u8", "uint8")
131            }
132        }
133        "Int16Property" => int_type("i16", "int16"),
134        "UInt16Property" => int_type("u16", "uint16"),
135        "IntProperty" => int_type("i32", "int32"),
136        "UInt32Property" => int_type("u32", "uint32"),
137        "Int64Property" => int_type("i64", "int64"),
138        "UInt64Property" => int_type("u64", "uint64"),
139        "FloatProperty" => MappedType {
140            rust_type: "f32".into(),
141            rust_ffi_type: "f32".into(),
142            cpp_type: "float".into(),
143            property_getter: "get_f32".into(),
144            property_setter: "set_f32".into(),
145            rust_to_ffi: ConversionKind::Identity,
146            ffi_to_rust: ConversionKind::Identity,
147            supported: true,
148        },
149        "DoubleProperty" => MappedType {
150            rust_type: "f64".into(),
151            rust_ffi_type: "f64".into(),
152            cpp_type: "double".into(),
153            property_getter: "get_f64".into(),
154            property_setter: "set_f64".into(),
155            rust_to_ffi: ConversionKind::Identity,
156            ffi_to_rust: ConversionKind::Identity,
157            supported: true,
158        },
159        "StrProperty" => MappedType {
160            rust_type: "String".into(),
161            rust_ffi_type: "*const u8".into(),
162            cpp_type: "FString".into(),
163            property_getter: "get_string".into(),
164            property_setter: "set_string".into(),
165            rust_to_ffi: ConversionKind::StringUtf8,
166            ffi_to_rust: ConversionKind::StringUtf8,
167            supported: true,
168        },
169        "TextProperty" => MappedType {
170            rust_type: "String".into(),
171            rust_ffi_type: "*const u8".into(),
172            cpp_type: "FText".into(),
173            property_getter: "get_string".into(),
174            property_setter: "set_string".into(),
175            rust_to_ffi: ConversionKind::StringUtf8,
176            ffi_to_rust: ConversionKind::StringUtf8,
177            supported: true,
178        },
179        "NameProperty" => MappedType {
180            rust_type: "rusteal_core::FNameHandle".into(),
181            rust_ffi_type: "rusteal_core::FNameHandle".into(),
182            cpp_type: "FName".into(),
183            property_getter: "get_fname".into(),
184            property_setter: "set_fname".into(),
185            rust_to_ffi: ConversionKind::FName,
186            ffi_to_rust: ConversionKind::FName,
187            supported: true,
188        },
189        "EnumProperty" => {
190            if let Some(en) = enum_name {
191                enum_type(en, enum_underlying_type.unwrap_or("uint8"))
192            } else {
193                unsupported("EnumProperty without enum_name")
194            }
195        }
196        "ObjectProperty" => {
197            if let Some(cls) = class_name {
198                MappedType {
199                    rust_type: format!("rusteal_core::UObjectRef<{cls}>"),
200                    rust_ffi_type: "rusteal_core::UObjectHandle".into(),
201                    cpp_type: format!("{cls}*"),
202                    property_getter: "get_object".into(),
203                    property_setter: "set_object".into(),
204                    rust_to_ffi: ConversionKind::ObjectRef,
205                    ffi_to_rust: ConversionKind::ObjectRef,
206                    supported: true,
207                }
208            } else {
209                // Untyped object reference — use UObject
210                MappedType {
211                    rust_type: "rusteal_core::UObjectHandle".into(),
212                    rust_ffi_type: "rusteal_core::UObjectHandle".into(),
213                    cpp_type: "UObject*".into(),
214                    property_getter: "get_object".into(),
215                    property_setter: "set_object".into(),
216                    rust_to_ffi: ConversionKind::Identity,
217                    ffi_to_rust: ConversionKind::Identity,
218                    supported: true,
219                }
220            }
221        }
222        "SoftObjectProperty" | "WeakObjectProperty" => {
223            // TSoftObjectPtr<T> / TWeakObjectPtr<T> resolve to UObject* via
224            // FObjectPropertyBase — use the same ObjectRef mapping as ObjectProperty.
225            if let Some(cls) = class_name {
226                MappedType {
227                    rust_type: format!("rusteal_core::UObjectRef<{cls}>"),
228                    rust_ffi_type: "rusteal_core::UObjectHandle".into(),
229                    cpp_type: format!("{cls}*"),
230                    property_getter: "get_object".into(),
231                    property_setter: "set_object".into(),
232                    rust_to_ffi: ConversionKind::ObjectRef,
233                    ffi_to_rust: ConversionKind::ObjectRef,
234                    supported: true,
235                }
236            } else {
237                MappedType {
238                    rust_type: "rusteal_core::UObjectHandle".into(),
239                    rust_ffi_type: "rusteal_core::UObjectHandle".into(),
240                    cpp_type: "UObject*".into(),
241                    property_getter: "get_object".into(),
242                    property_setter: "set_object".into(),
243                    rust_to_ffi: ConversionKind::Identity,
244                    ffi_to_rust: ConversionKind::Identity,
245                    supported: true,
246                }
247            }
248        }
249        "ClassProperty" => {
250            let effective_class = meta_class_name.or(class_name);
251            if let Some(cls) = effective_class {
252                MappedType {
253                    // A class (`TSubclassOf<T>`), not an object of it.
254                    rust_type: format!("rusteal_core::SubclassOf<{cls}>"),
255                    rust_ffi_type: "rusteal_core::UObjectHandle".into(),
256                    cpp_type: format!("{cls}*"),
257                    property_getter: "get_object".into(),
258                    property_setter: "set_object".into(),
259                    rust_to_ffi: ConversionKind::ObjectRef,
260                    ffi_to_rust: ConversionKind::ObjectRef,
261                    supported: true,
262                }
263            } else {
264                MappedType {
265                    rust_type: "rusteal_core::UObjectHandle".into(),
266                    rust_ffi_type: "rusteal_core::UObjectHandle".into(),
267                    cpp_type: "UObject*".into(),
268                    property_getter: "get_object".into(),
269                    property_setter: "set_object".into(),
270                    rust_to_ffi: ConversionKind::Identity,
271                    ffi_to_rust: ConversionKind::Identity,
272                    supported: true,
273                }
274            }
275        }
276        "InterfaceProperty" => {
277            if let Some(iface) = interface_name {
278                MappedType {
279                    rust_type: format!("rusteal_core::UObjectRef<{iface}>"),
280                    rust_ffi_type: "rusteal_core::UObjectHandle".into(),
281                    cpp_type: format!("{iface}*"),
282                    property_getter: "get_object".into(),
283                    property_setter: "set_object".into(),
284                    rust_to_ffi: ConversionKind::ObjectRef,
285                    ffi_to_rust: ConversionKind::ObjectRef,
286                    supported: true,
287                }
288            } else {
289                unsupported("InterfaceProperty without interface_name")
290            }
291        }
292        "StructProperty" => {
293            if let Some(sn) = struct_name {
294                MappedType {
295                    rust_type: format!("*const u8 /* {sn} */"),
296                    rust_ffi_type: "*const u8".into(),
297                    cpp_type: format!("F{sn}"),
298                    property_getter: "get_struct".into(),
299                    property_setter: "set_struct".into(),
300                    rust_to_ffi: ConversionKind::StructOpaque,
301                    ffi_to_rust: ConversionKind::StructOpaque,
302                    supported: true,
303                }
304            } else {
305                unsupported("StructProperty without struct_name")
306            }
307        }
308        "ArrayProperty" => MappedType {
309            rust_type: "rusteal_core::UeArray<_>".into(),
310            rust_ffi_type: String::new(),
311            cpp_type: String::new(),
312            property_getter: String::new(),
313            property_setter: String::new(),
314            rust_to_ffi: ConversionKind::ContainerArray,
315            ffi_to_rust: ConversionKind::ContainerArray,
316            supported: true,
317        },
318        "MapProperty" => MappedType {
319            rust_type: "rusteal_core::UeMap<_, _>".into(),
320            rust_ffi_type: String::new(),
321            cpp_type: String::new(),
322            property_getter: String::new(),
323            property_setter: String::new(),
324            rust_to_ffi: ConversionKind::ContainerMap,
325            ffi_to_rust: ConversionKind::ContainerMap,
326            supported: true,
327        },
328        "SetProperty" => MappedType {
329            rust_type: "rusteal_core::UeSet<_>".into(),
330            rust_ffi_type: String::new(),
331            cpp_type: String::new(),
332            property_getter: String::new(),
333            property_setter: String::new(),
334            rust_to_ffi: ConversionKind::ContainerSet,
335            ffi_to_rust: ConversionKind::ContainerSet,
336            supported: true,
337        },
338        "DelegateProperty" => MappedType {
339            rust_type: "/* delegate */".into(),
340            rust_ffi_type: String::new(),
341            cpp_type: String::new(),
342            property_getter: String::new(),
343            property_setter: String::new(),
344            rust_to_ffi: ConversionKind::Delegate,
345            ffi_to_rust: ConversionKind::Delegate,
346            supported: true,
347        },
348        "MulticastInlineDelegateProperty" | "MulticastSparseDelegateProperty" => MappedType {
349            rust_type: "/* multicast delegate */".into(),
350            rust_ffi_type: String::new(),
351            cpp_type: String::new(),
352            property_getter: String::new(),
353            property_setter: String::new(),
354            rust_to_ffi: ConversionKind::MulticastDelegate,
355            ffi_to_rust: ConversionKind::MulticastDelegate,
356            supported: true,
357        },
358        _ => unsupported(prop_type),
359    }
360}
361
362/// Map a param to its direction based on prop_flags.
363pub fn param_direction(param: &ParamInfo) -> ParamDirection {
364    use crate::schema::*;
365
366    if param.prop_flags & CPF_RETURN_PARM != 0 {
367        return ParamDirection::Return;
368    }
369    let is_out = param.prop_flags & CPF_OUT_PARM != 0;
370    let is_const = param.prop_flags & CPF_CONST_PARM != 0;
371    let is_ref = param.prop_flags & CPF_REFERENCE_PARM != 0;
372
373    if is_out && is_const {
374        // const& pseudo-output → actually input
375        ParamDirection::In
376    } else if is_out && is_ref {
377        ParamDirection::InOut
378    } else if is_out {
379        ParamDirection::Out
380    } else {
381        ParamDirection::In
382    }
383}
384
385fn int_type(rust: &str, _cpp: &str) -> MappedType {
386    // Map to the FFI type that matches the available PropertyApi methods.
387    // Available: get_u8/set_u8, get_i32/set_i32, get_i64/set_i64
388    let (getter, setter, ffi_type) = match rust {
389        "i8" => ("get_u8", "set_u8", "u8"),    // cast u8 ↔ i8
390        "u8" => ("get_u8", "set_u8", "u8"),
391        "i16" => ("get_i32", "set_i32", "i32"), // cast i32 ↔ i16
392        "u16" => ("get_i32", "set_i32", "i32"), // cast i32 ↔ u16
393        "i32" => ("get_i32", "set_i32", "i32"),
394        "u32" => ("get_i32", "set_i32", "i32"), // cast i32 ↔ u32
395        "i64" => ("get_i64", "set_i64", "i64"),
396        "u64" => ("get_i64", "set_i64", "i64"), // cast i64 ↔ u64
397        _ => ("get_i32", "set_i32", "i32"),
398    };
399    let cpp = match rust {
400        "i8" => "int8",
401        "u8" => "uint8",
402        "i16" => "int16",
403        "u16" => "uint16",
404        "i32" => "int32",
405        "u32" => "uint32",
406        "i64" => "int64",
407        "u64" => "uint64",
408        _ => "int32",
409    };
410    let needs_cast = rust != ffi_type;
411    MappedType {
412        rust_type: rust.into(),
413        rust_ffi_type: ffi_type.into(),
414        cpp_type: cpp.into(),
415        property_getter: getter.into(),
416        property_setter: setter.into(),
417        rust_to_ffi: if needs_cast { ConversionKind::IntCast } else { ConversionKind::Identity },
418        ffi_to_rust: if needs_cast { ConversionKind::IntCast } else { ConversionKind::Identity },
419        supported: true,
420    }
421}
422
423fn enum_type(enum_name: &str, underlying: &str) -> MappedType {
424    let repr = match underlying {
425        "uint8" => "u8",
426        "int8" => "i8",
427        "uint16" => "u16",
428        "int16" => "i16",
429        "uint32" => "u32",
430        "int32" => "i32",
431        "uint64" => "u64",
432        "int64" => "i64",
433        _ => "u8",
434    };
435    MappedType {
436        rust_type: enum_name.to_string(),
437        rust_ffi_type: repr.into(),
438        cpp_type: enum_name.to_string(),
439        property_getter: "get_enum".into(),
440        property_setter: "set_enum".into(),
441        rust_to_ffi: ConversionKind::EnumCast,
442        ffi_to_rust: ConversionKind::EnumCast,
443        supported: true,
444    }
445}
446
447fn unsupported(reason: &str) -> MappedType {
448    MappedType {
449        rust_type: format!("/* unsupported: {reason} */"),
450        rust_ffi_type: String::new(),
451        cpp_type: String::new(),
452        property_getter: String::new(),
453        property_setter: String::new(),
454        rust_to_ffi: ConversionKind::Identity,
455        ffi_to_rust: ConversionKind::Identity,
456        supported: false,
457    }
458}
459
460// ---------------------------------------------------------------------------
461// Container inner-type resolution
462// ---------------------------------------------------------------------------
463
464use crate::context::CodegenContext;
465use crate::schema::PropertyInfo;
466
467/// Map an inner property (inside a container) to its Rust `ContainerElement` type.
468/// When `ctx` is provided, validates that referenced types are in enabled modules
469/// and returns the actual typed names (e.g., `UObjectRef<Actor>` instead of `UObjectHandle`).
470/// Returns `None` if the inner type is unsupported for container elements.
471pub fn container_element_rust_type(
472    inner: &PropertyInfo,
473    ctx: Option<&CodegenContext>,
474) -> Option<String> {
475    match inner.prop_type.as_str() {
476        "BoolProperty" => Some("bool".into()),
477        "Int8Property" => Some("i8".into()),
478        "ByteProperty" => {
479            if let Some(en) = &inner.enum_name {
480                if let Some(ctx) = ctx
481                    && !ctx.enums.contains_key(en.as_str())
482                {
483                    return None;
484                }
485                Some(en.clone())
486            } else {
487                Some("u8".into())
488            }
489        }
490        "Int16Property" => Some("i16".into()),
491        "UInt16Property" => Some("u16".into()),
492        "IntProperty" => Some("i32".into()),
493        "UInt32Property" => Some("u32".into()),
494        "Int64Property" => Some("i64".into()),
495        "UInt64Property" => Some("u64".into()),
496        "FloatProperty" => Some("f32".into()),
497        "DoubleProperty" => Some("f64".into()),
498        "StrProperty" | "TextProperty" => Some("String".into()),
499        "NameProperty" => Some("rusteal_core::FNameHandle".into()),
500        "ObjectProperty" | "SoftObjectProperty" | "WeakObjectProperty" => {
501            if let Some(cls) = &inner.class_name {
502                if let Some(ctx) = ctx
503                    && !ctx.classes.contains_key(cls.as_str())
504                {
505                    return None;
506                }
507                Some(format!("rusteal_core::UObjectRef<{cls}>"))
508            } else {
509                Some("rusteal_core::UObjectHandle".into())
510            }
511        }
512        "ClassProperty" => {
513            let effective_class = inner.meta_class_name.as_deref().or(inner.class_name.as_deref());
514            if let Some(cls) = effective_class {
515                if let Some(ctx) = ctx
516                    && !ctx.classes.contains_key(cls)
517                {
518                    return None;
519                }
520                Some(format!("rusteal_core::SubclassOf<{cls}>"))
521            } else {
522                Some("rusteal_core::UObjectHandle".into())
523            }
524        }
525        "InterfaceProperty" => {
526            if let Some(ref iface) = inner.interface_name {
527                if let Some(ctx) = ctx
528                    && !ctx.classes.contains_key(iface.as_str())
529                {
530                    return None;
531                }
532                Some(format!("rusteal_core::UObjectRef<{iface}>"))
533            } else {
534                None
535            }
536        }
537        "EnumProperty" => {
538            if let Some(en) = &inner.enum_name {
539                if let Some(ctx) = ctx
540                    && !ctx.enums.contains_key(en.as_str())
541                {
542                    return None;
543                }
544                Some(en.clone())
545            } else {
546                None
547            }
548        }
549        "StructProperty" => {
550            if let Some(sn) = &inner.struct_name {
551                if let Some(ctx) = ctx {
552                    if let Some(si) = ctx.structs.get(sn.as_str()) {
553                        if si.has_static_struct {
554                            Some(format!("rusteal_core::OwnedStruct<{}>", si.cpp_name))
555                        } else {
556                            None // No static_struct → no UeStruct impl
557                        }
558                    } else {
559                        None // Not in enabled modules
560                    }
561                } else {
562                    Some(format!("rusteal_core::OwnedStruct<F{sn}>"))
563                }
564            } else {
565                None
566            }
567        }
568        _ => None, // Nested containers etc. — unsupported
569    }
570}
571
572/// Resolve the full Rust type for a container property.
573/// Returns `None` if any inner type is unsupported.
574pub fn resolve_container_rust_type(
575    prop: &PropertyInfo,
576    ctx: Option<&CodegenContext>,
577) -> Option<String> {
578    match prop.prop_type.as_str() {
579        "ArrayProperty" => {
580            let inner = prop.inner_prop.as_ref()?;
581            let elem_type = container_element_rust_type(inner, ctx)?;
582            Some(format!("rusteal_core::UeArray<{elem_type}>"))
583        }
584        "MapProperty" => {
585            let key = prop.key_prop.as_ref()?;
586            let val = prop.value_prop.as_ref()?;
587            let key_type = container_element_rust_type(key, ctx)?;
588            let val_type = container_element_rust_type(val, ctx)?;
589            Some(format!("rusteal_core::UeMap<{key_type}, {val_type}>"))
590        }
591        "SetProperty" => {
592            let elem = prop.element_prop.as_ref()?;
593            let elem_type = container_element_rust_type(elem, ctx)?;
594            Some(format!("rusteal_core::UeSet<{elem_type}>"))
595        }
596        _ => None,
597    }
598}
599
600// ---------------------------------------------------------------------------
601// Container parameter helpers (for function codegen)
602// ---------------------------------------------------------------------------
603
604/// Check if a function parameter is a container type (Array, Map, Set).
605pub fn is_container_param(param: &ParamInfo) -> bool {
606    matches!(
607        param.prop_type.as_str(),
608        "ArrayProperty" | "MapProperty" | "SetProperty"
609    )
610}
611
612/// Resolve the Rust input type for a container parameter (e.g., `&[Actor]`).
613pub fn container_param_input_type(param: &ParamInfo, ctx: &CodegenContext) -> Option<String> {
614    match param.prop_type.as_str() {
615        "ArrayProperty" => {
616            let inner = param.inner_prop.as_ref()?;
617            let elem = container_element_rust_type(inner, Some(ctx))?;
618            Some(format!("&[{elem}]"))
619        }
620        "SetProperty" => {
621            let elem = param.element_prop.as_ref()?;
622            let etype = container_element_rust_type(elem, Some(ctx))?;
623            Some(format!("&[{etype}]"))
624        }
625        "MapProperty" => {
626            let key = param.key_prop.as_ref()?;
627            let val = param.value_prop.as_ref()?;
628            let kt = container_element_rust_type(key, Some(ctx))?;
629            let vt = container_element_rust_type(val, Some(ctx))?;
630            Some(format!("&[({kt}, {vt})]"))
631        }
632        _ => None,
633    }
634}
635
636/// Resolve the Rust output type for a container parameter (e.g., `Vec<Actor>`).
637pub fn container_param_output_type(param: &ParamInfo, ctx: &CodegenContext) -> Option<String> {
638    match param.prop_type.as_str() {
639        "ArrayProperty" => {
640            let inner = param.inner_prop.as_ref()?;
641            let elem = container_element_rust_type(inner, Some(ctx))?;
642            Some(format!("Vec<{elem}>"))
643        }
644        "SetProperty" => {
645            let elem = param.element_prop.as_ref()?;
646            let etype = container_element_rust_type(elem, Some(ctx))?;
647            Some(format!("Vec<{etype}>"))
648        }
649        "MapProperty" => {
650            let key = param.key_prop.as_ref()?;
651            let val = param.value_prop.as_ref()?;
652            let kt = container_element_rust_type(key, Some(ctx))?;
653            let vt = container_element_rust_type(val, Some(ctx))?;
654            Some(format!("Vec<({kt}, {vt})>"))
655        }
656        _ => None,
657    }
658}
659
660/// Resolve the element type string for use in container type construction
661/// (e.g., `UObjectRef<Actor>` for UeArray, or `K, V` for UeMap).
662pub fn container_elem_type_str(param: &ParamInfo, ctx: &CodegenContext) -> Option<String> {
663    match param.prop_type.as_str() {
664        "ArrayProperty" => {
665            let inner = param.inner_prop.as_ref()?;
666            container_element_rust_type(inner, Some(ctx))
667        }
668        "SetProperty" => {
669            let elem = param.element_prop.as_ref()?;
670            container_element_rust_type(elem, Some(ctx))
671        }
672        "MapProperty" => {
673            let key = param.key_prop.as_ref()?;
674            let val = param.value_prop.as_ref()?;
675            let kt = container_element_rust_type(key, Some(ctx))?;
676            let vt = container_element_rust_type(val, Some(ctx))?;
677            Some(format!("{kt}, {vt}"))
678        }
679        _ => None,
680    }
681}