1pub mod schema;
4pub mod naming;
5pub mod config;
6pub mod context;
7pub mod type_map;
8pub mod defaults;
9pub mod filter;
10pub mod rust_gen;
11pub mod cpp_gen;
12
13use std::path::Path;
14
15use crate::config::{
16 CodegenConfig, LibraryKind, LibraryOutput, PluginConfig, PluginLayout, ProjectConfig,
17 ProjectLayout,
18};
19use crate::schema::{ClassesFile, EnumsFile, StructsFile};
20
21pub(crate) fn write_if_changed(path: &Path, contents: &str) -> std::io::Result<()> {
25 if std::fs::read(path).is_ok_and(|old| old == contents.as_bytes()) {
26 return Ok(());
27 }
28 std::fs::write(path, contents)
29}
30
31pub fn run_generate(project_root: &Path) {
36 let config = ProjectConfig::load(project_root).unwrap_or_else(|e| {
37 eprintln!("rusteal-codegen: {e}");
38 std::process::exit(1);
39 });
40 let layout = ProjectLayout::new(project_root);
41 generate_library(&layout.uht_json(), &config.codegen, &layout.game_output());
42}
43
44pub fn run_generate_plugin(project_root: &Path, plugin: &PluginLayout) {
48 let config = PluginConfig::load(&plugin.dir).unwrap_or_else(|e| {
49 eprintln!("rusteal-codegen: {e}");
50 std::process::exit(1);
51 });
52 let layout = ProjectLayout::new(project_root);
53 eprintln!("rusteal-codegen: plugin {}", plugin.name);
54 generate_library(&layout.uht_json(), &config.codegen, &plugin.output(config.plugin.kind));
55}
56
57pub fn generate_library(uht_input: &Path, codegen: &CodegenConfig, output: &LibraryOutput) {
60 let codegen = &codegen.for_library(output.kind);
61 let classes_path = uht_input.join("rusteal_classes.json");
62 let structs_path = uht_input.join("rusteal_structs.json");
63 let enums_path = uht_input.join("rusteal_enums.json");
64 let rust_out = output.bindings_crate.clone();
66 let rust_src = rust_out.join("src");
67 let cpp_out = output.cpp_generated.clone();
68
69 eprintln!("rusteal-codegen: loading JSON...");
70
71 let classes_json: ClassesFile = {
73 let data = std::fs::read_to_string(&classes_path)
74 .unwrap_or_else(|e| panic!("Failed to read {}: {e}", classes_path.display()));
75 serde_json::from_str(&data)
76 .unwrap_or_else(|e| panic!("Failed to parse {}: {e}", classes_path.display()))
77 };
78
79 let structs_json: StructsFile = {
80 let data = std::fs::read_to_string(&structs_path)
81 .unwrap_or_else(|e| panic!("Failed to read {}: {e}", structs_path.display()));
82 serde_json::from_str(&data)
83 .unwrap_or_else(|e| panic!("Failed to parse {}: {e}", structs_path.display()))
84 };
85
86 let enums_json: EnumsFile = {
87 let data = std::fs::read_to_string(&enums_path)
88 .unwrap_or_else(|e| panic!("Failed to read {}: {e}", enums_path.display()));
89 serde_json::from_str(&data)
90 .unwrap_or_else(|e| panic!("Failed to parse {}: {e}", enums_path.display()))
91 };
92
93 eprintln!(
94 " Loaded {} classes, {} structs, {} enums",
95 classes_json.classes.len(),
96 structs_json.structs.len(),
97 enums_json.enums.len()
98 );
99
100 let mut ctx = context::CodegenContext::new(
102 classes_json.classes,
103 structs_json.structs,
104 enums_json.enums,
105 codegen,
106 );
107 ctx.cpp_prefix = output.cpp_prefix.clone();
108
109 eprintln!(
110 " Enabled modules: {:?}",
111 ctx.enabled_modules.iter().collect::<Vec<_>>()
112 );
113 for (module, classes) in &ctx.module_classes {
114 eprintln!(" {}: {} classes", module, classes.len());
115 }
116 for (module, structs) in &ctx.module_structs {
117 eprintln!(" {}: {} structs", module, structs.len());
118 }
119 for (module, enums) in &ctx.module_enums {
120 eprintln!(" {}: {} enums", module, enums.len());
121 }
122
123 eprintln!("rusteal-codegen: filtering...");
125 let blocklist = with_unlisted_classes(&ctx, codegen);
126 filter::apply_filters(&mut ctx, &blocklist, output.kind == LibraryKind::Editor);
127
128 eprintln!("rusteal-codegen: building function table...");
130 build_func_table(&mut ctx);
131 eprintln!(" {} functions in func_table", ctx.func_table.len());
132
133 eprintln!("rusteal-codegen: generating Rust code...");
135 rust_gen::generate(&ctx, &rust_src);
136 rust_gen::manual::write_manual_module(&rust_src);
137 rust_gen::prelude::write_prelude(&rust_src, &ctx);
138 rust_gen::cargo_toml::write_crate_files(&rust_out, &ctx, codegen);
139
140 eprintln!("rusteal-codegen: generating C++ code...");
142 cpp_gen::generate(&ctx, &cpp_out);
143
144 generate_module_deps(codegen, &output.host_module, &cpp_out);
147 update_plugin_dependencies(codegen, &output.plugin_descriptor);
148
149 eprintln!("rusteal-codegen: verifying output...");
151 verify_output(&ctx, &rust_src, &cpp_out);
152
153 eprintln!("rusteal-codegen: done!");
154}
155
156fn with_unlisted_classes(ctx: &context::CodegenContext, codegen: &CodegenConfig) -> config::Blocklist {
160 use std::collections::HashSet;
161
162 let mut blocklist = codegen.blocklist.clone();
163 for (package, mapping) in &codegen.modules {
164 let Some(listed) = &mapping.classes else {
165 continue;
166 };
167 let mut wanted: HashSet<&str> = HashSet::new();
168 for name in listed {
169 let mut current = Some(name.as_str());
170 while let Some(class) = current.and_then(|n| ctx.classes.get(n)) {
171 if &class.package != package || !wanted.insert(class.name.as_str()) {
172 break;
173 }
174 current = class.super_class.as_deref();
175 }
176 }
177 for class in ctx.module_classes.get(&mapping.module).into_iter().flatten() {
178 if !wanted.contains(class.name.as_str()) {
179 blocklist.classes.push(class.name.clone());
180 }
181 }
182 }
183 blocklist
184}
185
186fn generate_module_deps(config: &CodegenConfig, host_module: &str, cpp_out: &Path) {
189 use std::collections::BTreeSet;
190
191 let enabled_features: std::collections::HashSet<&str> =
192 config.features.iter().map(|s| s.as_str()).collect();
193
194 let mut ue_modules: BTreeSet<&str> = BTreeSet::new();
197 ue_modules.insert("Core");
198 for (pkg, mapping) in &config.modules {
199 if enabled_features.contains(mapping.feature.as_str()) {
200 ue_modules.insert(pkg.as_str());
201 }
202 }
203 ue_modules.remove(host_module);
204
205 let content = ue_modules.iter().copied().collect::<Vec<_>>().join("\n");
206
207 let path = cpp_out.join("module_deps.txt");
208 write_if_changed(&path, &content)
209 .unwrap_or_else(|e| panic!("Failed to write {}: {e}", path.display()));
210
211 eprintln!(" module_deps.txt: {:?}", ue_modules.iter().collect::<Vec<_>>());
212}
213
214fn update_plugin_dependencies(config: &CodegenConfig, descriptor: &Path) {
218 let Ok(text) = std::fs::read_to_string(descriptor) else {
219 return; };
221 let mut json: serde_json::Value = serde_json::from_str(&text)
222 .unwrap_or_else(|e| panic!("Failed to parse {}: {e}", descriptor.display()));
223 let enabled: std::collections::HashSet<&str> =
224 config.features.iter().map(|s| s.as_str()).collect();
225 let mut wanted: Vec<&str> = config
226 .modules
227 .values()
228 .filter(|m| enabled.contains(m.feature.as_str()))
229 .filter_map(|m| m.plugin.as_deref())
230 .collect();
231 wanted.sort();
232 wanted.dedup();
233
234 let plugins = json
235 .as_object_mut()
236 .expect("a plugin descriptor is a JSON object")
237 .entry("Plugins")
238 .or_insert_with(|| serde_json::Value::Array(Vec::new()))
239 .as_array_mut()
240 .expect("a plugin descriptor's Plugins is an array");
241 let mut added = Vec::new();
242 for name in wanted {
243 if plugins.iter().any(|p| p["Name"] == name) {
244 continue;
245 }
246 plugins.push(serde_json::json!({ "Name": name, "Enabled": true }));
247 added.push(name);
248 }
249 if added.is_empty() {
250 return;
251 }
252 let mut out = serde_json::to_string_pretty(&json).expect("serializable descriptor");
253 out.push('\n');
254 std::fs::write(descriptor, out)
255 .unwrap_or_else(|e| panic!("Failed to write {}: {e}", descriptor.display()));
256 let name = descriptor.file_name().unwrap_or_default().to_string_lossy();
257 eprintln!(" {name}: now depends on {added:?}");
258}
259
260fn verify_output(ctx: &context::CodegenContext, rust_out: &Path, cpp_out: &Path) {
262 let mut errors: Vec<String> = Vec::new();
263
264 for (i, entry) in ctx.func_table.iter().enumerate() {
266 if entry.func_id != i as u32 {
267 errors.push(format!(
268 "FuncId gap: expected {} for module '{}' {}.{}, got {} (total functions: {})",
269 i, entry.module_name, entry.class_name, entry.func_name,
270 entry.func_id, ctx.func_table.len()
271 ));
272 break; }
274 }
275
276 let rust_required = ["lib.rs", "func_ids.rs"];
278 for name in &rust_required {
279 let path = rust_out.join(name);
280 match std::fs::metadata(&path) {
281 Ok(m) if m.len() == 0 => errors.push(format!("Rust output empty: {}", path.display())),
282 Err(_) => errors.push(format!("Rust output missing: {}", path.display())),
283 _ => {}
284 }
285 }
286
287 for module_name in ctx.enabled_modules.iter() {
289 let mod_path = rust_out.join(module_name).join("mod.rs");
290 if !mod_path.exists() {
291 errors.push(format!("Module mod.rs missing: {}", mod_path.display()));
292 }
293 }
294
295 let cpp_required = ["RustealFuncIds.h", "RustealFillFuncTable.cpp"];
297 for name in &cpp_required {
298 let path = cpp_out.join(name);
299 match std::fs::metadata(&path) {
300 Ok(m) if m.len() == 0 => errors.push(format!("C++ output empty: {}", path.display())),
301 Err(_) => errors.push(format!("C++ output missing: {}", path.display())),
302 _ => {}
303 }
304 }
305
306 let func_count = ctx.func_table.len();
308 let module_count = ctx.enabled_modules.len();
309 let class_count: usize = ctx.module_classes.values().map(|v| v.len()).sum();
310 let struct_count: usize = ctx.module_structs.values().map(|v| v.len()).sum();
311 let enum_count: usize = ctx.module_enums.values().map(|v| v.len()).sum();
312
313 if errors.is_empty() {
314 eprintln!(
315 " OK: {} modules, {} classes, {} structs, {} enums, {} functions",
316 module_count, class_count, struct_count, enum_count, func_count
317 );
318 } else {
319 eprintln!(" Verification FAILED:");
320 for e in &errors {
321 eprintln!(" - {e}");
322 }
323 std::process::exit(1);
324 }
325}
326
327fn build_func_table(ctx: &mut context::CodegenContext) {
329 let mut entries = Vec::new();
330
331 for (module_name, classes) in &ctx.module_classes {
332 for class in classes {
333 if class.super_class.as_deref() == Some("Interface") {
335 continue;
336 }
337 for func in &class.funcs {
338 let all_supported = func.params.iter().all(|p| {
340 type_map::map_property_type(
341 &p.prop_type,
342 p.class_name.as_deref(),
343 p.struct_name.as_deref(),
344 p.enum_name.as_deref(),
345 p.enum_underlying_type.as_deref(),
346 p.meta_class_name.as_deref(),
347 p.interface_name.as_deref(),
348 )
349 .supported
350 });
351 if !all_supported {
352 continue;
353 }
354 entries.push(context::FuncEntry {
355 func_id: 0, module_name: module_name.clone(),
357 class_name: class.name.clone(),
358 func_name: func.name.clone(),
359 rust_func_name: naming::to_snake_case(&func.name),
360 func: func.clone(),
361 cpp_class_name: class.cpp_name.clone(),
362 header: class.header.clone(),
363 });
364 }
365 }
366 }
367
368 entries.sort_by(|a, b| {
370 a.module_name
371 .cmp(&b.module_name)
372 .then_with(|| a.class_name.cmp(&b.class_name))
373 .then_with(|| a.func_name.cmp(&b.func_name))
374 });
375
376 for (i, entry) in entries.iter_mut().enumerate() {
378 entry.func_id = i as u32;
379 }
380
381 ctx.func_table = entries;
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387
388 #[test]
389 fn listed_classes_keep_their_ancestors_only() {
390 let class = |name: &str, package: &str, parent: &str| -> schema::ClassInfo {
391 serde_json::from_value(serde_json::json!({
392 "name": name, "cpp_name": format!("U{name}"), "package": package,
393 "header": "", "class_flags": 0, "super": parent,
394 }))
395 .unwrap()
396 };
397 let codegen: CodegenConfig = toml::from_str(
398 r#"
399 features = ["core", "editor"]
400 [modules]
401 CoreUObject = { module = "core_ue", feature = "core" }
402 UnrealEd = { module = "unreal_ed", feature = "editor", classes = ["EditorActorSubsystem"] }
403 "#,
404 )
405 .unwrap();
406 let ctx = context::CodegenContext::new(
407 vec![
408 class("Object", "CoreUObject", ""),
409 class("EditorSubsystemBase", "UnrealEd", "Object"),
410 class("EditorActorSubsystem", "UnrealEd", "EditorSubsystemBase"),
411 class("Factory", "UnrealEd", "Object"),
412 ],
413 vec![],
414 vec![],
415 &codegen,
416 );
417 let blocked = with_unlisted_classes(&ctx, &codegen).classes;
418 assert_eq!(blocked, ["Factory"]);
419 }
420
421 #[test]
422 fn unchanged_output_keeps_its_timestamp() {
423 let dir = std::env::temp_dir().join(format!("rusteal-write-{}", std::process::id()));
424 std::fs::create_dir_all(&dir).unwrap();
425 let path = dir.join("out.rs");
426 write_if_changed(&path, "a").unwrap();
427 let old = std::time::SystemTime::now() - std::time::Duration::from_secs(3600);
428 std::fs::File::options().write(true).open(&path).unwrap().set_modified(old).unwrap();
429
430 write_if_changed(&path, "a").unwrap();
431 assert_eq!(std::fs::metadata(&path).unwrap().modified().unwrap(), old);
432 write_if_changed(&path, "b").unwrap();
433 assert_ne!(std::fs::metadata(&path).unwrap().modified().unwrap(), old);
434 assert_eq!(std::fs::read_to_string(&path).unwrap(), "b");
435 std::fs::remove_dir_all(&dir).unwrap();
436 }
437
438 #[test]
439 fn plugin_dependencies_follow_enabled_modules() {
440 let config: crate::config::CodegenConfig = toml::from_str(
441 r#"
442 features = ["engine", "state-tree"]
443 [modules]
444 Engine = { module = "engine", feature = "engine" }
445 StateTreeModule = { module = "state_tree", feature = "state-tree", plugin = "StateTree" }
446 GameplayStateTreeModule = { module = "gameplay_state_tree", feature = "state-tree", plugin = "GameplayStateTree" }
447 Niagara = { module = "niagara", feature = "niagara", plugin = "Niagara" }
448 "#,
449 )
450 .unwrap();
451 let dir = std::env::temp_dir().join(format!("rusteal-uplugin-{}", std::process::id()));
452 std::fs::create_dir_all(&dir).unwrap();
453 let descriptor = dir.join("Rusteal.uplugin");
454 std::fs::write(
455 &descriptor,
456 r#"{ "VersionName": "0.0.0", "Plugins": [ { "Name": "EnhancedInput", "Enabled": true } ] }"#,
457 )
458 .unwrap();
459
460 update_plugin_dependencies(&config, &descriptor);
461 update_plugin_dependencies(&config, &descriptor);
462
463 let json: serde_json::Value =
464 serde_json::from_str(&std::fs::read_to_string(&descriptor).unwrap()).unwrap();
465 let names: Vec<&str> = json["Plugins"]
466 .as_array()
467 .unwrap()
468 .iter()
469 .map(|p| p["Name"].as_str().unwrap())
470 .collect();
471 assert_eq!(names, ["EnhancedInput", "GameplayStateTree", "StateTree"]);
472 assert_eq!(json["VersionName"], "0.0.0");
473 std::fs::remove_dir_all(&dir).unwrap();
474 }
475}