use std::{borrow::Cow, collections::BTreeMap, path::PathBuf}; use clap::{Args, Parser, Subcommand}; use unreal_sdk::sdk::repr::{ObjectRef, Sdk}; use crate::rust::Builder; #[derive(Parser)] #[command(author, version, about, long_about = None)] pub struct Cli { #[command(subcommand)] commands: Option, } #[derive(Args)] pub struct Build { #[arg(short, long)] in_archive: PathBuf, /// directory into which the sdk will be dumped. #[arg(short, long)] out: Option, #[arg(short, long, default_value = "false")] single_file: bool, #[arg(short, long, default_value = "true")] feature_gate: bool, #[arg(long, value_delimiter = ',', num_args = 1..)] packages: Option>, } #[derive(Subcommand)] pub enum Commands { Build(Build), } fn main() -> anyhow::Result<()> { env_logger::init(); println!("Hello, world!"); let cli = Cli::parse(); if let Some(Commands::Build(build)) = &cli.commands { let sdk = Sdk::from_path_ron(&build.in_archive)?; let builder = Builder::new(sdk); let path = build.out.clone().unwrap_or(std::env::current_dir()?); builder.build_in_dir(path, build)?; } Ok(()) } struct SplitResult<'a> { start: Option<&'a str>, middle: usize, end: Option<&'a str>, } impl<'a> SplitResult<'a> { pub fn is_valid(&self) -> bool { self.start.is_some() && self.middle == 0 && self.end.is_none() } pub fn into_valid(self, disallowed_tokens: &[char]) -> Cow<'a, str> { if self.is_valid() { Cow::Borrowed(self.start.unwrap()) } else { let mut valid = self.start.map(|s| s.to_string()).unwrap_or_default(); valid.extend(core::iter::repeat('_').take(self.middle)); match self.end { Some(end) => { valid.push_str( &split_at_illegal_char(end, disallowed_tokens) .into_valid(disallowed_tokens), ); } None => {} } Cow::Owned(valid) } } } fn split_at_illegal_char<'a>(input: &'a str, disallowed_tokens: &[char]) -> SplitResult<'a> { let illegal_chars = disallowed_tokens; if let Some(pos) = input.find(|c| illegal_chars.contains(&c)) { let start = empty_or_some(&input[..pos]); // skip the illegal char let rest = &input[pos + 1..]; if let Some(pos2) = rest.find(|c| !illegal_chars.contains(&c)) { SplitResult { start, middle: pos2 + 1, end: empty_or_some(&rest[pos2..]), } } else { SplitResult { start, middle: 1, end: empty_or_some(rest), } } } else { SplitResult { start: empty_or_some(input), middle: 0, end: None, } } } fn canonicalize_name<'a>( name: &'a str, disallowed_tokens: &[char], disallowed_strs: &[&str], ) -> Cow<'a, str> { let valid = split_at_illegal_char(name, disallowed_tokens).into_valid(disallowed_tokens); if disallowed_strs.contains(&valid.as_ref()) || valid.starts_with(|c: char| !c.is_alphabetic()) { Cow::Owned(format!("_{}", &valid)) } else { valid } } fn empty_or_some(s: &str) -> Option<&str> { if s.is_empty() { None } else { Some(s) } } pub struct CanonicalNames { /// canonicalized type names for lookup when handling return types and parameters. types: BTreeMap, } pub mod rust { use std::{ borrow::Cow, collections::{BTreeMap, BTreeSet, HashMap, HashSet}, path::Path, }; use anyhow::Context; use itertools::Itertools; use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, quote}; use rayon::prelude::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator}; use unreal_sdk::sdk::repr::{ Class, ClassField, ClassMethod, Enum, ObjectRef, PackageRef, PrimitiveType, ProcessedPackage, Sdk, StructKind, Type, UnrealType, }; use crate::split_at_illegal_char; // const KEYWORDS: [&'static str; 51] = [ // "as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", // "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", // "return", "self", "Self", "static", "struct", "super", "trait", "true", "type", "unsafe", // "use", "where", "while", "async", "await", "dyn", "abstract", "become", "box", "do", // "final", "macro", "override", "priv", "typeof", "unsized", "virtual", "yield", "try", // ]; // const TYPES: [&'static str; 17] = [ // "bool", "f64", "f32", "str", "char", "u8", "u16", "u32", "u64", "u128", "i8", "i16", "i32", // "i64", "i128", "usize", "isize", // ]; const WORDS: [&'static str; 68] = [ "as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return", "self", "Self", "static", "struct", "super", "trait", "true", "type", "unsafe", "use", "where", "while", "async", "await", "dyn", "abstract", "become", "box", "do", "final", "macro", "override", "priv", "typeof", "unsized", "virtual", "yield", "try", "bool", "f64", "f32", "str", "char", "u8", "u16", "u32", "u64", "u128", "i8", "i16", "i32", "i64", "i128", "usize", "isize", ]; const CHARS: [char; 20] = [ ' ', '?', '+', '-', ':', '/', '^', '(', ')', '[', ']', '<', '>', '&', '.', '#', '\'', '"', '%', ',', ]; pub struct Builder { type_name_cache: BTreeMap, sdk: Sdk, } fn canonicalize_name<'a>(name: &'a str) -> Cow<'a, str> { let valid = split_at_illegal_char(name, &CHARS).into_valid(&CHARS); if WORDS.contains(&valid.as_ref()) || valid.starts_with(|c: char| !c.is_alphabetic()) { Cow::Owned(format!("_{}", &valid)) } else { valid } } impl Builder { pub fn new(sdk: Sdk) -> Self { let type_name_cache = sdk .packages .iter() .flat_map(|(_, pkg)| { pkg.types .values() .map(|ty| (ty.obj_ref(), Self::get_prefixed_name(&ty))) }) .collect::>(); Self { type_name_cache, sdk, } } /// returns the absolute path of a type with the assumption that all /// packages are children of the path `crate::sdk` fn get_type_package_path(&self, key: &ObjectRef) -> Option { let pkg = &self.sdk.packages.get(&key.package)?.name; Some(format!("crate::sdk::{pkg}")) } /// returns the absolute path of a type with the assumption that all /// packages are children of the path `crate::sdk` fn get_type_path(&self, key: &ObjectRef) -> Option { let pkg = &self.sdk.packages.get(&key.package)?.name; self.get_type_name(key) .map(|name| format!("crate::sdk::{pkg}::{name}")) } /// returns the precached, prefixed and cannonicalized (for this /// language, Rust) name for this object-ref fn get_type_name(&self, key: &ObjectRef) -> Option { self.type_name_cache.get(key).cloned() } /// prefixes the typename according to its kind (UObject, AActor, FStruct, EEnum) fn get_prefixed_name(ty: &UnrealType) -> String { match ty { UnrealType::Class(_) => { format!("U{}", canonicalize_name(&ty.unique_name())) } UnrealType::Struct(_) => { format!("F{}", canonicalize_name(&ty.unique_name())) } UnrealType::Actor(_) => { format!("A{}", canonicalize_name(&ty.unique_name())) } UnrealType::Enum(_) => { format!("E{}", canonicalize_name(&ty.unique_name())) } } } pub fn build(self, args: &super::Build) -> anyhow::Result> { let pkgs = if let Some(packages) = &args.packages { let deps = self.dependencies_for_package_names(packages); log::debug!("all dependencies: {deps:?}"); deps.iter() .map(|id| self.sdk.packages.get(id).unwrap()) .collect::>() } else { self.sdk.packages.values().collect::>() }; let packages = pkgs .into_iter() .map(|pkg| { let name = canonicalize_name(&pkg.name).to_string(); let tokens = self.generate_package(pkg, args.feature_gate)?; anyhow::Ok((name, tokens)) }) .collect::, _>>()?; Ok(packages) } fn get_package_by_name(&self, name: &str) -> Option { self.sdk .packages .iter() .find(|(_, pkg)| &pkg.name == name) .map(|(id, _)| *id) } fn dependencies_for_package_names(&self, names: &Vec) -> BTreeSet { names .iter() .filter_map(|name| self.get_package_by_name(name)) .flat_map(|id| self.dependencies(self.sdk.packages.get(&id).unwrap())) .collect::>() } fn dependencies(&self, pkg: &ProcessedPackage) -> BTreeSet { let mut set = BTreeSet::new(); self.dependencies_inner(pkg, &mut set); set } fn dependencies_inner(&self, pkg: &ProcessedPackage, pkgs: &mut BTreeSet) { pkgs.insert(pkg.package_object); // depth first, does that matter? for id in pkg.dependencies.iter() { if !pkgs.contains(id) { if let Some(pkg) = self.sdk.packages.get(id) { self.dependencies_inner(pkg, pkgs); } } } } pub fn build_in_dir>( self, path: P, args: &super::Build, ) -> anyhow::Result<()> { let packages = self.build(args)?; let path = path.as_ref(); std::fs::create_dir_all(&path)?; let mut mod_rs = TokenStream::new(); for (name, tokens) in packages { let name = format_ident!("{name}"); if args.single_file { mod_rs.extend(quote! { pub mod #name { #tokens } }); } else { std::fs::write(path.join(format!("{name}.rs")), tokens.to_string())?; mod_rs.extend(quote! { pub mod #name; }); } } std::fs::write(path.join("mod.rs"), mod_rs.to_string())?; Ok(()) } fn type_name(&self, ty: &Type) -> anyhow::Result { let type_name = match ty { Type::Ptr(inner) | Type::Ref(inner) => { format!( "::core::option::Option>", self.type_name(&inner)? ) } Type::WeakPtr(inner) => { format!( "crate::engine::TWeakObjectPtr<{}>", self.get_type_path(inner) .context("type name was not cached.")? ) } Type::SoftPtr(inner) => { format!( "crate::engine::TSoftObjectPtr<{}>", self.get_type_path(inner) .context("type name was not cached.")? ) } Type::LazyPtr(inner) => { format!( "crate::engine::TLazyObjectPtr<{}>", self.get_type_path(inner) .context("type name was not cached.")? ) } Type::AssetPtr(inner) => format!( "crate::engine::TAssetPtr<{}>", self.get_type_path(inner) .context("type name was not cached.")? ), Type::Array(inner) => { format!("crate::engine::TArray<{}>", self.type_name(&inner)?) } Type::Primitive(prim) => { format!("{prim}") } Type::RawArray { ty, len } => { format!("[{}; {}]", self.type_name(&ty)?, len) } Type::Name => "crate::engine::FName".to_string(), Type::String => "crate::engine::FString".to_string(), Type::Text => "crate::engine::FText".to_string(), Type::Enum { enum_type, .. } => self .get_type_path(enum_type) .context("type name was not cached.")?, Type::Class(class) => { format!( "::core::option::Option<{}>", self.get_type_path(class) .context("type name was not cached.")? ) } Type::Struct(class) => self .get_type_path(class) .context("type name was not cached.")? .clone(), }; Ok(type_name) } fn generate_enum(&self, enum0: &Enum) -> anyhow::Result { let name = self .get_type_name(&enum0.obj_ref) .context("enum name was not previously canonicalized and cached.")?; let variants = enum0.values.iter().map(|(&value, name)| { let name = canonicalize_name(&name); quote! { #name = #value, } }); let tokens = quote! { #[repr(u8)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] pub enum #name { #(#variants)* } }; Ok(tokens) } /// returns a tuple of: /// - the type definition of the type as a TokenStream /// - the impls for that type, like Clone, AsUObject, AsPtr and StaticClass fn generate_object( &self, _class: &Class, name: &str, ) -> anyhow::Result<(TokenStream, TokenStream)> { let ident = format_ident!("{name}"); let typedef = quote! { #[derive(Eq, PartialEq, Copy, Clone)] pub struct #ident(pub ::core::ptr::NonNull); }; let static_class_impl: TokenStream = Self::generate_find_object(name); let impls = quote! { impl crate::engine::AsUObject for #ident { fn as_uobject(&self) -> crate::engine::UObject { crate::engine::UObject(self.0) } fn from_uobject(obj: &crate::engine::UObject) -> Self { Self(obj.0) } } impl crate::engine::AsPtr for #ident { fn as_ptr(&self) -> *const u8 { unsafe { self.0.as_ref().get() as _ } } fn as_mut_ptr(&self) -> *mut u8 { unsafe { self.0.as_ref().get() as _ } } } impl crate::engine::StaticClass for #ident { fn get_static_class() -> ::core::option::Option { let class: ::core::option::Option = #static_class_impl; class } } }; Ok((typedef, impls)) } /// returns a tuple of: /// - the type definition of the type as a TokenStream /// - the impls for that type, like Clone, AsUObject, AsPtr and StaticClass fn generate_struct( &self, class: &Class, name: &str, ctor: Option, ) -> anyhow::Result<(TokenStream, TokenStream)> { let size = class.size; let ident = format_ident!("{name}"); let typedef = quote! { pub struct #ident(pub ::core::cell::UnsafeCell); }; let impls = quote! { impl Eq for #ident {} impl PartialEq for #ident { fn eq(&self, other: &Self) -> bool { unsafe {(&*self.0.get()).eq(&*other.0.get())} } } impl Clone for #ident { fn clone(&self) -> Self { Self(::core::cell::UnsafeCell::new(unsafe {&*self.0.get()}.clone())) } } impl crate::engine::AsPtr for #ident { fn as_ptr(&self) -> *const u8 { self.0.get().cast() } fn as_mut_ptr(&self) -> *mut u8 { self.0.get().cast() } } impl #ident { pub fn zeroed() -> Self { unsafe { ::core::mem::MaybeUninit::::zeroed().assume_init() } } #ctor } }; Ok((typedef, impls)) } /// returns a tuple of: /// - all of the params struct definitions /// - all of the methods. fn generate_struct_methods( &self, class: &Class, name: &str, ) -> anyhow::Result<(Vec, Vec)> { let methods = class .methods .iter() .map(|method| self.generate_method(name, method)) .collect::, _>>()?; let (params, methods) = methods.into_iter().unzip::<_, _, Vec<_>, Vec<_>>(); Ok((params, methods)) } /// returns a tuple of: /// - the definition of the params struct /// - the method wrapper. fn generate_method( &self, struct_name: &str, method: &ClassMethod, ) -> anyhow::Result<(TokenStream, TokenStream)> { let method_name = canonicalize_name(&method.unique_name()); // all parameters collected as (parameter, canonicalized_name, type_ident) let parameters = method .parameters .iter() .map(|parameter| { let name = canonicalize_name(¶meter.unique_name()); let type_name = self.type_name(¶meter.ty)?; anyhow::Ok((parameter, name, type_name)) }) .collect::, _>>()?; // all parameters converted into "arg: Type" format of tokens. let all_params = parameters .iter() .map(|(param, name, ty)| { let name = format_ident!("{name}"); let ty = format_ident!("{ty}"); (param, quote! {#name: #ty}) }) .collect::>(); // params that the function will accept as arguments. let params = all_params .iter() .filter(|(param, _)| { param.is_param() || (!param.is_return_param() && param.is_const_param()) }) .map(|(_, tokens)| tokens.clone()); // tokens of all params, for the Params struct definition. let all_params = all_params.iter().map(|(_, tokens)| tokens.clone()); // param token streams for setting the fields of the params struct // with the arguments of the function. let init_params = parameters.iter().map(|(_, name, _)| { let name = format_ident!("{name}"); quote! {params.#name = #name;} }); let (return_type, return_expression) = { let (names, types) = parameters .iter() .filter(|(param, _, _)| { param.is_return_param() || (param.is_out_param() && !param.is_const_param()) }) .map(|(_, name, ty)| { let name = format_ident!("{name}"); let ty = format_ident!("{ty}"); ( quote! { #name }, quote! { #ty }, ) }) .unzip::<_, _, Vec<_>, Vec<_>>(); ( quote! { (#(#types),*) }, quote! { (#(params.#names),*) }, ) }; let find_function = Self::generate_find_object(&method.full_name); let params_type = format_ident!("{struct_name}{method_name}Params"); let params_def = quote! { #[repr(C)] #[derive(Debug)] pub struct #params_type { #(pub #all_params),* } }; let method_def = quote! { fn #method_name(&self, #(#params),*) -> #return_type { let mut func: UFunction = {#find_function}.expect("function '#full_name' not found."); let mut params = #params_type::zeroed(); #(#init_params )* let flags = *func.function_flags(); process_event(self.as_uobject(), func, &mut params); func.set_function_flags(flags); #return_expression } }; Ok((params_def, method_def)) } /// generates getter, setter and optionally mut_getter for the field, handles bitset booleans. fn generate_field_accessors( &self, field: &ClassField, field_name: &Cow, type_name: &String, ) -> TokenStream { let setter = format_ident!("set_{}", field_name); let getter = format_ident!("get_{}", field_name); let mut_getter = format_ident!("mut_{}", field_name); let offset = field.offset; let (getter, setter, mut_getter) = match field.ty { Type::Primitive(PrimitiveType::Bool { byte_mask, field_mask, .. }) if byte_mask != 0xFF => { let shift = field_mask.trailing_zeros(); let getter = quote! { fn #getter(&self) -> bool { unsafe { *self.as_ptr().offset(#offset) & (1u8 << #shift) != 0 } } }; let setter = quote! { fn #setter(&mut self, #field_name: bool) -> () { unsafe { if #field_name { *self.as_mut_ptr().offset(#offset) |= (#field_name as u8) << shift; } else { *self.as_mut_ptr().offset(#offset) &= !((#field_name as u8) << shift); } } } }; (getter, setter, None) } _ => { let getter = quote! { fn #getter(&self) -> &#type_name { unsafe {&*self.as_ptr().offset(#offset).cast()} } }; let setter = quote! { fn #setter(&mut self, #field_name: #type_name) { *unsafe {&*self.as_ptr().offset(#offset).cast()} = #field_name; } }; let mut_getter = quote! { fn #mut_getter(&mut self) -> &mut #type_name { unsafe {&mut *self.as_mut_ptr().offset(#offset).cast()} } }; (getter, setter, Some(mut_getter)) } }; quote! { #getter #setter #mut_getter } } fn generate_struct_ctor( &self, _class: &Class, type_name: &str, fields: &Vec<(&ClassField, Cow, String)>, ) -> TokenStream { let fields_defs = fields.iter().map(|(_, name, ty)| quote! {#name: #ty}); let this_field_asignments = fields.iter().map(|(_, name, _ty)| { let setter = format_ident!("set_{}", name); let field_trait = format_ident!("{type_name}Fields"); quote! {::#setter(this, #name);} }); // FIXME: handle super struct fields aswell, ARK doesnt seem to have those anyways. // export getting fields into a seperate function, this function will be fallible then. // it is a lot of work for nothing currently. quote! { pub fn new(#(#fields_defs),*) -> Self { let mut this = Self::zeroed(); #(#this_field_asignments)* this } } } /// returns a tokenstream with the accessor trait definition and implementation, /// as well as optionally a constructor for UScriptStructs fn generate_struct_fields( &self, class: &Class, name: &str, ) -> anyhow::Result<(TokenStream, Option)> { let fields = class .fields .iter() .map(|field| { let name = canonicalize_name(&field.unique_name()); let ty = self.type_name(&field.ty)?; anyhow::Ok((field, name, ty)) }) .collect::, _>>()?; let ctor = if class.kind == StructKind::Struct { Some(self.generate_struct_ctor(class, name, &fields)) } else { None }; let field_accessors = fields .iter() .map(|(field, name, ty)| self.generate_field_accessors(field, name, ty)); let fields_trait = format_ident!("{name}Fields"); let fields_trait = quote! { pub trait #fields_trait: AsPtr { #(#field_accessors)* } impl #fields_trait for #name {} }; Ok((fields_trait, ctor)) } fn generate_find_object(name: &str) -> TokenStream { let not_found = format!("static object \"{name}\" not found!"); quote! { static OBJECT: ::once_cell::sync::OnceCell<::core::option::Option> = ::once_cell::sync::OnceCell::new(); OBJECT.get_or_init(|| { match find_object(::obfstr::obfstr!(#name)) { object @ Some(_) => {object}, None => { ::log::error!("{}", ::obfstr::obfstr!(#not_found)); } } }) .map(|object| unsafe {object.cast()}) } } fn iter_super_types(&self, class: &Class) -> impl Iterator { let super_traits = core::iter::from_fn({ let mut sup = class.super_class; move || { if let Some(key) = sup { let next = self.sdk.get_object(&key); sup = next.and_then(|next| next.super_class()); next } else { None } } }); super_traits } fn generate_class(&self, class: &Class) -> anyhow::Result { let name = &self .get_type_name(&class.obj_ref) .context("enum name was not previously canonicalized and cached.")?; let (field_trait, ctor) = self.generate_struct_fields(class, name)?; let (typedef, impls) = match class.kind { StructKind::Object | StructKind::Actor => self.generate_object(class, name)?, StructKind::Struct => self.generate_struct(class, name, ctor)?, }; let (method_params, methods) = self.generate_struct_methods(class, &name)?; let method_trait = format_ident!("{name}Methods"); let methods = quote! { #(#method_params)* pub trait #method_trait { #(#methods)* } impl #method_trait for #name {} }; let super_traits = self .iter_super_types(class) // SAFETY: we already got this type by its obj_ref, so it must be there. .map(|ty| { ( self.get_type_package_path(&ty.obj_ref()).unwrap(), self.get_type_name(&ty.obj_ref()).unwrap(), ) }) .map(|(super_path, super_name)| { let fields = format_ident!("{super_name}Fields"); let methods = format_ident!("{super_name}Methods"); quote! { impl #super_path::#fields for #name {} impl #super_path::#methods for #name {} } }); let tokens = quote! { #[repr(transparent)] #[derive(Debug)] #typedef unsafe impl Send for #name {} unsafe impl Sync for #name {} #impls #(#super_traits)* #methods #field_trait }; Ok(tokens) } fn generate_package( &self, pkg: &ProcessedPackage, feature_gate: bool, ) -> anyhow::Result { let pkg_name = canonicalize_name(&pkg.name); log::info!( "generating package \"{pkg_name}\" with {} types..", pkg.types.len() ); let mut pkg_tokens = TokenStream::new(); for (_id, ty) in &pkg.types { let tokens = match ty { UnrealType::Class(class) | UnrealType::Actor(class) | UnrealType::Struct(class) => self.generate_class(class)?, UnrealType::Enum(enum0) => self.generate_enum(enum0)?, }; pkg_tokens.extend(tokens); } let deps = pkg .dependencies .iter() .filter_map(|id| self.sdk.packages.get(id)) .map(|package| format!("`{}`", package.name)) .join(","); let doc_msg = format!("Package `{pkg_name}` depends on the features {deps}."); let feature_gate = if feature_gate { Some(quote! { #![doc = #doc_msg] #![cfg(feature = "#pkg_name")] }) } else { None }; Ok(quote! { pub mod #pkg_name { #feature_gate #![allow(dead_code, unused_imports, non_snake_case, non_camel_case_types)] #pkg_tokens } }) } } }