Skip to main content

rusteal_codegen/
config.rs

1// Project configuration: `<Project>/rusteal.toml`, next to the .uproject.
2//
3// The file is versioned with the project and holds only what varies between
4// projects: the game crate and the codegen module selection. Everything else
5// is a fixed layout (see `ProjectLayout`). The engine location is per machine
6// and lives in the CLI's own configuration, not here.
7
8use serde::Deserialize;
9use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11
12/// Name of the project configuration file.
13pub const FILE_NAME: &str = "rusteal.toml";
14
15/// Top-level `rusteal.toml`.
16#[derive(Deserialize)]
17pub struct ProjectConfig {
18    pub project: ProjectSection,
19    pub codegen: CodegenConfig,
20}
21
22#[derive(Deserialize)]
23pub struct ProjectSection {
24    /// Cargo package, a cdylib member of `Rust/`, deployed as the Rusteal library.
25    #[serde(rename = "crate")]
26    pub crate_name: String,
27    /// Extra cargo features to build it with.
28    #[serde(default)]
29    pub features: Vec<String>,
30}
31
32#[derive(Deserialize)]
33pub struct CodegenConfig {
34    pub features: Vec<String>,
35    pub modules: HashMap<String, ModuleMapping>,
36    #[serde(default)]
37    pub blocklist: Blocklist,
38}
39
40#[derive(Deserialize)]
41pub struct ModuleMapping {
42    pub module: String,
43    pub feature: String,
44    /// The engine plugin the UE module belongs to (`StateTree`), for a module
45    /// outside the engine's own: the Rusteal plugin must list it.
46    #[serde(default)]
47    pub plugin: Option<String>,
48}
49
50#[derive(Deserialize, Default)]
51pub struct Blocklist {
52    #[serde(default)]
53    pub classes: Vec<String>,
54    #[serde(default)]
55    pub structs: Vec<String>,
56    /// Function blocklist in "Class.Function" format.
57    #[serde(default)]
58    pub functions: Vec<String>,
59}
60
61impl Blocklist {
62    /// Parse function blocklist entries into (class, function) tuples.
63    pub fn function_tuples(&self) -> Vec<(String, String)> {
64        self.functions
65            .iter()
66            .filter_map(|entry| {
67                let (class, func) = entry.split_once('.')?;
68                Some((class.to_string(), func.to_string()))
69            })
70            .collect()
71    }
72}
73
74impl ProjectConfig {
75    /// Read `rusteal.toml` from the project root.
76    pub fn load(root: &Path) -> Result<Self, String> {
77        let path = root.join(FILE_NAME);
78        let text = std::fs::read_to_string(&path)
79            .map_err(|e| format!("cannot read {}: {e}", path.display()))?;
80        toml::from_str(&text).map_err(|e| format!("cannot parse {}: {e}", path.display()))
81    }
82}
83
84/// Where things live inside a Rusteal project. Fixed by convention so that the
85/// config, the CLI and the plugin agree without a single path in `rusteal.toml`.
86pub struct ProjectLayout {
87    /// The directory holding the .uproject.
88    pub root: PathBuf,
89}
90
91impl ProjectLayout {
92    pub fn new(root: &Path) -> Self {
93        Self { root: root.to_path_buf() }
94    }
95
96    /// The .uproject file, if the root holds one.
97    pub fn uproject(&self) -> Option<PathBuf> {
98        find_uproject(&self.root)
99    }
100
101    /// Cargo workspace with the game crate(s) and the bindings.
102    pub fn rust_workspace(&self) -> PathBuf {
103        self.root.join("Rust")
104    }
105
106    /// The generated `bindings` crate.
107    pub fn bindings_crate(&self) -> PathBuf {
108        self.rust_workspace().join("bindings")
109    }
110
111    /// The generated C++ wrappers, compiled into the Rusteal plugin.
112    pub fn cpp_generated(&self) -> PathBuf {
113        self.root.join("Plugins/Rusteal/Source/Rusteal/Generated")
114    }
115
116    /// The Rusteal plugin's descriptor in the project.
117    pub fn plugin_descriptor(&self) -> PathBuf {
118        self.root.join("Plugins/Rusteal/Rusteal.uplugin")
119    }
120
121    /// The reflection JSON the codegen reads: build output, not versioned.
122    pub fn uht_json(&self) -> PathBuf {
123        self.root.join("Intermediate/Rusteal/uht")
124    }
125
126    /// The project's `rusteal.toml`.
127    pub fn config_file(&self) -> PathBuf {
128        self.root.join(FILE_NAME)
129    }
130}
131
132/// The .uproject file in `dir`, if any.
133pub fn find_uproject(dir: &Path) -> Option<PathBuf> {
134    std::fs::read_dir(dir)
135        .ok()?
136        .flatten()
137        .map(|entry| entry.path())
138        .find(|path| path.extension().is_some_and(|ext| ext == "uproject"))
139}
140
141/// Walk up from `start` to the first directory holding a .uproject.
142pub fn find_project_root(start: &Path) -> Option<PathBuf> {
143    let start = start.canonicalize().ok()?;
144    start
145        .ancestors()
146        .find(|dir| find_uproject(dir).is_some())
147        .map(Path::to_path_buf)
148}