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}