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::{ProjectConfig, ProjectLayout};
16use crate::schema::{ClassesFile, EnumsFile, StructsFile};
17
18pub fn run_generate(project_root: &Path) {
22 let config = ProjectConfig::load(project_root).unwrap_or_else(|e| {
23 eprintln!("rusteal-codegen: {e}");
24 std::process::exit(1);
25 });
26 let codegen = &config.codegen;
27 let layout = ProjectLayout::new(project_root);
28
29 let uht_input = layout.uht_json();
30 let classes_path = uht_input.join("rusteal_classes.json");
31 let structs_path = uht_input.join("rusteal_structs.json");
32 let enums_path = uht_input.join("rusteal_enums.json");
33 let rust_out = layout.bindings_crate();
35 let rust_src = rust_out.join("src");
36 let cpp_out = layout.cpp_generated();
37
38 eprintln!("rusteal-codegen: loading JSON...");
39
40 let classes_json: ClassesFile = {
42 let data = std::fs::read_to_string(&classes_path)
43 .unwrap_or_else(|e| panic!("Failed to read {}: {e}", classes_path.display()));
44 serde_json::from_str(&data)
45 .unwrap_or_else(|e| panic!("Failed to parse {}: {e}", classes_path.display()))
46 };
47
48 let structs_json: StructsFile = {
49 let data = std::fs::read_to_string(&structs_path)
50 .unwrap_or_else(|e| panic!("Failed to read {}: {e}", structs_path.display()));
51 serde_json::from_str(&data)
52 .unwrap_or_else(|e| panic!("Failed to parse {}: {e}", structs_path.display()))
53 };
54
55 let enums_json: EnumsFile = {
56 let data = std::fs::read_to_string(&enums_path)
57 .unwrap_or_else(|e| panic!("Failed to read {}: {e}", enums_path.display()));
58 serde_json::from_str(&data)
59 .unwrap_or_else(|e| panic!("Failed to parse {}: {e}", enums_path.display()))
60 };
61
62 eprintln!(
63 " Loaded {} classes, {} structs, {} enums",
64 classes_json.classes.len(),
65 structs_json.structs.len(),
66 enums_json.enums.len()
67 );
68
69 let mut ctx = context::CodegenContext::new(
71 classes_json.classes,
72 structs_json.structs,
73 enums_json.enums,
74 codegen,
75 );
76
77 eprintln!(
78 " Enabled modules: {:?}",
79 ctx.enabled_modules.iter().collect::<Vec<_>>()
80 );
81 for (module, classes) in &ctx.module_classes {
82 eprintln!(" {}: {} classes", module, classes.len());
83 }
84 for (module, structs) in &ctx.module_structs {
85 eprintln!(" {}: {} structs", module, structs.len());
86 }
87 for (module, enums) in &ctx.module_enums {
88 eprintln!(" {}: {} enums", module, enums.len());
89 }
90
91 eprintln!("rusteal-codegen: filtering...");
93 filter::apply_filters(&mut ctx, &codegen.blocklist);
94
95 eprintln!("rusteal-codegen: building function table...");
97 build_func_table(&mut ctx);
98 eprintln!(" {} functions in func_table", ctx.func_table.len());
99
100 eprintln!("rusteal-codegen: generating Rust code...");
102 rust_gen::generate(&ctx, &rust_src);
103 rust_gen::manual::write_manual_module(&rust_src);
104 rust_gen::prelude::write_prelude(&rust_src, &ctx);
105 rust_gen::cargo_toml::write_crate_files(&rust_out, &ctx, codegen);
106
107 eprintln!("rusteal-codegen: generating C++ code...");
109 cpp_gen::generate(&ctx, &cpp_out);
110
111 generate_module_deps(codegen, &cpp_out);
114 update_plugin_dependencies(codegen, &layout.plugin_descriptor());
115
116 eprintln!("rusteal-codegen: verifying output...");
118 verify_output(&ctx, &rust_src, &cpp_out);
119
120 eprintln!("rusteal-codegen: done!");
121}
122
123fn generate_module_deps(config: &crate::config::CodegenConfig, cpp_out: &Path) {
125 use std::collections::BTreeSet;
126
127 let enabled_features: std::collections::HashSet<&str> =
128 config.features.iter().map(|s| s.as_str()).collect();
129
130 let mut ue_modules: BTreeSet<&str> = BTreeSet::new();
133 ue_modules.insert("Core");
134 for (pkg, mapping) in &config.modules {
135 if enabled_features.contains(mapping.feature.as_str()) {
136 ue_modules.insert(pkg.as_str());
137 }
138 }
139
140 let content = ue_modules.iter().copied().collect::<Vec<_>>().join("\n");
141
142 let path = cpp_out.join("module_deps.txt");
143 std::fs::write(&path, &content)
144 .unwrap_or_else(|e| panic!("Failed to write {}: {e}", path.display()));
145
146 eprintln!(" module_deps.txt: {:?}", ue_modules.iter().collect::<Vec<_>>());
147}
148
149fn update_plugin_dependencies(config: &crate::config::CodegenConfig, descriptor: &Path) {
153 let Ok(text) = std::fs::read_to_string(descriptor) else {
154 return; };
156 let mut json: serde_json::Value = serde_json::from_str(&text)
157 .unwrap_or_else(|e| panic!("Failed to parse {}: {e}", descriptor.display()));
158 let enabled: std::collections::HashSet<&str> =
159 config.features.iter().map(|s| s.as_str()).collect();
160 let mut wanted: Vec<&str> = config
161 .modules
162 .values()
163 .filter(|m| enabled.contains(m.feature.as_str()))
164 .filter_map(|m| m.plugin.as_deref())
165 .collect();
166 wanted.sort();
167 wanted.dedup();
168
169 let plugins = json
170 .as_object_mut()
171 .expect("Rusteal.uplugin is a JSON object")
172 .entry("Plugins")
173 .or_insert_with(|| serde_json::Value::Array(Vec::new()))
174 .as_array_mut()
175 .expect("Rusteal.uplugin's Plugins is an array");
176 let mut added = Vec::new();
177 for name in wanted {
178 if plugins.iter().any(|p| p["Name"] == name) {
179 continue;
180 }
181 plugins.push(serde_json::json!({ "Name": name, "Enabled": true }));
182 added.push(name);
183 }
184 if added.is_empty() {
185 return;
186 }
187 let mut out = serde_json::to_string_pretty(&json).expect("serializable descriptor");
188 out.push('\n');
189 std::fs::write(descriptor, out)
190 .unwrap_or_else(|e| panic!("Failed to write {}: {e}", descriptor.display()));
191 eprintln!(" Rusteal.uplugin: now depends on {added:?}");
192}
193
194fn verify_output(ctx: &context::CodegenContext, rust_out: &Path, cpp_out: &Path) {
196 let mut errors: Vec<String> = Vec::new();
197
198 for (i, entry) in ctx.func_table.iter().enumerate() {
200 if entry.func_id != i as u32 {
201 errors.push(format!(
202 "FuncId gap: expected {} for module '{}' {}.{}, got {} (total functions: {})",
203 i, entry.module_name, entry.class_name, entry.func_name,
204 entry.func_id, ctx.func_table.len()
205 ));
206 break; }
208 }
209
210 let rust_required = ["lib.rs", "func_ids.rs"];
212 for name in &rust_required {
213 let path = rust_out.join(name);
214 match std::fs::metadata(&path) {
215 Ok(m) if m.len() == 0 => errors.push(format!("Rust output empty: {}", path.display())),
216 Err(_) => errors.push(format!("Rust output missing: {}", path.display())),
217 _ => {}
218 }
219 }
220
221 for module_name in ctx.enabled_modules.iter() {
223 let mod_path = rust_out.join(module_name).join("mod.rs");
224 if !mod_path.exists() {
225 errors.push(format!("Module mod.rs missing: {}", mod_path.display()));
226 }
227 }
228
229 let cpp_required = ["RustealFuncIds.h", "RustealFillFuncTable.cpp"];
231 for name in &cpp_required {
232 let path = cpp_out.join(name);
233 match std::fs::metadata(&path) {
234 Ok(m) if m.len() == 0 => errors.push(format!("C++ output empty: {}", path.display())),
235 Err(_) => errors.push(format!("C++ output missing: {}", path.display())),
236 _ => {}
237 }
238 }
239
240 let func_count = ctx.func_table.len();
242 let module_count = ctx.enabled_modules.len();
243 let class_count: usize = ctx.module_classes.values().map(|v| v.len()).sum();
244 let struct_count: usize = ctx.module_structs.values().map(|v| v.len()).sum();
245 let enum_count: usize = ctx.module_enums.values().map(|v| v.len()).sum();
246
247 if errors.is_empty() {
248 eprintln!(
249 " OK: {} modules, {} classes, {} structs, {} enums, {} functions",
250 module_count, class_count, struct_count, enum_count, func_count
251 );
252 } else {
253 eprintln!(" Verification FAILED:");
254 for e in &errors {
255 eprintln!(" - {e}");
256 }
257 std::process::exit(1);
258 }
259}
260
261fn build_func_table(ctx: &mut context::CodegenContext) {
263 let mut entries = Vec::new();
264
265 for (module_name, classes) in &ctx.module_classes {
266 for class in classes {
267 if class.super_class.as_deref() == Some("Interface") {
269 continue;
270 }
271 for func in &class.funcs {
272 let all_supported = func.params.iter().all(|p| {
274 type_map::map_property_type(
275 &p.prop_type,
276 p.class_name.as_deref(),
277 p.struct_name.as_deref(),
278 p.enum_name.as_deref(),
279 p.enum_underlying_type.as_deref(),
280 p.meta_class_name.as_deref(),
281 p.interface_name.as_deref(),
282 )
283 .supported
284 });
285 if !all_supported {
286 continue;
287 }
288 entries.push(context::FuncEntry {
289 func_id: 0, module_name: module_name.clone(),
291 class_name: class.name.clone(),
292 func_name: func.name.clone(),
293 rust_func_name: naming::to_snake_case(&func.name),
294 func: func.clone(),
295 cpp_class_name: class.cpp_name.clone(),
296 header: class.header.clone(),
297 });
298 }
299 }
300 }
301
302 entries.sort_by(|a, b| {
304 a.module_name
305 .cmp(&b.module_name)
306 .then_with(|| a.class_name.cmp(&b.class_name))
307 .then_with(|| a.func_name.cmp(&b.func_name))
308 });
309
310 for (i, entry) in entries.iter_mut().enumerate() {
312 entry.func_id = i as u32;
313 }
314
315 ctx.func_table = entries;
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 #[test]
323 fn plugin_dependencies_follow_enabled_modules() {
324 let config: crate::config::CodegenConfig = toml::from_str(
325 r#"
326 features = ["engine", "state-tree"]
327 [modules]
328 Engine = { module = "engine", feature = "engine" }
329 StateTreeModule = { module = "state_tree", feature = "state-tree", plugin = "StateTree" }
330 GameplayStateTreeModule = { module = "gameplay_state_tree", feature = "state-tree", plugin = "GameplayStateTree" }
331 Niagara = { module = "niagara", feature = "niagara", plugin = "Niagara" }
332 "#,
333 )
334 .unwrap();
335 let dir = std::env::temp_dir().join(format!("rusteal-uplugin-{}", std::process::id()));
336 std::fs::create_dir_all(&dir).unwrap();
337 let descriptor = dir.join("Rusteal.uplugin");
338 std::fs::write(
339 &descriptor,
340 r#"{ "VersionName": "0.0.0", "Plugins": [ { "Name": "EnhancedInput", "Enabled": true } ] }"#,
341 )
342 .unwrap();
343
344 update_plugin_dependencies(&config, &descriptor);
345 update_plugin_dependencies(&config, &descriptor);
346
347 let json: serde_json::Value =
348 serde_json::from_str(&std::fs::read_to_string(&descriptor).unwrap()).unwrap();
349 let names: Vec<&str> = json["Plugins"]
350 .as_array()
351 .unwrap()
352 .iter()
353 .map(|p| p["Name"].as_str().unwrap())
354 .collect();
355 assert_eq!(names, ["EnhancedInput", "GameplayStateTree", "StateTree"]);
356 assert_eq!(json["VersionName"], "0.0.0");
357 std::fs::remove_dir_all(&dir).unwrap();
358 }
359}