use proc_macro2::TokenStream;
use quote::quote;
use syn::Type;
pub struct PropTypeInfo {
pub prop_type_expr: TokenStream,
pub rust_type: TokenStream,
pub kind: PropKind,
}
pub enum PropKind {
Scalar {
getter_fn: syn::Ident,
setter_fn: syn::Ident,
zero_expr: TokenStream,
},
Object { class: Type },
Class { meta_class: Type },
Array { element: Box<PropTypeInfo> },
Struct { strukt: Type },
}
impl PropTypeInfo {
pub fn extra_fields(&self) -> Option<TokenStream> {
match &self.kind {
PropKind::Scalar { .. } => None,
PropKind::Object { class } => Some(quote! {
class_handle: <#class as ::rusteal_runtime::runtime::UeClass>::static_class(),
}),
PropKind::Class { meta_class } => Some(quote! {
meta_class_handle: <#meta_class as ::rusteal_runtime::runtime::UeClass>::static_class(),
}),
PropKind::Struct { strukt } => Some(quote! {
struct_handle: <#strukt as ::rusteal_runtime::runtime::UeStruct>::static_struct(),
}),
PropKind::Array { element } => {
let inner_type = &element.prop_type_expr;
let inner_fields = element.extra_fields().unwrap_or_default();
Some(quote! {
#inner_fields
inner_prop_type: #inner_type as u32,
})
}
}
}
}
fn single_type_arg(seg: &syn::PathSegment) -> Option<Type> {
let syn::PathArguments::AngleBracketed(args) = &seg.arguments else {
return None;
};
let mut types = args.args.iter().filter_map(|a| match a {
syn::GenericArgument::Type(t) => Some(t.clone()),
_ => None,
});
let ty = types.next()?;
types.next().is_none().then_some(ty)
}
pub fn map_type(ty: &Type) -> Option<PropTypeInfo> {
let seg = match ty {
Type::Path(tp) => tp.path.segments.last()?,
_ => return None,
};
match seg.ident.to_string().as_str() {
"UObjectRef" => {
let class = single_type_arg(seg)?;
return Some(PropTypeInfo {
prop_type_expr: quote! { ::rusteal_runtime::ffi::RustealReifyPropType::Object },
rust_type: quote! { #ty },
kind: PropKind::Object { class },
});
}
"SubclassOf" => {
let meta_class = single_type_arg(seg)?;
return Some(PropTypeInfo {
prop_type_expr: quote! { ::rusteal_runtime::ffi::RustealReifyPropType::Class },
rust_type: quote! { #ty },
kind: PropKind::Class { meta_class },
});
}
"UStructRef" => {
let strukt = single_type_arg(seg)?;
return Some(PropTypeInfo {
prop_type_expr: quote! { ::rusteal_runtime::ffi::RustealReifyPropType::Struct },
rust_type: quote! { #ty },
kind: PropKind::Struct { strukt },
});
}
"UeArray" => {
let element = map_type(&single_type_arg(seg)?)?;
if matches!(element.kind, PropKind::Array { .. } | PropKind::Struct { .. }) {
return None; }
return Some(PropTypeInfo {
prop_type_expr: quote! { ::rusteal_runtime::ffi::RustealReifyPropType::Array },
rust_type: quote! { #ty },
kind: PropKind::Array { element: Box::new(element) },
});
}
_ => {}
}
let ident = |s: &str| syn::Ident::new(s, proc_macro2::Span::call_site());
let scalar = |variant: &str, rust: TokenStream, get: &str, set: &str, zero: TokenStream| {
let variant = ident(variant);
PropTypeInfo {
prop_type_expr: quote! { ::rusteal_runtime::ffi::RustealReifyPropType::#variant },
rust_type: rust,
kind: PropKind::Scalar {
getter_fn: ident(get),
setter_fn: ident(set),
zero_expr: zero,
},
}
};
let info = match seg.ident.to_string().as_str() {
"bool" => scalar("Bool", quote! { bool }, "get_bool", "set_bool", quote! { false }),
"i32" => scalar("Int32", quote! { i32 }, "get_i32", "set_i32", quote! { 0i32 }),
"i64" => scalar("Int64", quote! { i64 }, "get_i64", "set_i64", quote! { 0i64 }),
"u8" => scalar("UInt8", quote! { u8 }, "get_u8", "set_u8", quote! { 0u8 }),
"f32" => scalar("Float", quote! { f32 }, "get_f32", "set_f32", quote! { 0.0f32 }),
"f64" => scalar("Double", quote! { f64 }, "get_f64", "set_f64", quote! { 0.0f64 }),
_ => return None,
};
Some(info)
}
pub fn to_pascal_case(s: &str) -> String {
s.split('_')
.filter(|part| !part.is_empty())
.map(|part| {
let mut chars = part.chars();
match chars.next() {
Some(c) => {
let upper: String = c.to_uppercase().collect();
upper + chars.as_str()
}
None => String::new(),
}
})
.collect()
}
pub fn fnv1a_hash(s: &str) -> u64 {
const FNV_OFFSET: u64 = 0xcbf29ce484222325;
const FNV_PRIME: u64 = 0x100000001b3;
let mut hash = FNV_OFFSET;
for &b in s.as_bytes() {
hash ^= b as u64;
hash = hash.wrapping_mul(FNV_PRIME);
}
hash
}
#[cfg(test)]
mod tests {
use super::*;
use syn::parse_quote;
fn kind(ty: Type) -> Option<&'static str> {
map_type(&ty).map(|info| match info.kind {
PropKind::Scalar { .. } => "scalar",
PropKind::Object { .. } => "object",
PropKind::Class { .. } => "class",
PropKind::Array { .. } => "array",
PropKind::Struct { .. } => "struct",
})
}
#[test]
fn maps_supported_types() {
assert_eq!(kind(parse_quote!(f32)), Some("scalar"));
assert_eq!(kind(parse_quote!(UObjectRef<InputAction>)), Some("object"));
assert_eq!(kind(parse_quote!(rusteal_runtime::runtime::UObjectRef<InputAction>)), Some("object"));
assert_eq!(kind(parse_quote!(SubclassOf<Pawn>)), Some("class"));
assert_eq!(kind(parse_quote!(UeArray<UObjectRef<InputMappingContext>>)), Some("array"));
assert_eq!(kind(parse_quote!(UeArray<SubclassOf<Actor>>)), Some("array"));
assert_eq!(kind(parse_quote!(UeArray<f32>)), Some("array"));
assert_eq!(kind(parse_quote!(UStructRef<FInputActionValue>)), Some("struct"));
}
#[test]
fn rejects_unsupported_types() {
assert_eq!(kind(parse_quote!(String)), None);
assert_eq!(kind(parse_quote!(UObjectRef)), None);
assert_eq!(kind(parse_quote!(UeArray<UeArray<f32>>)), None);
assert_eq!(kind(parse_quote!(UeArray<String>)), None);
assert_eq!(kind(parse_quote!(UeArray<UStructRef<FVector>>)), None);
assert_eq!(kind(parse_quote!(Vec<UObjectRef<InputAction>>)), None);
}
#[test]
fn array_extra_describes_the_element() {
let info = map_type(&parse_quote!(UeArray<UObjectRef<InputAction>>)).unwrap();
let extra = info.extra_fields().unwrap().to_string();
assert!(extra.contains("class_handle"), "{extra}");
assert!(extra.contains("inner_prop_type"), "{extra}");
assert!(extra.contains("RustealReifyPropType :: Object"), "{extra}");
}
}