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}
45
46#[derive(Deserialize, Default)]
47pub struct Blocklist {
48    #[serde(default)]
49    pub classes: Vec<String>,
50    #[serde(default)]
51    pub structs: Vec<String>,
52    /// Function blocklist in "Class.Function" format.
53    #[serde(default)]
54    pub functions: Vec<String>,
55}
56
57impl Blocklist {
58    /// Parse function blocklist entries into (class, function) tuples.
59    pub fn function_tuples(&self) -> Vec<(String, String)> {
60        self.functions
61            .iter()
62            .filter_map(|entry| {
63                let (class, func) = entry.split_once('.')?;
64                Some((class.to_string(), func.to_string()))
65            })
66            .collect()
67    }
68}
69
70impl ProjectConfig {
71    /// Read `rusteal.toml` from the project root.
72    pub fn load(root: &Path) -> Result<Self, String> {
73        let path = root.join(FILE_NAME);
74        let text = std::fs::read_to_string(&path)
75            .map_err(|e| format!("cannot read {}: {e}", path.display()))?;
76        toml::from_str(&text).map_err(|e| format!("cannot parse {}: {e}", path.display()))
77    }
78}
79
80/// Where things live inside a Rusteal project. Fixed by convention so that the
81/// config, the CLI and the plugin agree without a single path in `rusteal.toml`.
82pub struct ProjectLayout {
83    /// The directory holding the .uproject.
84    pub root: PathBuf,
85}
86
87impl ProjectLayout {
88    pub fn new(root: &Path) -> Self {
89        Self { root: root.to_path_buf() }
90    }
91
92    /// The .uproject file, if the root holds one.
93    pub fn uproject(&self) -> Option<PathBuf> {
94        find_uproject(&self.root)
95    }
96
97    /// Cargo workspace with the game crate(s) and the bindings.
98    pub fn rust_workspace(&self) -> PathBuf {
99        self.root.join("Rust")
100    }
101
102    /// The generated `bindings` crate.
103    pub fn bindings_crate(&self) -> PathBuf {
104        self.rust_workspace().join("bindings")
105    }
106
107    /// The generated C++ wrappers, compiled into the Rusteal plugin.
108    pub fn cpp_generated(&self) -> PathBuf {
109        self.root.join("Plugins/Rusteal/Source/Rusteal/Generated")
110    }
111
112    /// The reflection JSON the codegen reads: build output, not versioned.
113    pub fn uht_json(&self) -> PathBuf {
114        self.root.join("Intermediate/Rusteal/uht")
115    }
116
117    /// The project's `rusteal.toml`.
118    pub fn config_file(&self) -> PathBuf {
119        self.root.join(FILE_NAME)
120    }
121}
122
123/// The .uproject file in `dir`, if any.
124pub fn find_uproject(dir: &Path) -> Option<PathBuf> {
125    std::fs::read_dir(dir)
126        .ok()?
127        .flatten()
128        .map(|entry| entry.path())
129        .find(|path| path.extension().is_some_and(|ext| ext == "uproject"))
130}
131
132/// Walk up from `start` to the first directory holding a .uproject.
133pub fn find_project_root(start: &Path) -> Option<PathBuf> {
134    let start = start.canonicalize().ok()?;
135    start
136        .ancestors()
137        .find(|dir| find_uproject(dir).is_some())
138        .map(Path::to_path_buf)
139}