use anyhow::Context; use anyhow::Result; use genco::fmt; use genco::prelude::*; use std::ffi::OsStr; use std::fs::File; use std::path::Path; pub fn generate_csharp(path: &str, base_folder: &str) -> Result<()> { clear_contents(base_folder, "cs")?; let contents = std::fs::read_to_string(path)?; let ast = syn::parse_file(&contents)?; let custom_comps = generate_components_csharp(ast.clone(), base_folder)?; let custom_states = generate_states_csharp(ast.clone(), base_folder)?; generate_prefabs_csharp(ast.clone(), base_folder)?; generate_hooks(base_folder, custom_comps, custom_states)?; Ok(()) } fn generate_hooks( base_folder: &str, custom_comps: csharp::Tokens, custom_states: csharp::Tokens, ) -> Result<()> { let runtime_initialize = &csharp::import("UnityEngine", "RuntimeInitializeOnLoadMethod"); let runtime_initialize_load = &csharp::import("UnityEngine", "RuntimeInitializeLoadType"); let unrust_native = &csharp::import("unrust.runtime", "NativeWrapper"); let entity_manager = &csharp::import("Unity.Entities", "EntityManager"); let entity = &csharp::import("Unity.Entities", "Entity"); let create_callback = &csharp::import("unrust.runtime", "CustomCreateCallback"); let hooks: csharp::Tokens = quote! { namespace unrust.userland { public static class UnrustHooks { [$runtime_initialize($runtime_initialize_load.BeforeSceneLoad)] static void Initialize() { $unrust_native.CustomCreates = CreateCallback; } public static unsafe ulong CreateCallback($entity_manager manager, $entity entity, $create_callback cb) { var count = 0; var arr = new CustomData[CustomComponents.ComponentCount]; $(custom_comps) var stateCount = 0; var stateArr = new CustomState[CustomState.CustomStateCount]; $(custom_states) fixed (void* ptr = arr) { fixed (void* state = stateArr) { return cb(ptr, (nuint)count, state, (nuint)stateCount); } } } } } }; write_tokens_to_file(base_folder, "UnrustHooks.cs", hooks) } fn generate_prefabs_csharp(ast: syn::File, base_folder: &str) -> Result<()> { let monobehaviour = &csharp::import("UnityEngine", "MonoBehaviour"); let baker = &csharp::import("Unity.Entities", "Baker"); let spawnable = &csharp::import("unrust.runtime", "UnrustSpawnable"); let enums = ast .items .iter() .filter_map(|item| match item { syn::Item::Enum(s) => Some(s), _ => None, }) .filter(|item| { item.attrs .iter() .any(|attr| attr.path().is_ident("unity_prefab")) }) .map(|item| { ( item.ident.to_string(), item.variants.iter().map(|v| v.ident.to_string()), ) }) .enumerate() .map(|(index, (enum_name, variants))| { let variant_fields = variants.clone().map(|name| { quote! { $['\r']public GameObject $(&name); } }); let count = index; let author_fields = variants.map(|name| { quote! { buffer.Add(GetEntity(authoring.$(&name), TransformUsageFlags.Dynamic)); } }); let comp: csharp::Tokens = quote! { namespace unrust.userland { public class $(&enum_name)Authoring : $monobehaviour { $(for n in variant_fields => $n) public const int RESOURCE_ID = $count; class Baker : $baker<$(&enum_name)Authoring> { public override void Bake($(&enum_name)Authoring authoring) { var containerEntity = GetEntity(TransformUsageFlags.None); var buffer = AddBuffer<$spawnable>(containerEntity).Reinterpret(); $(for n in author_fields => $n) AddComponent(containerEntity, new UnrustResourceID { Value = $count}); } } } } }; (enum_name, comp) }); enums.clone().try_for_each(|(name, comp)| { write_tokens_to_file(base_folder, &format!("{}Authoring.cs", name), comp) })?; Ok(()) } fn generate_states_csharp(ast: syn::File, base_folder: &str) -> Result { let struct_layout = &csharp::import("System.Runtime.InteropServices", "StructLayout"); let layout_kind = &csharp::import("System.Runtime.InteropServices", "LayoutKind"); let monobehaviour = &csharp::import("UnityEngine", "MonoBehaviour"); let component_data = &csharp::import("Unity.Entities", "IComponentData"); let enums = ast .items .iter() .filter_map(|item| match item { syn::Item::Enum(s) => Some(s), _ => None, }) .filter(|item| { item.attrs .iter() .any(|attr| attr.path().is_ident("bevy_state")) }) .map(|item| (item.ident.to_string(), &item.variants)) .map(|(enum_name, enum_variants)| { let enum_fields = enum_variants.iter(); let name_list = enum_fields.clone().enumerate().map(|(index, v)| { quote! { $['\r']$(v.ident.to_string()) = $index, } }); let comp: csharp::Tokens = quote! { namespace unrust.userland { [$(struct_layout)($layout_kind.Sequential)] public struct $(&enum_name) : $component_data { public sbyte Value; } public class $(&enum_name)Authoring : $monobehaviour { public enum ENUM_$(&enum_name) : sbyte { $(for n in name_list => $n) } [SerializeField] public ENUM_$(&enum_name) $(&enum_name); class Baker : Baker<$(&enum_name)Authoring> { public override void Bake($(&enum_name)Authoring authoring) { var entity = GetEntity(TransformUsageFlags.None); AddComponent(entity, new $(&enum_name) { Value = (sbyte)authoring.$(&enum_name) }); } } } } }; (comp, enum_name) }); enums.clone().try_for_each(|(comp, enum_name)| { write_tokens_to_file(base_folder, &format!("{}Authoring.cs", enum_name), comp) })?; let enum_types = enums.clone().enumerate().map(|(index, (_, name))| { quote! { $(&name) = $index } }); let count = enums.clone().count(); let generated_comps: csharp::Tokens = quote! { namespace unrust.userland { [$struct_layout($layout_kind.Sequential)] public struct CustomState { public const int CustomStateCount = $count; public CustomStateType ty; public sbyte value; } public enum CustomStateType: byte { $(for n in enum_types => $n) } } }; write_tokens_to_file(base_folder, "UnrustState.cs", generated_comps)?; let add_enums = enums.clone().map(|(_, name)| { quote! { if (manager.HasComponent<$(&name)>(entity)) { stateArr[stateCount] = new CustomState { ty = CustomStateType.$(&name), value = manager.GetComponentData<$(&name)>(entity).Value, }; stateCount++; } } }); Ok(quote! { $(for n in add_enums => $n) }) } fn write_tokens_to_file(base: &str, name: &str, tokens: csharp::Tokens) -> Result<()> { let fmt = fmt::Config::from_lang::().with_indentation(fmt::Indentation::Space(4)); let config = csharp::Config::default(); let path = Path::new(base); let file = File::create(path.join(name)).context("failed to open file")?; let mut w = fmt::IoWriter::new(file); tokens .format_file(&mut w.as_formatter(&fmt), &config) .context("could not write to file") } fn generate_components_csharp(ast: syn::File, base_folder: &str) -> Result { let structs = find_structs_with_attr(ast, "unity_authoring") .into_iter() .map(generate_components_with_authoring); let fmt = fmt::Config::from_lang::().with_indentation(fmt::Indentation::Space(4)); let config = csharp::Config::default(); let path = Path::new(base_folder); structs.clone().try_for_each(|(comp, name)| { write_tokens_to_file(base_folder, &format!("{}Authoring.cs", name), comp) })?; let count = structs.clone().count(); let gen_comps = structs.clone().map(|(_, name)| { quote! { $['\r'][FieldOffset(0)] public $(&name) $(&name); } }); let enum_types = structs.clone().enumerate().map(|(index, (_, name))| { quote! { $['\r']$name = $index, } }); let struct_layout = &csharp::import("System.Runtime.InteropServices", "StructLayout"); let layout_kind = &csharp::import("System.Runtime.InteropServices", "LayoutKind"); let generated_comps: csharp::Tokens = quote! { namespace unrust.userland { [$struct_layout($layout_kind.Sequential)] public struct CustomData { public CustomType ty; public CustomComponents value; } public enum CustomType : byte { $(for n in enum_types => $n) } [$struct_layout($layout_kind.Explicit)] public struct CustomComponents { public const int ComponentCount = $count; $(for n in gen_comps => $n) } } }; let file = File::create(path.join("UnrustComponent.cs")).context("failed to open file")?; let mut w = fmt::IoWriter::new(file); generated_comps .format_file(&mut w.as_formatter(&fmt), &config) .context("could not write to file")?; let add_comps = structs.clone().map(|(_, name)| { quote! { if (manager.HasComponent<$(&name)>(entity)) { arr[count] = new CustomData { ty = CustomType.$(&name), value = new CustomComponents { $(&name) = manager.GetComponentData<$(&name)>(entity) }, }; count++; } } }); let add_comps: csharp::Tokens = quote! { $(for n in add_comps => $n) }; Ok(add_comps) } fn generate_components_with_authoring( (struct_name, fields): (String, Vec<(String, syn::Type)>), ) -> (csharp::Tokens, String) { let component_data = &csharp::import("Unity.Entities", "IComponentData"); let monobehaviour = &csharp::import("UnityEngine", "MonoBehaviour"); let struct_layout = &csharp::import("System.Runtime.InteropServices", "StructLayout"); let layout_kind = &csharp::import("System.Runtime.InteropServices", "LayoutKind"); let component_fields = fields.iter().map(|(field_name, field_type)| { let field_type = map_rust_type(field_type); quote! { $['\r']public $field_type $field_name; } }); let authoring_name = format!("{struct_name}Authoring"); let authoring_fields = fields.iter().map(|(field_name, _)| { quote! { $['\r']$field_name = authoring.$field_name, } }); let tokens: csharp::Tokens = quote! { namespace unrust.userland { [$struct_layout($layout_kind.Sequential)] public struct $(&struct_name) : $component_data { $(for n in component_fields.clone() => $n) } public class $(&authoring_name) : $monobehaviour { $(for n in component_fields => $n) class Baker : Baker<$(&authoring_name)> { public override void Bake($(&authoring_name) authoring) { var entity = GetEntity(TransformUsageFlags.Dynamic); AddComponent(entity, new $(&struct_name) { $(for n in authoring_fields => $n) }); } } } } }; (tokens, struct_name) } fn find_structs_with_attr( ast: syn::File, expected: &str, ) -> Vec<(String, Vec<(String, syn::Type)>)> { ast.items .iter() .filter_map(|item| match item { syn::Item::Struct(s) => Some(s), _ => None, }) .filter(|item| item.attrs.iter().any(|attr| attr.path().is_ident(expected))) .map(|item| { let fields = match &item.fields { syn::Fields::Named(fields) => fields .named .iter() .map(|f| { let field_name = f.ident.clone().unwrap().to_string(); let field_type = f.ty.clone(); (field_name, field_type) }) .collect::>(), _ => vec![], }; (item.ident.to_string(), fields) }) .collect() } fn clear_contents(path: &str, extension: &str) -> Result<()> { let files = std::fs::read_dir(path)?; files .filter_map(|p| p.ok()) .filter_map(|p| { let path = p.path(); let Some(ext) = path.extension() else { return None; }; if ext == OsStr::new(extension) { Some(p.path()) } else { None } }) .try_for_each(std::fs::remove_file)?; Ok(()) } fn map_rust_type(ty: &syn::Type) -> String { let syn::Type::Path(path) = ty else { panic!("expected rust path type"); }; let path = path .path .get_ident() .expect("expected simple path type") .to_string(); match path.as_str() { "f32" => "float", "f64" => "double", "i32" => "int", "i64" => "long", "u32" => "uint", "u64" => "ulong", _ => panic!("unsupported base type"), } .to_string() }