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