1pub(crate) mod animation_graph;
12pub(crate) mod asset_refs;
13pub(crate) mod audio;
14pub mod behavior;
15pub(crate) mod cross_reference;
16pub(crate) mod cubemap_texture;
17pub(crate) mod environment_map;
18pub mod fault;
19pub(crate) mod instanced_prop;
20pub(crate) mod mesh;
21pub(crate) mod physics;
22pub(crate) mod prop;
24pub(crate) mod sdf_volume;
26pub(crate) mod shader;
28pub(crate) mod shape;
29pub(crate) mod texture;
30pub(crate) mod voxel_chunk;
31pub(crate) mod voxel_world;
32
33use crate::authoring::world::WorldJsonlAsset;
34
35pub fn report_validation_errors(errors: &[String]) -> std::io::Error {
39 for e in errors {
40 eprintln!("error: {}", e);
41 }
42 eprintln!("\nvalidation failed ({} error(s))", errors.len());
43 std::io::Error::new(
44 std::io::ErrorKind::InvalidData,
45 format!("validation failed with {} error(s)", errors.len()),
46 )
47}
48
49fn check_authored_asset(
51 type_norm: &str,
52 name: &str,
53 args: &serde_json::Value,
54) -> Result<(), String> {
55 match type_norm {
56 "animationgraph" => animation_graph::check(name, args),
57 "behavior" => behavior::check(name, args),
58 "variables" => behavior::check_variables(name, args),
59 "shader" => shader::check(name, args),
60 "prop" => prop::check(name, args),
61 "sdfvolume" | "sdf" => sdf_volume::check(name, args),
62 "voxelchunk" | "chunk" => voxel_chunk::check(name, args),
63 "voxelworld" => voxel_world::check(name, args),
64 "instancedprop" | "instanced" => instanced_prop::check(name, args),
65 "triggervolume" => physics::check(name, args),
66 "audioemitter" => audio::check_emitter(name, args),
67 "audiocue" => audio::check_cue(name, args),
68 "propbody" => audio::check_prop_body(name, args),
69 _ => Ok(()),
70 }
71}
72
73fn check_compiled_asset(
75 type_norm: &str,
76 name: &str,
77 args: &serde_json::Value,
78) -> Result<(), String> {
79 match type_norm {
80 "texture" => texture::check(name, args),
81 "cubemaptexture" | "cubemap" => cubemap_texture::check(name, args),
82 "environmentmap" | "envmap" | "ibl" => environment_map::check(name, args),
83 "mesh" | "proceduralmesh" => mesh::check(name, args),
84 _ => Ok(()),
85 }
86}
87
88pub(crate) fn check_asset(
90 type_norm: &str,
91 name: &str,
92 args: &serde_json::Value,
93) -> Result<(), String> {
94 check_authored_asset(type_norm, name, args)?;
95 check_compiled_asset(type_norm, name, args)
96}
97
98pub(crate) fn check_world(assets: &[WorldJsonlAsset]) -> Result<(), Vec<String>> {
102 let mut errors: Vec<String> = Vec::new();
103
104 let mut seen_names: std::collections::HashSet<&str> = Default::default();
108 for asset in assets {
109 if !seen_names.insert(asset.name.as_str()) {
110 errors.push(format!(
111 "duplicate name '{}' after build-time expansion: a generated or \
112 injected asset collides with another; rename one of them",
113 asset.name
114 ));
115 }
116 }
117
118 let declared_vars = assets
122 .iter()
123 .find(|a| a.asset_type.to_lowercase().replace('_', "") == "variables")
124 .map(|a| behavior::DeclaredVars::from_args(&a.args))
125 .unwrap_or_default();
126
127 for asset in assets {
128 let type_norm = asset.asset_type.to_lowercase().replace('_', "");
129 let checked = if type_norm == "behavior" {
130 behavior::check_with_vars(&asset.name, &asset.args, &declared_vars)
131 } else {
132 check_authored_asset(&type_norm, &asset.name, &asset.args)
133 };
134 if let Err(e) = checked {
135 errors.push(e);
136 }
137 if let Err(e) = check_compiled_asset(&type_norm, &asset.name, &asset.args) {
138 errors.push(e);
139 }
140 }
141
142 if let Err(ref_errors) = cross_reference::validate_cross_references(assets) {
143 errors.extend(ref_errors);
144 }
145
146 shape::check_shape(assets, &mut errors);
147
148 if errors.is_empty() {
149 Ok(())
150 } else {
151 Err(errors)
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 fn asset(name: &str, asset_type: &str, args: serde_json::Value) -> WorldJsonlAsset {
160 WorldJsonlAsset {
161 name: name.to_string(),
162 asset_type: asset_type.to_string(),
163 args,
164 }
165 }
166
167 #[test]
168 fn graphics_config_with_full_render_stack_passes_graphics_rules() {
169 let assets = vec![
170 asset("gfx", "GraphicsConfig", serde_json::json!({})),
171 asset("win", "Window", serde_json::json!({})),
172 asset(
173 "scene_shader",
174 "Shader",
175 serde_json::json!({"fragment": "x.slang"}),
176 ),
177 ];
178 assert!(check_world(&assets).is_ok());
179 }
180
181 #[test]
182 fn per_asset_and_cross_reference_errors_both_collected() {
183 let assets = vec![
186 asset("bad_prop", "Prop", serde_json::json!({})),
187 asset("bad_mat", "Material", serde_json::json!({"albedo":"ghost"})),
188 ];
189 let errs = check_world(&assets).unwrap_err();
190 assert!(errs.iter().any(|e| e.contains("bad_prop")));
191 assert!(errs.iter().any(|e| e.contains("ghost")));
192 }
193
194 #[test]
198 fn composed_checks_collect_pure_and_compile_backed_errors() {
199 let assets = vec![
200 asset("bad_prop", "Prop", serde_json::json!({})),
201 asset(
202 "bad_tex",
203 "Texture",
204 serde_json::json!({"generator": "not_a_generator"}),
205 ),
206 ];
207 let errs = check_world(&assets).unwrap_err();
208 assert!(errs.iter().any(|e| e.contains("bad_prop")));
209 assert!(errs.iter().any(|e| e.contains("not_a_generator")));
210 }
211
212 #[test]
214 fn check_asset_routes_each_type_alias() {
215 for alias in ["cubemaptexture", "cubemap"] {
216 let args = serde_json::json!({"source": "studio.png"});
217 let err = check_asset(alias, "c", &args).unwrap_err();
218 assert!(err.contains("Radiance .hdr"), "{alias}: {err}");
219 }
220 for alias in ["environmentmap", "envmap", "ibl"] {
221 let args = serde_json::json!({"generator": "aurora"});
222 let err = check_asset(alias, "e", &args).unwrap_err();
223 assert!(
224 err.contains("unknown EnvironmentMap generator"),
225 "{alias}: {err}"
226 );
227 }
228 }
229
230 #[test]
231 fn check_asset_runs_both_check_sets() {
232 assert!(check_asset("prop", "p", &serde_json::json!({})).is_err());
234 assert!(
236 check_asset(
237 "texture",
238 "t",
239 &serde_json::json!({"generator": "not_a_generator"})
240 )
241 .is_err()
242 );
243 assert!(check_asset("window", "w", &serde_json::json!({})).is_ok());
245 }
246}