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