concinnity_cook/build_only/
expand.rs1use super::app_config::apply_app_config;
5use super::camera_shot::expand_camera_shots;
6use super::character_model::expand_character_models;
7use super::companion::inject_companions;
8use super::light_rig::expand_light_rigs;
9use super::main_menu::expand_main_menus;
10use super::material_palette::expand_material_palettes;
11use super::menu_defaults::inject_menu_defaults;
12use super::option_select::expand_option_selects;
13use super::panel::expand_panels;
14use super::prefab::expand_prefabs;
15use super::room::expand_room_textures;
16use super::scene_import::expand_scene_imports;
17use super::slider::expand_sliders;
18use super::story::expand_stories;
19
20use crate::authoring::world::load_world;
21
22use std::path::Path;
23
24pub(crate) fn type_norm(v: &serde_json::Value) -> String {
27 v.get("type")
28 .and_then(|t| t.as_str())
29 .unwrap_or("")
30 .to_lowercase()
31 .replace('_', "")
32}
33
34pub(crate) fn asset_name(v: &serde_json::Value) -> String {
35 asset_name_str(v).to_string()
36}
37
38pub(crate) fn asset_name_str(v: &serde_json::Value) -> &str {
40 v.get("name").and_then(|n| n.as_str()).unwrap_or("")
41}
42
43#[derive(Debug, Clone)]
47pub struct InjectedAsset {
48 pub name: String,
50 pub asset_type: String,
52 pub args: serde_json::Value,
54 pub injected_by: &'static str,
57}
58
59#[derive(Debug, Clone)]
63pub struct GeneratedAsset {
64 pub name: String,
66 pub asset_type: String,
68 pub generated_by: String,
70}
71
72#[derive(Debug, Clone)]
78pub struct ShadowedAsset {
79 pub name: String,
81 pub asset_type: String,
83 pub generated_by: String,
85 pub args: serde_json::Value,
88}
89
90#[derive(Debug, Default)]
92pub(crate) struct ExpandReport {
93 pub injected: Vec<InjectedAsset>,
94 pub generated: Vec<GeneratedAsset>,
95 pub shadowed: Vec<ShadowedAsset>,
96}
97
98impl ExpandReport {
99 pub(crate) fn record(
100 &mut self,
101 name: &str,
102 asset_type: &str,
103 args: serde_json::Value,
104 injected_by: &'static str,
105 ) {
106 self.injected.push(InjectedAsset {
107 name: name.to_string(),
108 asset_type: asset_type.to_string(),
109 args,
110 injected_by,
111 });
112 }
113
114 pub(crate) fn record_generated(&mut self, name: &str, asset_type: &str, generated_by: &str) {
115 self.generated.push(GeneratedAsset {
116 name: name.to_string(),
117 asset_type: asset_type.to_string(),
118 generated_by: generated_by.to_string(),
119 });
120 }
121
122 pub(crate) fn record_shadowed(
126 &mut self,
127 name: &str,
128 asset_type: &str,
129 generated_by: &str,
130 args: serde_json::Value,
131 ) {
132 if self.shadowed.iter().any(|s| s.name == name) {
133 return;
134 }
135 self.shadowed.push(ShadowedAsset {
136 name: name.to_string(),
137 asset_type: asset_type.to_string(),
138 generated_by: generated_by.to_string(),
139 args,
140 });
141 }
142}
143
144pub(crate) fn expand_world(
150 assets: &mut Vec<serde_json::Value>,
151 assets_dir: Option<&Path>,
152) -> Result<ExpandReport, String> {
153 let mut report = ExpandReport::default();
154 let authored: std::collections::HashMap<String, String> = assets
159 .iter()
160 .map(|v| {
161 (
162 asset_name(v),
163 v.get("type")
164 .and_then(|t| t.as_str())
165 .unwrap_or("?")
166 .to_string(),
167 )
168 })
169 .filter(|(n, _)| !n.is_empty())
170 .collect();
171 expand_scene_imports(assets, &mut report, assets_dir)?;
175 expand_stories(assets)?;
179 expand_camera_shots(assets, assets_dir);
180 expand_character_models(assets)?;
183 expand_light_rigs(assets, assets_dir);
184 expand_material_palettes(assets, assets_dir);
185 expand_prefabs(assets, &authored, &mut report, assets_dir)?;
186 expand_room_textures(assets);
187 inject_companions(assets, &mut report);
191 apply_app_config(assets, &mut report)?;
196 inject_menu_defaults(assets, &mut report)?;
201 expand_main_menus(assets)?;
206 expand_option_selects(assets)?;
210 expand_sliders(assets)?;
214 expand_panels(assets)?;
217 inject_companions(assets, &mut report);
220 Ok(report)
221}
222
223pub fn expand_world_from_str(
228 content: &str,
229 assets_dir: Option<&Path>,
230) -> std::io::Result<Vec<serde_json::Value>> {
231 let mut assets = load_world(content)
232 .map_err(|errs| std::io::Error::new(std::io::ErrorKind::InvalidData, errs.join("\n")))?;
233
234 let _ = expand_world(&mut assets, assets_dir)
235 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
236
237 Ok(assets)
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn type_norm_lowercases_and_strips_underscores() {
246 let v = serde_json::json!({"type": "MaterialPalette"});
247 assert_eq!(type_norm(&v), "materialpalette");
248 }
249
250 #[test]
251 fn type_norm_handles_underscored_type() {
252 let v = serde_json::json!({"type": "Camera3D"});
253 assert_eq!(type_norm(&v), "camera3d");
254 }
255
256 #[test]
257 fn type_norm_missing_type_returns_empty() {
258 let v = serde_json::json!({"name": "x"});
259 assert_eq!(type_norm(&v), "");
260 }
261
262 #[test]
263 fn asset_name_extracts_name() {
264 let v = serde_json::json!({"name": "my_asset", "type": "Logger"});
265 assert_eq!(asset_name(&v), "my_asset");
266 }
267
268 #[test]
269 fn asset_name_missing_returns_empty() {
270 let v = serde_json::json!({"type": "Logger"});
271 assert_eq!(asset_name(&v), "");
272 }
273
274 #[test]
277 fn a_failing_pass_aborts_the_whole_expansion() {
278 for (asset, needle) in [
279 (
280 serde_json::json!({"name":"s","type":"SceneImport","args":{}}),
281 "SceneImport 's': missing `source`",
282 ),
283 (
284 serde_json::json!({"name":"t","type":"StoryImport","args":{}}),
285 "StoryImport 't': missing `source`",
286 ),
287 (
288 serde_json::json!({"name":"p","type":"Prop","args":{"prefab":"ghost"}}),
289 "prefab 'ghost' not found",
290 ),
291 (
292 serde_json::json!({"type":"MainMenu","args":{}}),
293 "MainMenu: missing `name`",
294 ),
295 (
296 serde_json::json!({"type":"OptionSelect","args":{}}),
297 "OptionSelect: missing `name`",
298 ),
299 (
300 serde_json::json!({"type":"Slider","args":{}}),
301 "Slider: missing `name`",
302 ),
303 (
304 serde_json::json!({"type":"Panel","args":{}}),
305 "Panel: missing `name`",
306 ),
307 ] {
308 let mut assets = vec![asset.clone()];
309 let err = expand_world(&mut assets, None).unwrap_err();
310 assert!(err.contains(needle), "{asset} -> {err}");
311 }
312 }
313
314 #[test]
315 fn a_second_engine_defaults_entry_aborts_the_expansion() {
316 let mut assets = vec![
317 serde_json::json!({"name":"a","type":"EngineDefaults","args":{}}),
318 serde_json::json!({"name":"b","type":"EngineDefaults","args":{}}),
319 ];
320 let err = expand_world(&mut assets, None).unwrap_err();
321 assert!(err.contains("at most one"), "{err}");
322 }
323
324 #[test]
325 fn a_window_that_cannot_take_the_app_config_title_aborts_the_expansion() {
326 let mut assets = vec![
327 serde_json::json!({"name":"app","type":"AppConfig","args":{"name":"My Game"}}),
328 serde_json::json!({"name":"win","type":"Window","args":[]}),
329 ];
330 let err = expand_world(&mut assets, None).unwrap_err();
331 assert!(err.contains("Window 'win'"), "{err}");
332 assert!(err.contains("args must be an object"), "{err}");
333 }
334
335 #[test]
338 fn expand_world_from_str_surfaces_load_and_expansion_errors() {
339 let malformed = expand_world_from_str("not json at all\n", None).unwrap_err();
340 assert_eq!(malformed.kind(), std::io::ErrorKind::InvalidData);
341 assert!(!malformed.to_string().is_empty());
342
343 let broken = r#"{"name":"p","type":"Prop","args":{"prefab":"ghost"}}"#;
344 let err = expand_world_from_str(broken, None).unwrap_err();
345 assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
346 assert!(err.to_string().contains("ghost"), "{err}");
347 }
348
349 #[test]
350 fn expand_world_from_str_injects_companions() {
351 let content = r#"{"name":"gfx","type":"GraphicsConfig","args":{}}"#;
352 let assets = expand_world_from_str(content, None).unwrap();
353 assert!(assets.iter().any(|v| type_norm(v) == "graphicsconfig"));
354 assert!(assets.iter().any(|v| type_norm(v) == "window"));
356 }
357
358 #[test]
359 fn bare_main_menu_world_expands_and_pulls_companions() {
360 let content = r#"{"name":"main_menu","type":"MainMenu"}"#;
361 let assets = expand_world_from_str(content, None).unwrap();
362 assert!(!assets.iter().any(|v| type_norm(v) == "mainmenu"));
364 assert!(assets.iter().any(|v| type_norm(v) == "screen"));
365 assert!(assets.iter().any(|v| type_norm(v) == "hitregion"));
366 assert!(assets.iter().any(|v| type_norm(v) == "textlabel"));
368 assert!(assets.iter().any(|v| type_norm(v) == "graphicsconfig"));
369 assert!(assets.iter().any(|v| type_norm(v) == "font"));
370 }
371}