Skip to main content

rusteal_codegen/rust_gen/
properties.rs

1// Shared property codegen for both classes and structs.
2//
3// Property generation (getters/setters via the reflection PropertyApi) is
4// identical for UClass and UScriptStruct except for:
5// - The reflection lookup function (find_property vs find_struct_property)
6// - The handle expression (static_class() vs static_struct())
7// - The handle access (classes do self.handle(), structs don't)
8// - The container expression passed to the PropertyApi (h vs self.as_ptr())
9//
10// `PropertyContext` captures these differences so all the codegen helpers
11// can be shared.
12
13use crate::context::CodegenContext;
14use crate::naming::{strip_bool_prefix, to_snake_case};
15use crate::schema::PropertyInfo;
16use crate::type_map::{self, ConversionKind, MappedType};
17
18/// Context that parameterizes property codegen for classes vs structs.
19pub struct PropertyContext {
20    /// Reflection API function name: "find_property" or "find_struct_property".
21    pub find_prop_fn: String,
22    /// Expression to get the type handle for property lookup.
23    /// e.g., "Actor::static_class()" or "FVector::static_struct()".
24    pub handle_expr: String,
25    /// Validity check statement (including semicolon). Empty string for structs.
26    /// e.g., "let h = self.handle();"
27    pub pre_access: String,
28    /// Container expression for property API calls: "h" or "self.as_ptr()".
29    pub container_expr: String,
30    /// Whether this is a UClass context (true) or struct context (false).
31    /// Container properties are only valid in class contexts.
32    pub is_class: bool,
33}
34
35/// Collect supported, deduplicated properties, returning their getter name set and the property list.
36/// Properties referencing types not in the context (e.g., enums/classes from non-enabled modules)
37/// are filtered out.
38pub fn collect_deduped_properties<'a>(
39    props: &'a [PropertyInfo],
40    ctx: Option<&CodegenContext>,
41) -> (std::collections::HashSet<String>, Vec<&'a PropertyInfo>) {
42    let mut prop_names = std::collections::HashSet::new();
43    let mut deduped = Vec::new();
44
45    for prop in props {
46        // Skip BlueprintGetter/Setter properties (accessed via function calls)
47        if prop.getter.is_some() || prop.setter.is_some() {
48            continue;
49        }
50
51        let mapped = type_map::map_property_type(
52            &prop.prop_type,
53            prop.class_name.as_deref(),
54            prop.struct_name.as_deref(),
55            prop.enum_name.as_deref(),
56            prop.enum_underlying_type.as_deref(),
57            prop.meta_class_name.as_deref(),
58            prop.interface_name.as_deref(),
59        );
60        if !mapped.supported {
61            continue;
62        }
63
64        // ObjectRef: check that the referenced class/interface is available
65        if matches!(mapped.rust_to_ffi, ConversionKind::ObjectRef)
66            && let Some(ctx) = ctx
67        {
68            let type_available = match prop.prop_type.as_str() {
69                "ClassProperty" => {
70                    let effective = prop.meta_class_name.as_deref().or(prop.class_name.as_deref());
71                    effective.is_none_or(|c| ctx.classes.contains_key(c))
72                }
73                "InterfaceProperty" => {
74                    prop.interface_name.as_deref().is_some_and(|i| ctx.classes.contains_key(i))
75                }
76                _ => prop.class_name.as_deref().is_none_or(|c| ctx.classes.contains_key(c)),
77            };
78            if !type_available {
79                continue;
80            }
81        }
82
83        // StructOpaque: only allow if the struct is in enabled modules with static_struct
84        if matches!(mapped.rust_to_ffi, ConversionKind::StructOpaque) {
85            let valid = if let Some(ctx) = ctx {
86                prop.struct_name
87                    .as_deref()
88                    .is_some_and(|sn| ctx.structs.get(sn).is_some_and(|si| si.has_static_struct))
89            } else {
90                prop.struct_name.is_some()
91            };
92            if !valid {
93                continue;
94            }
95        }
96
97        // Container types: skip if inner types can't be resolved
98        if matches!(
99            mapped.rust_to_ffi,
100            ConversionKind::ContainerArray | ConversionKind::ContainerMap | ConversionKind::ContainerSet
101        )
102            && type_map::resolve_container_rust_type(prop, ctx).is_none()
103        {
104            continue;
105        }
106            // Container properties only work on UClass contexts (need UObject owner),
107            // not on struct contexts. `ctx` is None only for non-class callers.
108            // More precisely: skip containers in struct property contexts.
109
110        // Skip properties that reference types not in enabled modules
111        if let Some(ctx) = ctx {
112            match mapped.rust_to_ffi {
113                ConversionKind::EnumCast => {
114                    if let Some(en) = &prop.enum_name
115                        && !ctx.enums.contains_key(en.as_str())
116                    {
117                        continue;
118                    }
119                }
120                ConversionKind::ObjectRef => {
121                    if let Some(cn) = &prop.class_name
122                        && !ctx.classes.contains_key(cn.as_str())
123                    {
124                        continue;
125                    }
126                }
127                _ => {}
128            }
129        }
130
131        let rust_name = if prop.prop_type == "BoolProperty" {
132            strip_bool_prefix(&prop.name)
133        } else {
134            to_snake_case(&prop.name)
135        };
136
137        // Container and delegate properties use bare name (no get_/set_ prefix)
138        let is_container = matches!(
139            mapped.rust_to_ffi,
140            ConversionKind::ContainerArray | ConversionKind::ContainerMap | ConversionKind::ContainerSet
141        );
142        let is_delegate = matches!(
143            mapped.rust_to_ffi,
144            ConversionKind::Delegate | ConversionKind::MulticastDelegate
145        );
146        let getter_name = if is_container || is_delegate {
147            rust_name.clone()
148        } else {
149            format!("get_{rust_name}")
150        };
151        if prop_names.contains(&getter_name) {
152            continue;
153        }
154        prop_names.insert(getter_name);
155        if !is_container && !is_delegate {
156            prop_names.insert(format!("set_{rust_name}"));
157        }
158        deduped.push(prop);
159    }
160
161    (prop_names, deduped)
162}
163
164/// Generate a getter and setter for a single property (used as default impls in Ext trait).
165///
166/// Setters whose name appears in `suppress_setters` are skipped.
167pub fn generate_property(
168    out: &mut String,
169    prop: &PropertyInfo,
170    pctx: &PropertyContext,
171    ctx: &CodegenContext,
172    suppress_setters: &std::collections::HashSet<String>,
173) {
174    let mapped = type_map::map_property_type(
175        &prop.prop_type,
176        prop.class_name.as_deref(),
177        prop.struct_name.as_deref(),
178        prop.enum_name.as_deref(),
179        prop.enum_underlying_type.as_deref(),
180        prop.meta_class_name.as_deref(),
181        prop.interface_name.as_deref(),
182    );
183    if !mapped.supported {
184        return;
185    }
186
187    // Delegate properties: handled by delegates.rs, skip here.
188    if matches!(
189        mapped.rust_to_ffi,
190        ConversionKind::Delegate | ConversionKind::MulticastDelegate
191    ) {
192        return;
193    }
194
195    let prop_name = &prop.name;
196    let rust_name = if prop.prop_type == "BoolProperty" {
197        strip_bool_prefix(prop_name)
198    } else {
199        to_snake_case(prop_name)
200    };
201    let prop_name_len = prop_name.len();
202    let byte_lit = format!("b\"{}\\0\"", prop_name);
203
204    // Fixed array properties: use indexed access via get_property_at/set_property_at
205    if prop.array_dim > 1 {
206        generate_fixed_array_property(out, prop, &rust_name, &byte_lit, prop_name_len, pctx, ctx, &mapped);
207        return;
208    }
209
210    // Container types: generate handle-returning getter (no setter)
211    // Only valid in class contexts (containers need a UObject owner)
212    if matches!(
213        mapped.rust_to_ffi,
214        ConversionKind::ContainerArray | ConversionKind::ContainerMap | ConversionKind::ContainerSet
215    ) {
216        if pctx.is_class
217            && let Some(container_type) = type_map::resolve_container_rust_type(prop, Some(ctx))
218        {
219            generate_container_getter(out, &rust_name, &byte_lit, prop_name_len, pctx, &container_type);
220        }
221        return;
222    }
223
224    // Getter
225    match mapped.rust_to_ffi {
226        ConversionKind::StringUtf8 => {
227            generate_string_getter(out, &rust_name, &byte_lit, prop_name_len, pctx);
228        }
229        ConversionKind::StructOpaque => {
230            let struct_cpp = prop.struct_name.as_deref()
231                .and_then(|sn| ctx.structs.get(sn))
232                .map(|si| si.cpp_name.clone())
233                .unwrap_or_else(|| format!("F{}", prop.struct_name.as_deref().unwrap_or("Unknown")));
234            generate_struct_getter(out, &rust_name, &byte_lit, prop_name_len, pctx, &struct_cpp);
235            let setter_name = format!("set_{rust_name}");
236            if !suppress_setters.contains(&setter_name) {
237                generate_struct_setter(out, &rust_name, &byte_lit, prop_name_len, pctx, &struct_cpp);
238            }
239            return;
240        }
241        ConversionKind::ObjectRef => {
242            generate_object_getter(out, &rust_name, &byte_lit, prop_name_len, pctx, &mapped);
243        }
244        ConversionKind::EnumCast => {
245            let actual_repr = prop
246                .enum_name
247                .as_deref()
248                .and_then(|en| ctx.enum_actual_repr(en))
249                .unwrap_or(&mapped.rust_ffi_type);
250            generate_enum_getter(out, &rust_name, &byte_lit, prop_name_len, pctx, &mapped, actual_repr);
251        }
252        ConversionKind::FName => {
253            generate_fname_getter(out, &rust_name, &byte_lit, prop_name_len, pctx);
254        }
255        ConversionKind::IntCast => {
256            generate_int_cast_getter(out, &rust_name, &byte_lit, prop_name_len, pctx, &mapped);
257        }
258        _ => {
259            generate_primitive_getter(out, &rust_name, &byte_lit, prop_name_len, pctx, &mapped);
260        }
261    }
262
263    // Setter (suppressed when a UFUNCTION with the same name takes priority)
264    let setter_name = format!("set_{rust_name}");
265    if suppress_setters.contains(&setter_name) {
266        return;
267    }
268    match mapped.rust_to_ffi {
269        ConversionKind::StringUtf8 => {
270            generate_string_setter(out, &rust_name, &byte_lit, prop_name_len, pctx);
271        }
272        ConversionKind::StructOpaque => { /* handled in getter branch with early return */ }
273        ConversionKind::ObjectRef => {
274            generate_object_setter(out, &rust_name, &byte_lit, prop_name_len, pctx, &mapped);
275        }
276        ConversionKind::EnumCast => {
277            generate_enum_setter(out, &rust_name, &byte_lit, prop_name_len, pctx, &mapped);
278        }
279        ConversionKind::FName => {
280            generate_fname_setter(out, &rust_name, &byte_lit, prop_name_len, pctx);
281        }
282        ConversionKind::IntCast => {
283            generate_int_cast_setter(out, &rust_name, &byte_lit, prop_name_len, pctx, &mapped);
284        }
285        _ => {
286            generate_primitive_setter(out, &rust_name, &byte_lit, prop_name_len, pctx, &mapped);
287        }
288    }
289}
290
291/// Get a default value literal for a Rust type.
292pub fn default_value_for(rust_type: &str) -> &'static str {
293    match rust_type {
294        "bool" => "false",
295        "i8" | "u8" | "i16" | "u16" | "i32" | "u32" | "i64" | "u64" => "0",
296        "f32" => "0.0f32",
297        "f64" => "0.0f64",
298        _ => "Default::default()",
299    }
300}
301
302// ---------------------------------------------------------------------------
303// Prop lookup boilerplate
304// ---------------------------------------------------------------------------
305
306/// Generates the property OnceLock lookup. Returns after the closing `});`.
307fn emit_prop_lookup(out: &mut String, byte_lit: &str, prop_name_len: usize, pctx: &PropertyContext) {
308    let find = &pctx.find_prop_fn;
309    let handle = &pctx.handle_expr;
310    out.push_str(&format!(
311        "        static PROP: std::sync::OnceLock<rusteal_core::FPropertyHandle> = std::sync::OnceLock::new();\n\
312         \x20       let prop = *PROP.get_or_init(|| unsafe {{\n\
313         \x20           rusteal_core::ffi_dispatch::reflection_{find}(\n\
314         \x20               {handle}, {byte_lit}.as_ptr(), {prop_name_len}\n\
315         \x20           )\n\
316         \x20       }});\n"
317    ));
318}
319
320/// Emit pre-access (validity check) if needed.
321fn emit_pre_access(out: &mut String, pctx: &PropertyContext) {
322    if !pctx.pre_access.is_empty() {
323        out.push_str(&format!("        {}\n", pctx.pre_access));
324    }
325}
326
327// ---------------------------------------------------------------------------
328// Getters
329// ---------------------------------------------------------------------------
330
331fn generate_primitive_getter(
332    out: &mut String,
333    rust_name: &str,
334    byte_lit: &str,
335    prop_name_len: usize,
336    pctx: &PropertyContext,
337    mapped: &MappedType,
338) {
339    let rust_type = &mapped.rust_type;
340    let getter = &mapped.property_getter;
341    let default = default_value_for(rust_type);
342    let c = &pctx.container_expr;
343
344    out.push_str(&format!(
345        "    fn get_{rust_name}(&self) -> {rust_type} {{\n"
346    ));
347    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
348    emit_pre_access(out, pctx);
349    out.push_str(&format!(
350        "        let mut out = {default};\n\
351         \x20       rusteal_core::ffi_infallible_ctx(unsafe {{ rusteal_core::ffi_dispatch::property_{getter}({c}, prop, &mut out) }}, \"{rust_name}\");\n\
352         \x20       out\n\
353         \x20   }}\n\n"
354    ));
355}
356
357fn generate_int_cast_getter(
358    out: &mut String,
359    rust_name: &str,
360    byte_lit: &str,
361    prop_name_len: usize,
362    pctx: &PropertyContext,
363    mapped: &MappedType,
364) {
365    let rust_type = &mapped.rust_type;
366    let ffi_type = &mapped.rust_ffi_type;
367    let getter = &mapped.property_getter;
368    let default = default_value_for(ffi_type);
369    let c = &pctx.container_expr;
370
371    out.push_str(&format!(
372        "    fn get_{rust_name}(&self) -> {rust_type} {{\n"
373    ));
374    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
375    emit_pre_access(out, pctx);
376    out.push_str(&format!(
377        "        let mut out = {default};\n\
378         \x20       rusteal_core::ffi_infallible_ctx(unsafe {{ rusteal_core::ffi_dispatch::property_{getter}({c}, prop, &mut out) }}, \"{rust_name}\");\n\
379         \x20       out as {rust_type}\n\
380         \x20   }}\n\n"
381    ));
382}
383
384fn generate_string_getter(
385    out: &mut String,
386    rust_name: &str,
387    byte_lit: &str,
388    prop_name_len: usize,
389    pctx: &PropertyContext,
390) {
391    let c = &pctx.container_expr;
392
393    out.push_str(&format!(
394        "    fn get_{rust_name}(&self) -> String {{\n"
395    ));
396    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
397    emit_pre_access(out, pctx);
398    out.push_str(&format!(
399        "        let mut buf = vec![0u8; 512];\n\
400         \x20       let mut out_len: u32 = 0;\n\
401         \x20       rusteal_core::ffi_infallible_ctx(unsafe {{\n\
402         \x20           rusteal_core::ffi_dispatch::property_get_string({c}, prop, buf.as_mut_ptr(), buf.len() as u32, &mut out_len)\n\
403         \x20       }}, \"{rust_name}\");\n\
404         \x20       buf.truncate(out_len as usize);\n\
405         \x20       String::from_utf8_lossy(&buf).into_owned()\n\
406         \x20   }}\n\n"
407    ));
408}
409
410fn generate_object_getter(
411    out: &mut String,
412    rust_name: &str,
413    byte_lit: &str,
414    prop_name_len: usize,
415    pctx: &PropertyContext,
416    mapped: &MappedType,
417) {
418    let rust_type = &mapped.rust_type;
419    let c = &pctx.container_expr;
420
421    out.push_str(&format!(
422        "    fn get_{rust_name}(&self) -> {rust_type} {{\n"
423    ));
424    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
425    emit_pre_access(out, pctx);
426    out.push_str(&format!(
427        "        let mut raw = rusteal_core::UObjectHandle::null();\n\
428         \x20       rusteal_core::ffi_infallible_ctx(unsafe {{ rusteal_core::ffi_dispatch::property_get_object({c}, prop, &mut raw) }}, \"{rust_name}\");\n\
429         \x20       unsafe {{ rusteal_core::UObjectRef::from_raw(raw) }}\n\
430         \x20   }}\n\n"
431    ));
432}
433
434fn generate_enum_getter(
435    out: &mut String,
436    rust_name: &str,
437    byte_lit: &str,
438    prop_name_len: usize,
439    pctx: &PropertyContext,
440    mapped: &MappedType,
441    actual_repr: &str,
442) {
443    let rust_type = &mapped.rust_type;
444    let c = &pctx.container_expr;
445
446    out.push_str(&format!(
447        "    fn get_{rust_name}(&self) -> {rust_type} {{\n"
448    ));
449    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
450    emit_pre_access(out, pctx);
451    out.push_str(&format!(
452        "        let mut raw: i64 = 0;\n\
453         \x20       rusteal_core::ffi_infallible_ctx(unsafe {{ rusteal_core::ffi_dispatch::property_get_enum({c}, prop, &mut raw) }}, \"{rust_name}\");\n\
454         \x20       {rust_type}::from_value(raw as {actual_repr}).expect(\"unknown enum value\")\n\
455         \x20   }}\n\n"
456    ));
457}
458
459fn generate_fname_getter(
460    out: &mut String,
461    rust_name: &str,
462    byte_lit: &str,
463    prop_name_len: usize,
464    pctx: &PropertyContext,
465) {
466    let c = &pctx.container_expr;
467
468    out.push_str(&format!(
469        "    fn get_{rust_name}(&self) -> rusteal_core::FNameHandle {{\n"
470    ));
471    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
472    emit_pre_access(out, pctx);
473    out.push_str(&format!(
474        "        let mut out = rusteal_core::FNameHandle(0);\n\
475         \x20       rusteal_core::ffi_infallible_ctx(unsafe {{ rusteal_core::ffi_dispatch::property_get_fname({c}, prop, &mut out) }}, \"{rust_name}\");\n\
476         \x20       out\n\
477         \x20   }}\n\n"
478    ));
479}
480
481// ---------------------------------------------------------------------------
482// Setters
483// ---------------------------------------------------------------------------
484
485fn generate_primitive_setter(
486    out: &mut String,
487    rust_name: &str,
488    byte_lit: &str,
489    prop_name_len: usize,
490    pctx: &PropertyContext,
491    mapped: &MappedType,
492) {
493    let rust_type = &mapped.rust_type;
494    let setter = &mapped.property_setter;
495    let c = &pctx.container_expr;
496
497    out.push_str(&format!(
498        "    fn set_{rust_name}(&self, val: {rust_type}) {{\n"
499    ));
500    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
501    emit_pre_access(out, pctx);
502    out.push_str(&format!(
503        "        rusteal_core::ffi_infallible_ctx(unsafe {{ rusteal_core::ffi_dispatch::property_{setter}({c}, prop, val) }}, \"{rust_name}\");\n\
504         \x20   }}\n\n"
505    ));
506}
507
508fn generate_int_cast_setter(
509    out: &mut String,
510    rust_name: &str,
511    byte_lit: &str,
512    prop_name_len: usize,
513    pctx: &PropertyContext,
514    mapped: &MappedType,
515) {
516    let rust_type = &mapped.rust_type;
517    let ffi_type = &mapped.rust_ffi_type;
518    let setter = &mapped.property_setter;
519    let c = &pctx.container_expr;
520
521    out.push_str(&format!(
522        "    fn set_{rust_name}(&self, val: {rust_type}) {{\n"
523    ));
524    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
525    emit_pre_access(out, pctx);
526    out.push_str(&format!(
527        "        rusteal_core::ffi_infallible_ctx(unsafe {{ rusteal_core::ffi_dispatch::property_{setter}({c}, prop, val as {ffi_type}) }}, \"{rust_name}\");\n\
528         \x20   }}\n\n"
529    ));
530}
531
532fn generate_string_setter(
533    out: &mut String,
534    rust_name: &str,
535    byte_lit: &str,
536    prop_name_len: usize,
537    pctx: &PropertyContext,
538) {
539    let c = &pctx.container_expr;
540
541    out.push_str(&format!(
542        "    fn set_{rust_name}(&self, val: &str) {{\n"
543    ));
544    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
545    emit_pre_access(out, pctx);
546    out.push_str(&format!(
547        "        rusteal_core::ffi_infallible_ctx(unsafe {{\n\
548         \x20           rusteal_core::ffi_dispatch::property_set_string({c}, prop, val.as_ptr(), val.len() as u32)\n\
549         \x20       }}, \"{rust_name}\");\n\
550         \x20   }}\n\n"
551    ));
552}
553
554fn generate_object_setter(
555    out: &mut String,
556    rust_name: &str,
557    byte_lit: &str,
558    prop_name_len: usize,
559    pctx: &PropertyContext,
560    mapped: &MappedType,
561) {
562    let rust_type = &mapped.rust_type;
563    let c = &pctx.container_expr;
564
565    out.push_str(&format!(
566        "    fn set_{rust_name}(&self, val: {rust_type}) {{\n"
567    ));
568    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
569    emit_pre_access(out, pctx);
570    out.push_str(&format!(
571        "        rusteal_core::ffi_infallible_ctx(unsafe {{ rusteal_core::ffi_dispatch::property_set_object({c}, prop, val.raw()) }}, \"{rust_name}\");\n\
572         \x20   }}\n\n"
573    ));
574}
575
576fn generate_enum_setter(
577    out: &mut String,
578    rust_name: &str,
579    byte_lit: &str,
580    prop_name_len: usize,
581    pctx: &PropertyContext,
582    mapped: &MappedType,
583) {
584    let rust_type = &mapped.rust_type;
585    let c = &pctx.container_expr;
586
587    out.push_str(&format!(
588        "    fn set_{rust_name}(&self, val: {rust_type}) {{\n"
589    ));
590    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
591    emit_pre_access(out, pctx);
592    out.push_str(&format!(
593        "        rusteal_core::ffi_infallible_ctx(unsafe {{ rusteal_core::ffi_dispatch::property_set_enum({c}, prop, val as i64) }}, \"{rust_name}\");\n\
594         \x20   }}\n\n"
595    ));
596}
597
598fn generate_fname_setter(
599    out: &mut String,
600    rust_name: &str,
601    byte_lit: &str,
602    prop_name_len: usize,
603    pctx: &PropertyContext,
604) {
605    let c = &pctx.container_expr;
606
607    out.push_str(&format!(
608        "    fn set_{rust_name}(&self, val: rusteal_core::FNameHandle) {{\n"
609    ));
610    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
611    emit_pre_access(out, pctx);
612    out.push_str(&format!(
613        "        rusteal_core::ffi_infallible_ctx(unsafe {{ rusteal_core::ffi_dispatch::property_set_fname({c}, prop, val) }}, \"{rust_name}\");\n\
614         \x20   }}\n\n"
615    ));
616}
617
618// ---------------------------------------------------------------------------
619// Struct getters/setters (OwnedStruct<T>)
620// ---------------------------------------------------------------------------
621
622fn generate_struct_getter(
623    out: &mut String,
624    rust_name: &str,
625    byte_lit: &str,
626    prop_name_len: usize,
627    pctx: &PropertyContext,
628    struct_cpp: &str,
629) {
630    let c = &pctx.container_expr;
631
632    out.push_str(&format!(
633        "    fn get_{rust_name}(&self) -> rusteal_core::OwnedStruct<{struct_cpp}> {{\n"
634    ));
635    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
636    emit_pre_access(out, pctx);
637    out.push_str(&format!(
638        "        let size = unsafe {{ rusteal_core::ffi_dispatch::reflection_get_property_size(prop) }} as usize;\n\
639         \x20       let mut buf = vec![0u8; size];\n\
640         \x20       rusteal_core::ffi_infallible_ctx(unsafe {{\n\
641         \x20           rusteal_core::ffi_dispatch::property_get_struct({c}, prop, buf.as_mut_ptr(), size as u32)\n\
642         \x20       }}, \"{rust_name}\");\n\
643         \x20       rusteal_core::OwnedStruct::from_bytes(buf)\n\
644         \x20   }}\n\n"
645    ));
646}
647
648fn generate_struct_setter(
649    out: &mut String,
650    rust_name: &str,
651    byte_lit: &str,
652    prop_name_len: usize,
653    pctx: &PropertyContext,
654    struct_cpp: &str,
655) {
656    let c = &pctx.container_expr;
657
658    out.push_str(&format!(
659        "    fn set_{rust_name}(&self, val: &rusteal_core::OwnedStruct<{struct_cpp}>) {{\n"
660    ));
661    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
662    emit_pre_access(out, pctx);
663    out.push_str(&format!(
664        "        let __bytes = val.to_bytes();\n\
665         \x20       rusteal_core::ffi_infallible_ctx(unsafe {{\n\
666         \x20           rusteal_core::ffi_dispatch::property_set_struct({c}, prop, __bytes.as_ptr(), __bytes.len() as u32)\n\
667         \x20       }}, \"{rust_name}\");\n\
668         \x20   }}\n\n"
669    ));
670}
671
672// ---------------------------------------------------------------------------
673// Fixed array getter/setter (array_dim > 1)
674// ---------------------------------------------------------------------------
675
676// Every argument is a distinct piece of the emission context; bundling them
677// would only move the list into a struct nobody else needs.
678#[allow(clippy::too_many_arguments)]
679fn generate_fixed_array_property(
680    out: &mut String,
681    prop: &PropertyInfo,
682    rust_name: &str,
683    byte_lit: &str,
684    prop_name_len: usize,
685    pctx: &PropertyContext,
686    ctx: &CodegenContext,
687    mapped: &MappedType,
688) {
689    let array_dim = prop.array_dim;
690    let c = &pctx.container_expr;
691
692    // Determine the Rust return type and conversion for the getter
693    let (getter_ret_type, getter_conversion, setter_param_type, setter_conversion) = match mapped.rust_to_ffi {
694        ConversionKind::Identity if mapped.rust_type == "bool" => (
695            "bool".to_string(),
696            "Ok(buf[0] != 0)".to_string(),
697            "bool".to_string(),
698            "let mut buf = vec![0u8; elem_size];\n\
699             \x20       if val { buf[0] = 1; }"
700                .to_string(),
701        ),
702        ConversionKind::Identity | ConversionKind::IntCast => {
703            let rust_type = &mapped.rust_type;
704            let byte_count = rust_type_byte_size(rust_type);
705            (
706                rust_type.clone(),
707                format!("Ok({rust_type}::from_ne_bytes(buf[..{byte_count}].try_into().unwrap()))"),
708                rust_type.clone(),
709                "let buf = val.to_ne_bytes().to_vec();".to_string(),
710            )
711        }
712        ConversionKind::ObjectRef => {
713            let rust_type = &mapped.rust_type;
714            (
715                rust_type.clone(),
716                "let handle = rusteal_core::UObjectHandle::from_addr(u64::from_ne_bytes(buf[..8].try_into().unwrap()));\n\
717                 \x20       Ok(unsafe { rusteal_core::UObjectRef::from_raw(handle) })".to_string(),
718                rust_type.clone(),
719                "let buf = val.raw().to_addr().to_ne_bytes().to_vec();".to_string(),
720            )
721        }
722        ConversionKind::EnumCast => {
723            let rust_type = &mapped.rust_type;
724            let actual_repr = prop
725                .enum_name
726                .as_deref()
727                .and_then(|en| ctx.enum_actual_repr(en))
728                .unwrap_or(&mapped.rust_ffi_type);
729            let byte_count = rust_type_byte_size(actual_repr);
730            (
731                rust_type.clone(),
732                format!(
733                    "let raw = {actual_repr}::from_ne_bytes(buf[..{byte_count}].try_into().unwrap());\n\
734                     \x20       {rust_type}::from_value(raw).ok_or(rusteal_core::RustealError::TypeMismatch)"
735                ),
736                rust_type.clone(),
737                format!("let buf = (val as {actual_repr}).to_ne_bytes().to_vec();"),
738            )
739        }
740        ConversionKind::StructOpaque => {
741            let struct_cpp = prop.struct_name.as_deref()
742                .and_then(|sn| ctx.structs.get(sn))
743                .map(|si| si.cpp_name.clone())
744                .unwrap_or_else(|| format!("F{}", prop.struct_name.as_deref().unwrap_or("Unknown")));
745            (
746                format!("rusteal_core::OwnedStruct<{struct_cpp}>"),
747                "Ok(rusteal_core::OwnedStruct::from_bytes(buf))".to_string(),
748                format!("&rusteal_core::OwnedStruct<{struct_cpp}>"),
749                "let buf = val.to_bytes();".to_string(),
750            )
751        }
752        _ => return, // Unsupported conversion kind for fixed arrays
753    };
754
755    // Getter
756    out.push_str(&format!(
757        "    fn get_{rust_name}(&self, index: u32) -> rusteal_core::RustealResult<{getter_ret_type}> {{\n"
758    ));
759    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
760    emit_pre_access(out, pctx);
761    out.push_str(&format!(
762        "        if index >= {array_dim} {{ return Err(rusteal_core::RustealError::IndexOutOfRange); }}\n\
763         \x20       let elem_size = unsafe {{ rusteal_core::ffi_dispatch::reflection_get_element_size(prop) }} as usize;\n\
764         \x20       let mut buf = vec![0u8; elem_size];\n\
765         \x20       rusteal_core::check_ffi_ctx(unsafe {{\n\
766         \x20           rusteal_core::ffi_dispatch::property_get_property_at({c}, prop, index, buf.as_mut_ptr(), elem_size as u32)\n\
767         \x20       }}, \"{rust_name}\")?;\n\
768         \x20       {getter_conversion}\n\
769         \x20   }}\n\n"
770    ));
771
772    // Setter
773    out.push_str(&format!(
774        "    fn set_{rust_name}(&self, index: u32, val: {setter_param_type}) -> rusteal_core::RustealResult<()> {{\n"
775    ));
776    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
777    emit_pre_access(out, pctx);
778    out.push_str(&format!(
779        "        if index >= {array_dim} {{ return Err(rusteal_core::RustealError::IndexOutOfRange); }}\n\
780         \x20       let elem_size = unsafe {{ rusteal_core::ffi_dispatch::reflection_get_element_size(prop) }} as usize;\n\
781         \x20       {setter_conversion}\n\
782         \x20       rusteal_core::check_ffi_ctx(unsafe {{\n\
783         \x20           rusteal_core::ffi_dispatch::property_set_property_at({c}, prop, index, buf.as_ptr(), elem_size as u32)\n\
784         \x20       }}, \"{rust_name}\")?;\n\
785         \x20       Ok(())\n\
786         \x20   }}\n\n"
787    ));
788}
789
790/// Get the byte size of a Rust primitive type for from_ne_bytes conversions.
791fn rust_type_byte_size(rust_type: &str) -> usize {
792    match rust_type {
793        "bool" | "i8" | "u8" => 1,
794        "i16" | "u16" => 2,
795        "i32" | "u32" | "f32" => 4,
796        "i64" | "u64" | "f64" => 8,
797        _ => 8, // fallback for pointer-sized types
798    }
799}
800
801// ---------------------------------------------------------------------------
802// Container getter
803// ---------------------------------------------------------------------------
804
805fn generate_container_getter(
806    out: &mut String,
807    rust_name: &str,
808    byte_lit: &str,
809    prop_name_len: usize,
810    pctx: &PropertyContext,
811    container_type: &str,
812) {
813    out.push_str(&format!(
814        "    fn {rust_name}(&self) -> {container_type} {{\n"
815    ));
816    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
817    emit_pre_access(out, pctx);
818    // Container getter returns a handle (not data), constructed from the owner + prop handles.
819    // Use turbofish syntax (::< >) because the type is in expression position.
820    let turbofish_type = container_type.replace('<', "::<");
821    out.push_str(&format!(
822        "        {turbofish_type}::new(h, prop)\n\
823         \x20   }}\n\n"
824    ));
825}