1use serde::Deserialize;
9use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11
12pub const FILE_NAME: &str = "rusteal.toml";
14
15#[derive(Deserialize)]
17pub struct ProjectConfig {
18 pub project: ProjectSection,
19 pub codegen: CodegenConfig,
20}
21
22#[derive(Deserialize)]
23pub struct ProjectSection {
24 #[serde(rename = "crate")]
26 pub crate_name: String,
27 #[serde(default)]
29 pub features: Vec<String>,
30}
31
32#[derive(Deserialize)]
35pub struct PluginConfig {
36 pub plugin: PluginSection,
37 pub codegen: CodegenConfig,
38}
39
40#[derive(Deserialize)]
41pub struct PluginSection {
42 #[serde(rename = "crate")]
44 pub crate_name: String,
45 #[serde(default)]
47 pub features: Vec<String>,
48 #[serde(default)]
51 pub kind: LibraryKind,
52}
53
54#[derive(Deserialize, Default, Clone, Copy, Debug, PartialEq, Eq)]
56#[serde(rename_all = "lowercase")]
57pub enum LibraryKind {
58 #[default]
60 Runtime,
61 Editor,
63}
64
65impl PluginConfig {
66 pub fn load(plugin_dir: &Path) -> Result<Self, String> {
68 let path = plugin_dir.join(FILE_NAME);
69 let text = std::fs::read_to_string(&path)
70 .map_err(|e| format!("cannot read {}: {e}", path.display()))?;
71 toml::from_str(&text).map_err(|e| format!("cannot parse {}: {e}", path.display()))
72 }
73}
74
75#[derive(Deserialize, Clone)]
76pub struct CodegenConfig {
77 pub features: Vec<String>,
78 pub modules: HashMap<String, ModuleMapping>,
79 #[serde(default)]
80 pub blocklist: Blocklist,
81}
82
83#[derive(Deserialize, Clone)]
84pub struct ModuleMapping {
85 pub module: String,
86 pub feature: String,
87 #[serde(default)]
90 pub plugin: Option<String>,
91 #[serde(default)]
95 pub classes: Option<Vec<String>>,
96}
97
98#[derive(Deserialize, Default, Clone)]
99pub struct Blocklist {
100 #[serde(default)]
101 pub classes: Vec<String>,
102 #[serde(default)]
103 pub structs: Vec<String>,
104 #[serde(default)]
106 pub functions: Vec<String>,
107}
108
109const RUSTEAL_INTERNAL_CLASSES: &[&str] =
112 &["RustealReifiedClass", "RustealReifiedFunction", "RustealDelegateProxy"];
113
114impl CodegenConfig {
115 pub fn for_library(&self, kind: LibraryKind) -> CodegenConfig {
120 let mut config = self.clone();
121 let core = config.features.first().cloned().unwrap_or_else(|| "core".to_string());
122 let mut add = |package: &str, module: &str| {
123 config.modules.entry(package.to_string()).or_insert_with(|| ModuleMapping {
124 module: module.to_string(),
125 feature: core.clone(),
126 plugin: None,
127 classes: None,
128 });
129 };
130 add("Rusteal", "rusteal");
131 if kind == LibraryKind::Editor {
132 add("RustealEditor", "rusteal_editor");
133 }
134 for class in RUSTEAL_INTERNAL_CLASSES {
135 if !config.blocklist.classes.iter().any(|c| c == class) {
136 config.blocklist.classes.push(class.to_string());
137 }
138 }
139 config
140 }
141}
142
143impl Blocklist {
144 pub fn function_tuples(&self) -> Vec<(String, String)> {
146 self.functions
147 .iter()
148 .filter_map(|entry| {
149 let (class, func) = entry.split_once('.')?;
150 Some((class.to_string(), func.to_string()))
151 })
152 .collect()
153 }
154}
155
156impl ProjectConfig {
157 pub fn load(root: &Path) -> Result<Self, String> {
159 let path = root.join(FILE_NAME);
160 let text = std::fs::read_to_string(&path)
161 .map_err(|e| format!("cannot read {}: {e}", path.display()))?;
162 toml::from_str(&text).map_err(|e| format!("cannot parse {}: {e}", path.display()))
163 }
164}
165
166pub struct ProjectLayout {
169 pub root: PathBuf,
171}
172
173impl ProjectLayout {
174 pub fn new(root: &Path) -> Self {
175 Self { root: root.to_path_buf() }
176 }
177
178 pub fn uproject(&self) -> Option<PathBuf> {
180 find_uproject(&self.root)
181 }
182
183 pub fn rust_workspace(&self) -> PathBuf {
185 self.root.join("Rust")
186 }
187
188 pub fn bindings_crate(&self) -> PathBuf {
190 self.rust_workspace().join("bindings")
191 }
192
193 pub fn cpp_generated(&self) -> PathBuf {
195 self.root.join("Plugins/Rusteal/Source/Rusteal/Generated")
196 }
197
198 pub fn plugin_descriptor(&self) -> PathBuf {
200 self.root.join("Plugins/Rusteal/Rusteal.uplugin")
201 }
202
203 pub fn uht_json(&self) -> PathBuf {
205 self.root.join("Intermediate/Rusteal/uht")
206 }
207
208 pub fn config_file(&self) -> PathBuf {
210 self.root.join(FILE_NAME)
211 }
212}
213
214pub struct LibraryOutput {
218 pub bindings_crate: PathBuf,
220 pub cpp_generated: PathBuf,
222 pub plugin_descriptor: PathBuf,
225 pub host_module: String,
227 pub cpp_prefix: String,
229 pub kind: LibraryKind,
232}
233
234impl ProjectLayout {
235 pub fn game_output(&self) -> LibraryOutput {
237 LibraryOutput {
238 bindings_crate: self.bindings_crate(),
239 cpp_generated: self.cpp_generated(),
240 plugin_descriptor: self.plugin_descriptor(),
241 host_module: "Rusteal".to_string(),
242 cpp_prefix: String::new(),
243 kind: LibraryKind::Runtime,
244 }
245 }
246}
247
248pub struct PluginLayout {
251 pub dir: PathBuf,
253 pub name: String,
255}
256
257impl PluginLayout {
258 pub fn new(dir: &Path) -> Self {
259 let name = dir
260 .file_name()
261 .map(|n| n.to_string_lossy().into_owned())
262 .unwrap_or_default();
263 Self { dir: dir.to_path_buf(), name }
264 }
265
266 pub fn descriptor(&self) -> PathBuf {
268 self.dir.join(format!("{}.uplugin", self.name))
269 }
270
271 pub fn config_file(&self) -> PathBuf {
273 self.dir.join(FILE_NAME)
274 }
275
276 pub fn rust_workspace(&self) -> PathBuf {
278 self.dir.join("Rust")
279 }
280
281 pub fn module_dir(&self) -> PathBuf {
283 self.dir.join("Source").join(&self.name)
284 }
285
286 pub fn binaries(&self, platform: &str) -> PathBuf {
289 self.dir.join("Binaries").join(platform)
290 }
291
292 pub fn library_stem(&self) -> String {
295 format!("rusteal_{}", self.name)
296 }
297
298 pub fn output(&self, kind: LibraryKind) -> LibraryOutput {
300 LibraryOutput {
301 bindings_crate: self.rust_workspace().join("bindings"),
302 cpp_generated: self.module_dir().join("Generated"),
303 plugin_descriptor: self.descriptor(),
304 host_module: self.name.clone(),
305 cpp_prefix: format!("{}_", self.name),
306 kind,
307 }
308 }
309}
310
311pub fn find_plugins(project_root: &Path) -> Vec<PluginLayout> {
314 let Ok(entries) = std::fs::read_dir(project_root.join("Plugins")) else {
315 return Vec::new();
316 };
317 let mut plugins: Vec<PluginLayout> = entries
318 .flatten()
319 .map(|entry| entry.path())
320 .filter(|dir| dir.join(FILE_NAME).is_file())
321 .map(|dir| PluginLayout::new(&dir))
322 .collect();
323 plugins.sort_by(|a, b| a.name.cmp(&b.name));
324 plugins
325}
326
327pub fn find_uproject(dir: &Path) -> Option<PathBuf> {
329 std::fs::read_dir(dir)
330 .ok()?
331 .flatten()
332 .map(|entry| entry.path())
333 .find(|path| path.extension().is_some_and(|ext| ext == "uproject"))
334}
335
336pub fn find_project_root(start: &Path) -> Option<PathBuf> {
338 let start = start.canonicalize().ok()?;
339 start
340 .ancestors()
341 .find(|dir| find_uproject(dir).is_some())
342 .map(Path::to_path_buf)
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348
349 fn codegen() -> CodegenConfig {
350 toml::from_str(
351 r#"
352 features = ["core", "engine"]
353 [modules]
354 Engine = { module = "engine", feature = "engine" }
355 [blocklist]
356 classes = ["Foo"]
357 "#,
358 )
359 .unwrap()
360 }
361
362 #[test]
363 fn libraries_bind_rusteal_modules() {
364 let runtime = codegen().for_library(LibraryKind::Runtime);
365 assert_eq!(runtime.modules["Rusteal"].module, "rusteal");
366 assert_eq!(runtime.modules["Rusteal"].feature, "core");
367 assert!(!runtime.modules.contains_key("RustealEditor"));
368 assert!(runtime.blocklist.classes.iter().any(|c| c == "RustealDelegateProxy"));
369 assert!(runtime.blocklist.classes.iter().any(|c| c == "Foo"));
370
371 let editor = codegen().for_library(LibraryKind::Editor);
372 assert_eq!(editor.modules["RustealEditor"].module, "rusteal_editor");
373 }
374}