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}