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}