Skip to main content

rusteal_codegen/
config.rs

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