Skip to main content

rusteal_codegen/rust_gen/
properties.rs

1use crate::context::CodegenContext;
2use crate::naming::{strip_bool_prefix, to_snake_case};
3use crate::schema::PropertyInfo;
4use crate::type_map::{self, ConversionKind, MappedType};
5
6pub struct PropertyContext {
7    pub find_prop_fn: String,
8    pub handle_expr: String,
9    pub pre_access: String,
10    pub container_expr: String,
11    pub is_class: bool,
12}
13
14pub fn rust_property_name(prop: &PropertyInfo) -> String {
15    if prop.prop_type == "BoolProperty" {
16        strip_bool_prefix(&prop.name)
17    } else {
18        to_snake_case(&prop.name)
19    }
20}
21
22pub fn is_read_only_non_public(prop: &PropertyInfo) -> bool {
23    use crate::schema::{
24        CPF_BLUEPRINT_READ_ONLY, CPF_NATIVE_ACCESS_PRIVATE, CPF_NATIVE_ACCESS_PROTECTED,
25    };
26
27    prop.prop_flags & (CPF_NATIVE_ACCESS_PRIVATE | CPF_NATIVE_ACCESS_PROTECTED) != 0
28        && prop.prop_flags & CPF_BLUEPRINT_READ_ONLY != 0
29}
30
31pub fn collect_deduped_properties<'a>(
32    props: &'a [PropertyInfo],
33    ctx: Option<&CodegenContext>,
34) -> (std::collections::HashSet<String>, Vec<&'a PropertyInfo>) {
35    let mut prop_names = std::collections::HashSet::new();
36    let mut deduped = Vec::new();
37
38    for prop in props {
39        if prop.getter.is_some() || prop.setter.is_some() {
40            continue;
41        }
42
43        let mapped = type_map::map_property_type(
44            &prop.prop_type,
45            prop.class_name.as_deref(),
46            prop.struct_name.as_deref(),
47            prop.enum_name.as_deref(),
48            prop.enum_underlying_type.as_deref(),
49            prop.meta_class_name.as_deref(),
50            prop.interface_name.as_deref(),
51        );
52
53        if !mapped.supported {
54            continue;
55        }
56
57        if matches!(mapped.rust_to_ffi, ConversionKind::ObjectRef)
58            && let Some(ctx) = ctx
59        {
60            let type_available = match prop.prop_type.as_str() {
61                "ClassProperty" => {
62                    let effective = prop
63                        .meta_class_name
64                        .as_deref()
65                        .or(prop.class_name.as_deref());
66
67                    effective.is_none_or(|c| ctx.classes.contains_key(c))
68                }
69                "InterfaceProperty" => prop
70                    .interface_name
71                    .as_deref()
72                    .is_some_and(|i| ctx.classes.contains_key(i)),
73                _ => prop
74                    .class_name
75                    .as_deref()
76                    .is_none_or(|c| ctx.classes.contains_key(c)),
77            };
78
79            if !type_available {
80                continue;
81            }
82        }
83
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
93            if !valid {
94                continue;
95            }
96        }
97
98        if matches!(
99            mapped.rust_to_ffi,
100            ConversionKind::ContainerArray
101                | ConversionKind::ContainerMap
102                | ConversionKind::ContainerSet
103        ) && type_map::resolve_container_rust_type(prop, ctx).is_none()
104        {
105            continue;
106        }
107
108        if let Some(ctx) = ctx {
109            match mapped.rust_to_ffi {
110                ConversionKind::EnumCast => {
111                    if let Some(en) = &prop.enum_name
112                        && !ctx.enums.contains_key(en.as_str())
113                    {
114                        continue;
115                    }
116                }
117                ConversionKind::ObjectRef => {
118                    if let Some(cn) = &prop.class_name
119                        && !ctx.classes.contains_key(cn.as_str())
120                    {
121                        continue;
122                    }
123                }
124                _ => {}
125            }
126        }
127
128        let rust_name = if prop.prop_type == "BoolProperty" {
129            strip_bool_prefix(&prop.name)
130        } else {
131            to_snake_case(&prop.name)
132        };
133
134        let is_container = matches!(
135            mapped.rust_to_ffi,
136            ConversionKind::ContainerArray
137                | ConversionKind::ContainerMap
138                | ConversionKind::ContainerSet
139        );
140
141        let is_delegate = matches!(
142            mapped.rust_to_ffi,
143            ConversionKind::Delegate | ConversionKind::MulticastDelegate
144        );
145
146        let getter_name = if is_container || is_delegate {
147            rust_name.clone()
148        } else {
149            format!("get_{rust_name}")
150        };
151
152        if prop_names.contains(&getter_name) {
153            continue;
154        }
155
156        prop_names.insert(getter_name);
157
158        if !is_container && !is_delegate {
159            prop_names.insert(format!("set_{rust_name}"));
160        }
161
162        deduped.push(prop);
163    }
164
165    (prop_names, deduped)
166}
167
168pub fn generate_property(
169    out: &mut String,
170    prop: &PropertyInfo,
171    pctx: &PropertyContext,
172    ctx: &CodegenContext,
173    suppress_setters: &std::collections::HashSet<String>,
174) {
175    let mapped = type_map::map_property_type(
176        &prop.prop_type,
177        prop.class_name.as_deref(),
178        prop.struct_name.as_deref(),
179        prop.enum_name.as_deref(),
180        prop.enum_underlying_type.as_deref(),
181        prop.meta_class_name.as_deref(),
182        prop.interface_name.as_deref(),
183    );
184
185    if !mapped.supported {
186        return;
187    }
188
189    if matches!(
190        mapped.rust_to_ffi,
191        ConversionKind::Delegate | ConversionKind::MulticastDelegate
192    ) {
193        return;
194    }
195
196    let prop_name = &prop.name;
197
198    let rust_name = if prop.prop_type == "BoolProperty" {
199        strip_bool_prefix(prop_name)
200    } else {
201        to_snake_case(prop_name)
202    };
203
204    let prop_name_len = prop_name.len();
205    let byte_lit = format!("b\"{prop_name}\\0\"");
206
207    if prop.array_dim > 1 {
208        generate_fixed_array_property(
209            out,
210            prop,
211            &rust_name,
212            &byte_lit,
213            prop_name_len,
214            pctx,
215            ctx,
216            &mapped,
217        );
218
219        return;
220    }
221
222    if matches!(
223        mapped.rust_to_ffi,
224        ConversionKind::ContainerArray
225            | ConversionKind::ContainerMap
226            | ConversionKind::ContainerSet
227    ) {
228        if pctx.is_class
229            && let Some(container_type) = type_map::resolve_container_rust_type(prop, Some(ctx))
230        {
231            generate_container_getter(
232                out,
233                &rust_name,
234                &byte_lit,
235                prop_name_len,
236                pctx,
237                &container_type,
238            );
239        }
240
241        return;
242    }
243
244    match mapped.rust_to_ffi {
245        ConversionKind::StringUtf8 => {
246            generate_string_getter(out, &rust_name, &byte_lit, prop_name_len, pctx);
247        }
248        ConversionKind::StructOpaque => {
249            let struct_cpp = prop
250                .struct_name
251                .as_deref()
252                .and_then(|sn| ctx.structs.get(sn))
253                .map(|si| si.cpp_name.clone())
254                .unwrap_or_else(|| {
255                    format!("F{}", prop.struct_name.as_deref().unwrap_or("Unknown"))
256                });
257
258            generate_struct_getter(out, &rust_name, &byte_lit, prop_name_len, pctx, &struct_cpp);
259            let setter_name = format!("set_{rust_name}");
260
261            if !suppress_setters.contains(&setter_name) {
262                generate_struct_setter(
263                    out,
264                    &rust_name,
265                    &byte_lit,
266                    prop_name_len,
267                    pctx,
268                    &struct_cpp,
269                );
270            }
271
272            return;
273        }
274        ConversionKind::ObjectRef => {
275            generate_object_getter(out, &rust_name, &byte_lit, prop_name_len, pctx, &mapped);
276        }
277        ConversionKind::EnumCast => {
278            let actual_repr = prop
279                .enum_name
280                .as_deref()
281                .and_then(|en| ctx.enum_actual_repr(en))
282                .unwrap_or(&mapped.rust_ffi_type);
283
284            generate_enum_getter(
285                out,
286                &rust_name,
287                &byte_lit,
288                prop_name_len,
289                pctx,
290                &mapped,
291                actual_repr,
292            );
293        }
294        ConversionKind::FName => {
295            generate_fname_getter(out, &rust_name, &byte_lit, prop_name_len, pctx);
296        }
297        ConversionKind::IntCast => {
298            generate_int_cast_getter(out, &rust_name, &byte_lit, prop_name_len, pctx, &mapped);
299        }
300        _ => {
301            generate_primitive_getter(out, &rust_name, &byte_lit, prop_name_len, pctx, &mapped);
302        }
303    }
304
305    let setter_name = format!("set_{rust_name}");
306
307    if suppress_setters.contains(&setter_name) {
308        return;
309    }
310
311    match mapped.rust_to_ffi {
312        ConversionKind::StringUtf8 => {
313            generate_string_setter(out, &rust_name, &byte_lit, prop_name_len, pctx);
314        }
315        ConversionKind::StructOpaque => {}
316        ConversionKind::ObjectRef => {
317            generate_object_setter(out, &rust_name, &byte_lit, prop_name_len, pctx, &mapped);
318        }
319        ConversionKind::EnumCast => {
320            generate_enum_setter(out, &rust_name, &byte_lit, prop_name_len, pctx, &mapped);
321        }
322        ConversionKind::FName => {
323            generate_fname_setter(out, &rust_name, &byte_lit, prop_name_len, pctx);
324        }
325        ConversionKind::IntCast => {
326            generate_int_cast_setter(out, &rust_name, &byte_lit, prop_name_len, pctx, &mapped);
327        }
328        _ => {
329            generate_primitive_setter(out, &rust_name, &byte_lit, prop_name_len, pctx, &mapped);
330        }
331    }
332}
333
334pub fn default_value_for(rust_type: &str) -> &'static str {
335    match rust_type {
336        "bool" => "false",
337        "i8" | "u8" | "i16" | "u16" | "i32" | "u32" | "i64" | "u64" => "0",
338        "f32" => "0.0f32",
339        "f64" => "0.0f64",
340        _ => "Default::default()",
341    }
342}
343
344fn emit_prop_lookup(
345    out: &mut String,
346    byte_lit: &str,
347    prop_name_len: usize,
348    pctx: &PropertyContext,
349) {
350    let find = &pctx.find_prop_fn;
351    let handle = &pctx.handle_expr;
352
353    out.push_str(&format!(
354        "        static PROP: std::sync::OnceLock<rusteal_core::FPropertyHandle> = std::sync::OnceLock::new();\n\
355         \x20       let prop = *PROP.get_or_init(|| unsafe {{\n\
356         \x20           rusteal_core::ffi_dispatch::reflection_{find}(\n\
357         \x20               {handle}, {byte_lit}.as_ptr(), {prop_name_len}\n\
358         \x20           )\n\
359         \x20       }});\n"
360    ));
361}
362
363fn emit_pre_access(out: &mut String, pctx: &PropertyContext) {
364    if !pctx.pre_access.is_empty() {
365        out.push_str(&format!("        {}\n", pctx.pre_access));
366    }
367}
368
369fn generate_primitive_getter(
370    out: &mut String,
371    rust_name: &str,
372    byte_lit: &str,
373    prop_name_len: usize,
374    pctx: &PropertyContext,
375    mapped: &MappedType,
376) {
377    let rust_type = &mapped.rust_type;
378    let getter = &mapped.property_getter;
379    let default = default_value_for(rust_type);
380    let c = &pctx.container_expr;
381
382    out.push_str(&format!(
383        "    fn get_{rust_name}(&self) -> {rust_type} {{\n"
384    ));
385
386    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
387    emit_pre_access(out, pctx);
388
389    out.push_str(&format!(
390        "        let mut out = {default};\n\
391         \x20       rusteal_core::ffi_infallible_ctx(unsafe {{ rusteal_core::ffi_dispatch::property_{getter}({c}, prop, &mut out) }}, \"{rust_name}\");\n\
392         \x20       out\n\
393         \x20   }}\n\n"
394    ));
395}
396
397fn generate_int_cast_getter(
398    out: &mut String,
399    rust_name: &str,
400    byte_lit: &str,
401    prop_name_len: usize,
402    pctx: &PropertyContext,
403    mapped: &MappedType,
404) {
405    let rust_type = &mapped.rust_type;
406    let ffi_type = &mapped.rust_ffi_type;
407    let getter = &mapped.property_getter;
408    let default = default_value_for(ffi_type);
409    let c = &pctx.container_expr;
410
411    out.push_str(&format!(
412        "    fn get_{rust_name}(&self) -> {rust_type} {{\n"
413    ));
414
415    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
416    emit_pre_access(out, pctx);
417
418    out.push_str(&format!(
419        "        let mut out = {default};\n\
420         \x20       rusteal_core::ffi_infallible_ctx(unsafe {{ rusteal_core::ffi_dispatch::property_{getter}({c}, prop, &mut out) }}, \"{rust_name}\");\n\
421         \x20       out as {rust_type}\n\
422         \x20   }}\n\n"
423    ));
424}
425
426fn generate_string_getter(
427    out: &mut String,
428    rust_name: &str,
429    byte_lit: &str,
430    prop_name_len: usize,
431    pctx: &PropertyContext,
432) {
433    let c = &pctx.container_expr;
434
435    out.push_str(&format!("    fn get_{rust_name}(&self) -> String {{\n"));
436
437    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
438    emit_pre_access(out, pctx);
439
440    out.push_str(&format!(
441        "        let mut buf = vec![0u8; 512];\n\
442         \x20       let mut out_len: u32 = 0;\n\
443         \x20       rusteal_core::ffi_infallible_ctx(unsafe {{\n\
444         \x20           rusteal_core::ffi_dispatch::property_get_string({c}, prop, buf.as_mut_ptr(), buf.len() as u32, &mut out_len)\n\
445         \x20       }}, \"{rust_name}\");\n\
446         \x20       buf.truncate(out_len as usize);\n\
447         \x20       String::from_utf8_lossy(&buf).into_owned()\n\
448         \x20   }}\n\n"
449    ));
450}
451
452fn generate_object_getter(
453    out: &mut String,
454    rust_name: &str,
455    byte_lit: &str,
456    prop_name_len: usize,
457    pctx: &PropertyContext,
458    mapped: &MappedType,
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
467    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
468    emit_pre_access(out, pctx);
469
470    out.push_str(&format!(
471        "        let mut raw = rusteal_core::UObjectHandle::null();\n\
472         \x20       rusteal_core::ffi_infallible_ctx(unsafe {{ rusteal_core::ffi_dispatch::property_get_object({c}, prop, &mut raw) }}, \"{rust_name}\");\n\
473         \x20       unsafe {{ rusteal_core::ObjectPointer::from_object_handle(raw) }}\n\
474         \x20   }}\n\n"
475    ));
476}
477
478fn generate_enum_getter(
479    out: &mut String,
480    rust_name: &str,
481    byte_lit: &str,
482    prop_name_len: usize,
483    pctx: &PropertyContext,
484    mapped: &MappedType,
485    actual_repr: &str,
486) {
487    let rust_type = &mapped.rust_type;
488    let c = &pctx.container_expr;
489
490    out.push_str(&format!(
491        "    fn get_{rust_name}(&self) -> {rust_type} {{\n"
492    ));
493
494    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
495    emit_pre_access(out, pctx);
496
497    out.push_str(&format!(
498        "        let mut raw: i64 = 0;\n\
499         \x20       rusteal_core::ffi_infallible_ctx(unsafe {{ rusteal_core::ffi_dispatch::property_get_enum({c}, prop, &mut raw) }}, \"{rust_name}\");\n\
500         \x20       {rust_type}::from_value(raw as {actual_repr}).expect(\"unknown enum value\")\n\
501         \x20   }}\n\n"
502    ));
503}
504
505fn generate_fname_getter(
506    out: &mut String,
507    rust_name: &str,
508    byte_lit: &str,
509    prop_name_len: usize,
510    pctx: &PropertyContext,
511) {
512    let c = &pctx.container_expr;
513
514    out.push_str(&format!(
515        "    fn get_{rust_name}(&self) -> rusteal_core::FNameHandle {{\n"
516    ));
517
518    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
519    emit_pre_access(out, pctx);
520
521    out.push_str(&format!(
522        "        let mut out = rusteal_core::FNameHandle(0);\n\
523         \x20       rusteal_core::ffi_infallible_ctx(unsafe {{ rusteal_core::ffi_dispatch::property_get_fname({c}, prop, &mut out) }}, \"{rust_name}\");\n\
524         \x20       out\n\
525         \x20   }}\n\n"
526    ));
527}
528
529fn generate_primitive_setter(
530    out: &mut String,
531    rust_name: &str,
532    byte_lit: &str,
533    prop_name_len: usize,
534    pctx: &PropertyContext,
535    mapped: &MappedType,
536) {
537    let rust_type = &mapped.rust_type;
538    let setter = &mapped.property_setter;
539    let c = &pctx.container_expr;
540
541    out.push_str(&format!(
542        "    fn set_{rust_name}(&self, val: {rust_type}) {{\n"
543    ));
544
545    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
546    emit_pre_access(out, pctx);
547
548    out.push_str(&format!(
549        "        rusteal_core::ffi_infallible_ctx(unsafe {{ rusteal_core::ffi_dispatch::property_{setter}({c}, prop, val) }}, \"{rust_name}\");\n\
550         \x20   }}\n\n"
551    ));
552}
553
554fn generate_int_cast_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 ffi_type = &mapped.rust_ffi_type;
564    let setter = &mapped.property_setter;
565    let c = &pctx.container_expr;
566
567    out.push_str(&format!(
568        "    fn set_{rust_name}(&self, val: {rust_type}) {{\n"
569    ));
570
571    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
572    emit_pre_access(out, pctx);
573
574    out.push_str(&format!(
575        "        rusteal_core::ffi_infallible_ctx(unsafe {{ rusteal_core::ffi_dispatch::property_{setter}({c}, prop, val as {ffi_type}) }}, \"{rust_name}\");\n\
576         \x20   }}\n\n"
577    ));
578}
579
580fn generate_string_setter(
581    out: &mut String,
582    rust_name: &str,
583    byte_lit: &str,
584    prop_name_len: usize,
585    pctx: &PropertyContext,
586) {
587    let c = &pctx.container_expr;
588
589    out.push_str(&format!("    fn set_{rust_name}(&self, val: &str) {{\n"));
590
591    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
592    emit_pre_access(out, pctx);
593
594    out.push_str(&format!(
595        "        rusteal_core::ffi_infallible_ctx(unsafe {{\n\
596         \x20           rusteal_core::ffi_dispatch::property_set_string({c}, prop, val.as_ptr(), val.len() as u32)\n\
597         \x20       }}, \"{rust_name}\");\n\
598         \x20   }}\n\n"
599    ));
600}
601
602fn generate_object_setter(
603    out: &mut String,
604    rust_name: &str,
605    byte_lit: &str,
606    prop_name_len: usize,
607    pctx: &PropertyContext,
608    mapped: &MappedType,
609) {
610    let rust_type = &mapped.rust_type;
611    let c = &pctx.container_expr;
612
613    out.push_str(&format!(
614        "    fn set_{rust_name}(&self, val: {rust_type}) {{\n"
615    ));
616
617    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
618    emit_pre_access(out, pctx);
619
620    out.push_str(&format!(
621        "        rusteal_core::ffi_infallible_ctx(unsafe {{ rusteal_core::ffi_dispatch::property_set_object({c}, prop, rusteal_core::ObjectPointer::object_handle(&val)) }}, \"{rust_name}\");\n\
622         \x20   }}\n\n"
623    ));
624}
625
626fn generate_enum_setter(
627    out: &mut String,
628    rust_name: &str,
629    byte_lit: &str,
630    prop_name_len: usize,
631    pctx: &PropertyContext,
632    mapped: &MappedType,
633) {
634    let rust_type = &mapped.rust_type;
635    let c = &pctx.container_expr;
636
637    out.push_str(&format!(
638        "    fn set_{rust_name}(&self, val: {rust_type}) {{\n"
639    ));
640
641    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
642    emit_pre_access(out, pctx);
643
644    out.push_str(&format!(
645        "        rusteal_core::ffi_infallible_ctx(unsafe {{ rusteal_core::ffi_dispatch::property_set_enum({c}, prop, val as i64) }}, \"{rust_name}\");\n\
646         \x20   }}\n\n"
647    ));
648}
649
650fn generate_fname_setter(
651    out: &mut String,
652    rust_name: &str,
653    byte_lit: &str,
654    prop_name_len: usize,
655    pctx: &PropertyContext,
656) {
657    let c = &pctx.container_expr;
658
659    out.push_str(&format!(
660        "    fn set_{rust_name}(&self, val: rusteal_core::FNameHandle) {{\n"
661    ));
662
663    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
664    emit_pre_access(out, pctx);
665
666    out.push_str(&format!(
667        "        rusteal_core::ffi_infallible_ctx(unsafe {{ rusteal_core::ffi_dispatch::property_set_fname({c}, prop, val) }}, \"{rust_name}\");\n\
668         \x20   }}\n\n"
669    ));
670}
671
672fn generate_struct_getter(
673    out: &mut String,
674    rust_name: &str,
675    byte_lit: &str,
676    prop_name_len: usize,
677    pctx: &PropertyContext,
678    struct_cpp: &str,
679) {
680    let c = &pctx.container_expr;
681
682    out.push_str(&format!(
683        "    fn get_{rust_name}(&self) -> rusteal_core::OwnedStruct<{struct_cpp}> {{\n"
684    ));
685
686    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
687    emit_pre_access(out, pctx);
688
689    out.push_str(&format!(
690        "        let size = unsafe {{ rusteal_core::ffi_dispatch::reflection_get_property_size(prop) }} as usize;\n\
691         \x20       let mut buf = vec![0u8; size];\n\
692         \x20       rusteal_core::ffi_infallible_ctx(unsafe {{\n\
693         \x20           rusteal_core::ffi_dispatch::property_get_struct({c}, prop, buf.as_mut_ptr(), size as u32)\n\
694         \x20       }}, \"{rust_name}\");\n\
695         \x20       rusteal_core::OwnedStruct::from_bytes(buf)\n\
696         \x20   }}\n\n"
697    ));
698}
699
700fn generate_struct_setter(
701    out: &mut String,
702    rust_name: &str,
703    byte_lit: &str,
704    prop_name_len: usize,
705    pctx: &PropertyContext,
706    struct_cpp: &str,
707) {
708    let c = &pctx.container_expr;
709
710    out.push_str(&format!(
711        "    fn set_{rust_name}(&self, val: &rusteal_core::OwnedStruct<{struct_cpp}>) {{\n"
712    ));
713
714    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
715    emit_pre_access(out, pctx);
716
717    out.push_str(&format!(
718        "        let __bytes = val.to_bytes();\n\
719         \x20       rusteal_core::ffi_infallible_ctx(unsafe {{\n\
720         \x20           rusteal_core::ffi_dispatch::property_set_struct({c}, prop, __bytes.as_ptr(), __bytes.len() as u32)\n\
721         \x20       }}, \"{rust_name}\");\n\
722         \x20   }}\n\n"
723    ));
724}
725
726#[allow(clippy::too_many_arguments)]
727fn generate_fixed_array_property(
728    out: &mut String,
729    prop: &PropertyInfo,
730    rust_name: &str,
731    byte_lit: &str,
732    prop_name_len: usize,
733    pctx: &PropertyContext,
734    ctx: &CodegenContext,
735    mapped: &MappedType,
736) {
737    let array_dim = prop.array_dim;
738    let c = &pctx.container_expr;
739
740    let (getter_ret_type, getter_conversion, setter_param_type, setter_conversion) = match mapped
741        .rust_to_ffi
742    {
743        ConversionKind::Identity if mapped.rust_type == "bool" => (
744            "bool".to_string(),
745            "Ok(buf[0] != 0)".to_string(),
746            "bool".to_string(),
747            "let mut buf = vec![0u8; elem_size];\n\
748             \x20       if val { buf[0] = 1; }"
749                .to_string(),
750        ),
751        ConversionKind::Identity | ConversionKind::IntCast => {
752            let rust_type = &mapped.rust_type;
753            let byte_count = rust_type_byte_size(rust_type);
754
755            (
756                rust_type.clone(),
757                format!("Ok({rust_type}::from_ne_bytes(buf[..{byte_count}].try_into().unwrap()))"),
758                rust_type.clone(),
759                "let buf = val.to_ne_bytes().to_vec();".to_string(),
760            )
761        }
762        ConversionKind::ObjectRef => {
763            let rust_type = &mapped.rust_type;
764
765            (
766                rust_type.clone(),
767                "let handle = rusteal_core::UObjectHandle::from_addr(u64::from_ne_bytes(buf[..8].try_into().unwrap()));\n\
768                 \x20       Ok(unsafe { rusteal_core::ObjectPointer::from_object_handle(handle) })".to_string(),
769                rust_type.clone(),
770                "let buf = rusteal_core::ObjectPointer::object_handle(&val).to_addr().to_ne_bytes().to_vec();".to_string(),
771            )
772        }
773        ConversionKind::EnumCast => {
774            let rust_type = &mapped.rust_type;
775
776            let actual_repr = prop
777                .enum_name
778                .as_deref()
779                .and_then(|en| ctx.enum_actual_repr(en))
780                .unwrap_or(&mapped.rust_ffi_type);
781
782            let byte_count = rust_type_byte_size(actual_repr);
783
784            (
785                rust_type.clone(),
786                format!(
787                    "let raw = {actual_repr}::from_ne_bytes(buf[..{byte_count}].try_into().unwrap());\n\
788                     \x20       {rust_type}::from_value(raw).ok_or(rusteal_core::RustealError::TypeMismatch)"
789                ),
790                rust_type.clone(),
791                format!("let buf = (val as {actual_repr}).to_ne_bytes().to_vec();"),
792            )
793        }
794        ConversionKind::StructOpaque => {
795            let struct_cpp = prop
796                .struct_name
797                .as_deref()
798                .and_then(|sn| ctx.structs.get(sn))
799                .map(|si| si.cpp_name.clone())
800                .unwrap_or_else(|| {
801                    format!("F{}", prop.struct_name.as_deref().unwrap_or("Unknown"))
802                });
803
804            (
805                format!("rusteal_core::OwnedStruct<{struct_cpp}>"),
806                "Ok(rusteal_core::OwnedStruct::from_bytes(buf))".to_string(),
807                format!("&rusteal_core::OwnedStruct<{struct_cpp}>"),
808                "let buf = val.to_bytes();".to_string(),
809            )
810        }
811        _ => return,
812    };
813
814    out.push_str(&format!(
815        "    fn get_{rust_name}(&self, index: u32) -> rusteal_core::RustealResult<{getter_ret_type}> {{\n"
816    ));
817
818    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
819    emit_pre_access(out, pctx);
820
821    out.push_str(&format!(
822        "        if index >= {array_dim} {{ return Err(rusteal_core::RustealError::IndexOutOfRange); }}\n\
823         \x20       let elem_size = unsafe {{ rusteal_core::ffi_dispatch::reflection_get_element_size(prop) }} as usize;\n\
824         \x20       let mut buf = vec![0u8; elem_size];\n\
825         \x20       rusteal_core::check_ffi_ctx(unsafe {{\n\
826         \x20           rusteal_core::ffi_dispatch::property_get_property_at({c}, prop, index, buf.as_mut_ptr(), elem_size as u32)\n\
827         \x20       }}, \"{rust_name}\")?;\n\
828         \x20       {getter_conversion}\n\
829         \x20   }}\n\n"
830    ));
831
832    out.push_str(&format!(
833        "    fn set_{rust_name}(&self, index: u32, val: {setter_param_type}) -> rusteal_core::RustealResult<()> {{\n"
834    ));
835
836    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
837    emit_pre_access(out, pctx);
838
839    out.push_str(&format!(
840        "        if index >= {array_dim} {{ return Err(rusteal_core::RustealError::IndexOutOfRange); }}\n\
841         \x20       let elem_size = unsafe {{ rusteal_core::ffi_dispatch::reflection_get_element_size(prop) }} as usize;\n\
842         \x20       {setter_conversion}\n\
843         \x20       rusteal_core::check_ffi_ctx(unsafe {{\n\
844         \x20           rusteal_core::ffi_dispatch::property_set_property_at({c}, prop, index, buf.as_ptr(), elem_size as u32)\n\
845         \x20       }}, \"{rust_name}\")?;\n\
846         \x20       Ok(())\n\
847         \x20   }}\n\n"
848    ));
849}
850
851fn rust_type_byte_size(rust_type: &str) -> usize {
852    match rust_type {
853        "bool" | "i8" | "u8" => 1,
854        "i16" | "u16" => 2,
855        "i32" | "u32" | "f32" => 4,
856        "i64" | "u64" | "f64" => 8,
857        _ => 8,
858    }
859}
860
861fn generate_container_getter(
862    out: &mut String,
863    rust_name: &str,
864    byte_lit: &str,
865    prop_name_len: usize,
866    pctx: &PropertyContext,
867    container_type: &str,
868) {
869    out.push_str(&format!(
870        "    fn {rust_name}(&self) -> {container_type} {{\n"
871    ));
872
873    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
874    emit_pre_access(out, pctx);
875
876    let turbofish_type = container_type.replace('<', "::<");
877
878    out.push_str(&format!(
879        "        {turbofish_type}::new(h, prop)\n\
880         \x20   }}\n\n"
881    ));
882}
883
884#[cfg(test)]
885mod tests {
886    use super::*;
887    use rusteal_ue_flags::{
888        CPF_BLUEPRINT_READ_ONLY, CPF_BLUEPRINT_VISIBLE, CPF_NATIVE_ACCESS_SPECIFIER_PRIVATE,
889        CPF_NATIVE_ACCESS_SPECIFIER_PUBLIC,
890    };
891
892    fn prop(name: &str, prop_type: &str, flags: u64) -> PropertyInfo {
893        serde_json::from_value(serde_json::json!({
894            "name": name,
895            "type": prop_type,
896            "prop_flags": flags,
897        }))
898        .unwrap()
899    }
900
901    #[test]
902    fn only_non_public_read_only_properties_lose_their_setter() {
903        let read_only = CPF_BLUEPRINT_VISIBLE | CPF_BLUEPRINT_READ_ONLY;
904
905        assert!(is_read_only_non_public(&prop(
906            "Mesh",
907            "ObjectProperty",
908            CPF_NATIVE_ACCESS_SPECIFIER_PRIVATE | read_only
909        )));
910
911        assert!(!is_read_only_non_public(&prop(
912            "Mesh",
913            "ObjectProperty",
914            CPF_NATIVE_ACCESS_SPECIFIER_PRIVATE | CPF_BLUEPRINT_VISIBLE
915        )));
916
917        assert!(!is_read_only_non_public(&prop(
918            "Speed",
919            "FloatProperty",
920            CPF_NATIVE_ACCESS_SPECIFIER_PUBLIC | read_only
921        )));
922    }
923
924    #[test]
925    fn accessor_stems() {
926        assert_eq!(
927            rust_property_name(&prop("CharacterMovement", "ObjectProperty", 0)),
928            "character_movement"
929        );
930
931        assert_eq!(
932            rust_property_name(&prop("bUseControllerRotationYaw", "BoolProperty", 0)),
933            "use_controller_rotation_yaw"
934        );
935    }
936}