unreal-sdk/sdk-builder/src/main.rs

975 lines
33 KiB
Rust

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<Commands>,
}
#[derive(Args)]
pub struct Build {
#[arg(short, long)]
in_archive: PathBuf,
/// directory into which the sdk will be dumped.
#[arg(short, long)]
out: Option<PathBuf>,
#[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<Vec<String>>,
}
#[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<ObjectRef, String>,
}
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<ObjectRef, String>,
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::<BTreeMap<_, _>>();
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<String> {
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<String> {
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<String> {
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<BTreeMap<String, TokenStream>> {
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::<Vec<_>>()
} else {
self.sdk.packages.values().collect::<Vec<_>>()
};
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::<Result<BTreeMap<_, _>, _>>()?;
Ok(packages)
}
fn get_package_by_name(&self, name: &str) -> Option<PackageRef> {
self.sdk
.packages
.iter()
.find(|(_, pkg)| &pkg.name == name)
.map(|(id, _)| *id)
}
fn dependencies_for_package_names(&self, names: &Vec<String>) -> BTreeSet<PackageRef> {
names
.iter()
.filter_map(|name| self.get_package_by_name(name))
.flat_map(|id| self.dependencies(self.sdk.packages.get(&id).unwrap()))
.collect::<BTreeSet<_>>()
}
fn dependencies(&self, pkg: &ProcessedPackage) -> BTreeSet<PackageRef> {
let mut set = BTreeSet::new();
self.dependencies_inner(pkg, &mut set);
set
}
fn dependencies_inner(&self, pkg: &ProcessedPackage, pkgs: &mut BTreeSet<PackageRef>) {
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<P: AsRef<Path>>(
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<String> {
let type_name = match ty {
Type::Ptr(inner) | Type::Ref(inner) => {
format!(
"::core::option::Option<NonNull<{}>>",
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<TokenStream> {
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<u8>);
};
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<crate::engine::UClass> {
let class: ::core::option::Option<crate::engine::UClass> =
#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<TokenStream>,
) -> anyhow::Result<(TokenStream, TokenStream)> {
let size = class.size;
let ident = format_ident!("{name}");
let typedef = quote! {
pub struct #ident(pub ::core::cell::UnsafeCell<u8; #size>);
};
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::<Self>::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<TokenStream>, Vec<TokenStream>)> {
let methods = class
.methods
.iter()
.map(|method| self.generate_method(name, method))
.collect::<Result<Vec<_>, _>>()?;
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(&parameter.unique_name());
let type_name = self.type_name(&parameter.ty)?;
anyhow::Ok((parameter, name, type_name))
})
.collect::<Result<Vec<_>, _>>()?;
// 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::<Vec<_>>();
// 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<str>,
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<str>, 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! {<Self as #field_trait>::#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<TokenStream>)> {
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::<Result<Vec<_>, _>>()?;
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<UObject>> = ::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<Item = &UnrealType> {
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<TokenStream> {
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<TokenStream> {
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
}
})
}
}
}