1pub(crate) mod animation_graph;
13pub(crate) mod asset_refs;
14pub mod audio;
15pub mod behavior;
16pub mod cross_reference;
17pub mod fault;
18pub(crate) mod instanced_prop;
19pub mod physics;
20pub mod prop;
22pub mod sdf_volume;
24pub mod shader;
26pub(crate) mod shape;
27pub mod voxel_chunk;
28pub(crate) mod voxel_world;
29
30use crate::world::WorldJsonlAsset;
31
32pub(crate) type ExtraAssetCheck<'a> =
36 &'a dyn Fn(&str, &str, &serde_json::Value) -> Result<(), String>;
37
38pub fn report_validation_errors(errors: &[String]) -> std::io::Error {
42 for e in errors {
43 eprintln!("error: {}", e);
44 }
45 eprintln!("\nvalidation failed ({} error(s))", errors.len());
46 std::io::Error::new(
47 std::io::ErrorKind::InvalidData,
48 format!("validation failed with {} error(s)", errors.len()),
49 )
50}
51
52pub fn check_asset(type_norm: &str, name: &str, args: &serde_json::Value) -> Result<(), String> {
56 match type_norm {
57 "animationgraph" => animation_graph::check(name, args),
58 "behavior" => behavior::check(name, args),
59 "variables" => behavior::check_variables(name, args),
60 "shader" => shader::check(name, args),
61 "prop" => prop::check(name, args),
62 "sdfvolume" | "sdf" => sdf_volume::check(name, args),
63 "voxelchunk" | "chunk" => voxel_chunk::check(name, args),
64 "voxelworld" => voxel_world::check(name, args),
65 "instancedprop" | "instanced" => instanced_prop::check(name, args),
66 "triggervolume" => physics::check(name, args),
67 "audioemitter" => audio::check_emitter(name, args),
68 "audiocue" => audio::check_cue(name, args),
69 "propbody" => audio::check_prop_body(name, args),
70 _ => Ok(()),
71 }
72}
73
74pub fn check_world_with(
79 assets: &[WorldJsonlAsset],
80 extra: ExtraAssetCheck,
81) -> Result<(), Vec<String>> {
82 let mut errors: Vec<String> = Vec::new();
83
84 let mut seen_names: std::collections::HashSet<&str> = Default::default();
88 for asset in assets {
89 if !seen_names.insert(asset.name.as_str()) {
90 errors.push(format!(
91 "duplicate name '{}' after build-time expansion: a generated or \
92 injected asset collides with another; rename one of them",
93 asset.name
94 ));
95 }
96 }
97
98 let declared_vars = assets
102 .iter()
103 .find(|a| a.asset_type.to_lowercase().replace('_', "") == "variables")
104 .map(|a| behavior::DeclaredVars::from_args(&a.args))
105 .unwrap_or_default();
106
107 for asset in assets {
108 let type_norm = asset.asset_type.to_lowercase().replace('_', "");
109 let checked = if type_norm == "behavior" {
110 behavior::check_with_vars(&asset.name, &asset.args, &declared_vars)
111 } else {
112 check_asset(&type_norm, &asset.name, &asset.args)
113 };
114 if let Err(e) = checked {
115 errors.push(e);
116 }
117 if let Err(e) = extra(&type_norm, &asset.name, &asset.args) {
118 errors.push(e);
119 }
120 }
121
122 if let Err(ref_errors) = cross_reference::validate_cross_references(assets) {
123 errors.extend(ref_errors);
124 }
125
126 shape::check_shape(assets, &mut errors);
127
128 if errors.is_empty() {
129 Ok(())
130 } else {
131 Err(errors)
132 }
133}
134
135pub fn check_world(assets: &[WorldJsonlAsset]) -> Result<(), Vec<String>> {
138 check_world_with(assets, &|_, _, _| Ok(()))
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 fn asset(name: &str, asset_type: &str, args: serde_json::Value) -> WorldJsonlAsset {
146 WorldJsonlAsset {
147 name: name.to_string(),
148 asset_type: asset_type.to_string(),
149 args,
150 }
151 }
152
153 #[test]
154 fn graphics_config_with_full_render_stack_passes_graphics_rules() {
155 let assets = vec![
156 asset("gfx", "GraphicsConfig", serde_json::json!({})),
157 asset("win", "Window", serde_json::json!({})),
158 asset(
159 "scene_shader",
160 "Shader",
161 serde_json::json!({
162 "vertex": {"sources": {"metal": "x.metal", "hlsl": "x.hlsl", "glsl": "x.glsl"}},
163 "fragment": {"sources": {"metal": "x.metal", "hlsl": "x.hlsl", "glsl": "x.glsl"}}
164 }),
165 ),
166 ];
167 assert!(check_world(&assets).is_ok());
168 }
169
170 #[test]
171 fn per_asset_and_cross_reference_errors_both_collected() {
172 let assets = vec![
175 asset("bad_prop", "Prop", serde_json::json!({})),
176 asset("bad_mat", "Material", serde_json::json!({"albedo":"ghost"})),
177 ];
178 let errs = check_world(&assets).unwrap_err();
179 assert!(errs.iter().any(|e| e.contains("bad_prop")));
180 assert!(errs.iter().any(|e| e.contains("ghost")));
181 }
182
183 #[test]
184 fn extra_checks_fold_into_the_same_error_pass() {
185 let assets = vec![asset("t", "Texture", serde_json::json!({}))];
186 let errs = check_world_with(&assets, &|type_norm, name, _args| {
187 if type_norm == "texture" {
188 Err(format!("Asset '{name}': extra check fired"))
189 } else {
190 Ok(())
191 }
192 })
193 .unwrap_err();
194 assert!(errs.iter().any(|e| e.contains("extra check fired")));
195 }
196}