Skip to main content

concinnity_core/components/
app_config.rs

1// src/components/app_config.rs
2//
3// The `AppConfig` asset. Only what the running process needs survives
4// the bake: the state-tree location and the resource budgets. The distribution
5// metadata (name, id, version, author, icon) is read at build / export time
6// from the authored world, so it never ships in the blob.
7
8use crate::ecs::Component;
9use alloc::string::{String, ToString};
10
11/// Names, identifies, and sizes the application.
12///
13/// Declare at most one `AppConfig` per world. It supplies the display name,
14/// bundle identifier, version, author, and icon that the export step reads when
15/// it packages the world into a distributable game (the archive and executable
16/// name, and on macOS the `.app` bundle metadata and icon). When the world
17/// declares no [Window](#window) title of its own, `name` also fills the window
18/// title, so a running game shows its own name in the title bar.
19///
20/// `icon` is a path to a source image (a square PNG, 512x512 or larger)
21/// relative to the world; it is read by the packaging step and is not compiled
22/// into the world's data. `id` is a reverse-DNS bundle identifier
23/// (e.g. `gg.studio.mygame`); when left empty the export derives one from
24/// `name`. Empty string fields mean "unset".
25///
26/// `home` chooses where the running application keeps what it writes: the
27/// settings file, the save files, crash reports, and the shader caches. Leave
28/// it empty and those sit beside the application's data, which is what a
29/// portable install wants. A relative path resolves against that same content
30/// directory, so `"state"` puts them in a `state/` subfolder; an absolute path
31/// is used verbatim. A read-only install that sets no `home` relocates them to
32/// a per-user directory on its own.
33///
34/// `max_memory_mb` and `job_threads` are `0` for "auto", where the engine sizes
35/// both from the host machine. A non-zero value overrides that choice, clamped
36/// to what the machine can safely give.
37#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
38#[serde(default)]
39pub struct AppConfigArgs {
40    /// Display name of the application: the game's window title, the exported
41    /// archive and executable name, and the macOS bundle display name.
42    pub name: String,
43    /// Reverse-DNS bundle identifier (e.g. `gg.studio.mygame`). When empty the
44    /// export derives one from `name`.
45    pub id: String,
46    /// Human-readable version string (e.g. `1.0.0`).
47    pub version: String,
48    /// Author or studio name, recorded in the exported bundle's metadata.
49    pub author: String,
50    /// Path to a source icon image (a square PNG, 512x512 or larger) relative
51    /// to the world, used to build the platform icon at export time. Empty for
52    /// no custom icon.
53    pub icon: String,
54    /// Where the running application writes its settings, saves, crash reports,
55    /// and shader caches. Empty means beside the application's data; a relative
56    /// path resolves against that directory; an absolute path is used verbatim.
57    pub home: String,
58    /// Soft ceiling on host memory the runtime aims to stay under, in
59    /// mebibytes. `0` = auto (a fraction of total RAM, capped by a built-in
60    /// ceiling). A non-zero value is clamped so it never exceeds what the
61    /// machine can safely give.
62    pub max_memory_mb: u32,
63    /// Worker threads for the shared job pool. `0` = auto (one per core, less
64    /// one for the main thread). A non-zero value never exceeds the core count.
65    pub job_threads: u32,
66}
67
68impl Default for AppConfigArgs {
69    fn default() -> Self {
70        Self {
71            name: "Concinnity".to_string(),
72            id: String::new(),
73            version: "0.1.0".to_string(),
74            author: String::new(),
75            icon: String::new(),
76            home: String::new(),
77            max_memory_mb: 0,
78            job_threads: 0,
79        }
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn defaults_name_an_unversioned_unlimited_application() {
89        let a = AppConfigArgs::default();
90        assert_eq!(a.name, "Concinnity");
91        assert_eq!(a.version, "0.1.0");
92        assert!(a.id.is_empty());
93        assert!(a.author.is_empty());
94        assert!(a.icon.is_empty());
95        // Empty is "beside the data", not "the filesystem root".
96        assert!(a.home.is_empty());
97        // Zero is "no budget declared", not "no memory and no threads".
98        assert_eq!(a.max_memory_mb, 0);
99        assert_eq!(a.job_threads, 0);
100    }
101
102    #[test]
103    fn export_metadata_parses_and_round_trips_through_postcard() {
104        let a: AppConfigArgs = serde_json::from_str(
105            r#"{"name":"Pong","id":"com.example.pong","version":"1.2.0","author":"Bob",
106                "icon":"icon.png","home":"state","max_memory_mb":2048,"job_threads":8}"#,
107        )
108        .unwrap();
109        assert_eq!(a.id, "com.example.pong");
110        assert_eq!(a.job_threads, 8);
111
112        // Only `home` and the budgets ship in the blob, but the whole struct is
113        // what the export step reads back, so it has to survive the baked
114        // format.
115        let bytes = postcard::to_allocvec(&a).unwrap();
116        let back: AppConfigArgs = postcard::from_bytes(&bytes).unwrap();
117        assert_eq!(back.name, "Pong");
118        assert_eq!(back.version, "1.2.0");
119        assert_eq!(back.author, "Bob");
120        assert_eq!(back.icon, "icon.png");
121        assert_eq!(back.home, "state");
122        assert_eq!(back.max_memory_mb, 2048);
123    }
124}
125
126/// Runtime half of the AppConfig asset: where the application keeps what it
127/// writes, and its process resource budgets.
128#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
129pub struct AppConfig {
130    /// Where the running application writes its settings, saves, crash
131    /// reports, and shader caches. Empty means beside the application's data.
132    pub home: String,
133    /// Soft ceiling on host memory the runtime aims to stay under, in
134    /// mebibytes. `0` = auto.
135    pub max_memory_mb: u32,
136    /// Worker threads for the shared job pool. `0` = auto.
137    pub job_threads: u32,
138}
139
140impl AppConfig {
141    /// Translate the authored args into the runtime component: keep the state
142    /// location and the resource budgets. Run by cook at build time (the baked
143    /// blob record carries the result).
144    pub fn bake(args: AppConfigArgs) -> Self {
145        Self {
146            home: args.home,
147            max_memory_mb: args.max_memory_mb,
148            job_threads: args.job_threads,
149        }
150    }
151}
152
153impl Component for AppConfig {
154    const NAME: &'static str = "AppConfig";
155
156    fn from_baked(bytes: &[u8]) -> Result<Self, crate::result::CnResult> {
157        Ok(crate::blob::decode_exact(bytes)?)
158    }
159}