1use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use std::io::Write;
16use std::path::Path;
17
18use crate::hash::fnv1a_64;
19use crate::waymark::schema::{GridConfig, SimulationConfig, SpatialConfig};
20
21pub const DREAM_MAGIC: &[u8; 8] = b"DREAMWL\0";
27
28pub const DREAM_VERSION: u32 = 1;
30
31pub const SCENE_SCHEMA_VERSION: &str = "dreamwell_scene_v1.0.0";
33
34pub const TAPESTRY_SCHEMA_VERSION: &str = "dreamwell_tapestry_v1.0.0";
36
37pub const FLAG_COMPRESSED: u32 = 1 << 0;
39pub const FLAG_SIGNED: u32 = 1 << 1;
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct DreamSceneV1 {
49 pub scene_id: String,
51 pub name: String,
52 #[serde(default)]
53 pub description: String,
54 #[serde(default = "default_scene_schema")]
55 pub schema_version: String,
56
57 #[serde(default = "default_topology_layer")]
59 pub topology_layer: u8,
60 #[serde(default)]
61 pub starting_zone_id: String,
62 #[serde(default)]
63 pub world_id: Option<String>,
64
65 #[serde(default = "default_camera")]
67 pub default_camera: String,
68 #[serde(default)]
69 pub allowed_cameras: Vec<String>,
70
71 #[serde(default)]
73 pub objects: Vec<SceneObject>,
74
75 #[serde(default)]
77 pub directional_lights: Vec<DirectionalLightDef>,
78 #[serde(default)]
79 pub point_lights: Vec<PointLightDef>,
80 #[serde(default)]
81 pub spot_lights: Vec<SpotLightDef>,
82
83 #[serde(default)]
85 pub colliders: Vec<ColliderDef>,
86 #[serde(default)]
87 pub physics_defaults: PhysicsDefaults,
88
89 #[serde(default)]
91 pub pois: Vec<PoiDef>,
92
93 #[serde(default)]
95 pub asset_refs: Vec<AssetRef>,
96
97 #[serde(default = "default_render_path")]
99 pub render_path: String,
100 #[serde(default)]
101 pub quality_preset: Option<String>,
102 #[serde(default)]
103 pub feature_flags: FeatureFlags,
104
105 #[serde(default)]
107 pub waymark_pack: Option<serde_json::Value>,
108
109 #[serde(default)]
111 pub grid: GridConfig,
112 #[serde(default)]
113 pub spatial: SpatialConfig,
114 #[serde(default)]
115 pub simulation: SimulationConfig,
116}
117
118fn default_scene_schema() -> String {
119 SCENE_SCHEMA_VERSION.to_string()
120}
121fn default_topology_layer() -> u8 {
122 6 }
124fn default_camera() -> String {
125 "ThirdPerson".to_string()
126}
127fn default_render_path() -> String {
128 "Dreamwell".to_string()
129}
130
131impl Default for DreamSceneV1 {
132 fn default() -> Self {
133 Self {
134 scene_id: String::new(),
135 name: String::new(),
136 description: String::new(),
137 schema_version: default_scene_schema(),
138 topology_layer: default_topology_layer(),
139 starting_zone_id: String::new(),
140 world_id: None,
141 default_camera: default_camera(),
142 allowed_cameras: Vec::new(),
143 objects: Vec::new(),
144 directional_lights: Vec::new(),
145 point_lights: Vec::new(),
146 spot_lights: Vec::new(),
147 colliders: Vec::new(),
148 physics_defaults: PhysicsDefaults::default(),
149 pois: Vec::new(),
150 asset_refs: Vec::new(),
151 render_path: default_render_path(),
152 quality_preset: None,
153 feature_flags: FeatureFlags::default(),
154 waymark_pack: None,
155 grid: GridConfig::default(),
156 spatial: SpatialConfig::default(),
157 simulation: SimulationConfig::default(),
158 }
159 }
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct SceneObject {
168 pub id: String,
169 #[serde(default)]
170 pub name: String,
171 #[serde(default = "default_kind")]
173 pub kind: String,
174 #[serde(default)]
175 pub position: [f32; 3],
176 #[serde(default)]
178 pub rotation: [f32; 3],
179 #[serde(default = "default_scale")]
180 pub scale: [f32; 3],
181 #[serde(default)]
182 pub parent_id: Option<String>,
183 #[serde(default)]
185 pub asset_ref: Option<String>,
186 #[serde(default)]
187 pub material: Option<MaterialDef>,
188 #[serde(default)]
190 pub primitive: Option<String>,
191 #[serde(default)]
193 pub properties: HashMap<String, serde_json::Value>,
194}
195
196fn default_kind() -> String {
197 "Mesh".to_string()
198}
199fn default_scale() -> [f32; 3] {
200 [1.0, 1.0, 1.0]
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct DirectionalLightDef {
209 #[serde(default)]
210 pub id: String,
211 pub direction: [f32; 3],
212 #[serde(default = "default_light_color")]
213 pub color: [f32; 3],
214 #[serde(default = "default_one")]
215 pub intensity: f32,
216 #[serde(default)]
217 pub cast_shadows: bool,
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct PointLightDef {
222 #[serde(default)]
223 pub id: String,
224 pub position: [f32; 3],
225 #[serde(default = "default_light_color")]
226 pub color: [f32; 3],
227 #[serde(default = "default_one")]
228 pub intensity: f32,
229 #[serde(default = "default_light_range")]
230 pub range: f32,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct SpotLightDef {
235 #[serde(default)]
236 pub id: String,
237 pub position: [f32; 3],
238 pub direction: [f32; 3],
239 #[serde(default = "default_light_color")]
240 pub color: [f32; 3],
241 #[serde(default = "default_one")]
242 pub intensity: f32,
243 #[serde(default = "default_light_range")]
244 pub range: f32,
245 #[serde(default = "default_inner_cone")]
247 pub inner_cone_degrees: f32,
248 #[serde(default = "default_outer_cone")]
250 pub outer_cone_degrees: f32,
251}
252
253fn default_light_color() -> [f32; 3] {
254 [1.0, 1.0, 1.0]
255}
256fn default_one() -> f32 {
257 1.0
258}
259fn default_light_range() -> f32 {
260 20.0
261}
262fn default_inner_cone() -> f32 {
263 30.0
264}
265fn default_outer_cone() -> f32 {
266 45.0
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize)]
274pub struct ColliderDef {
275 #[serde(default)]
276 pub id: String,
277 pub shape: String,
279 #[serde(default)]
280 pub position: [f32; 3],
281 #[serde(default)]
283 pub dimensions: HashMap<String, f32>,
284 #[serde(default = "default_one")]
285 pub restitution: f32,
286 #[serde(default = "default_friction")]
287 pub friction: f32,
288 #[serde(default)]
289 pub is_static: bool,
290}
291
292fn default_friction() -> f32 {
293 0.5
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize)]
297pub struct PhysicsDefaults {
298 #[serde(default = "default_gravity")]
299 pub gravity: [f32; 3],
300 #[serde(default = "default_friction")]
301 pub friction: f32,
302 #[serde(default = "default_one")]
303 pub atmosphere_density: f32,
304}
305
306fn default_gravity() -> [f32; 3] {
307 [0.0, -9.81, 0.0]
308}
309
310impl Default for PhysicsDefaults {
311 fn default() -> Self {
312 Self {
313 gravity: default_gravity(),
314 friction: default_friction(),
315 atmosphere_density: 1.0,
316 }
317 }
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct MaterialDef {
326 #[serde(default = "default_base_color")]
327 pub base_color: [f32; 4],
328 #[serde(default = "default_roughness")]
329 pub roughness: f32,
330 #[serde(default)]
331 pub metallic: f32,
332 #[serde(default)]
333 pub emissive: [f32; 3],
334 #[serde(default = "default_one")]
335 pub emissive_strength: f32,
336 #[serde(default)]
337 pub double_sided: bool,
338}
339
340fn default_base_color() -> [f32; 4] {
341 [0.8, 0.8, 0.8, 1.0]
342}
343fn default_roughness() -> f32 {
344 0.5
345}
346
347#[derive(Debug, Clone, Serialize, Deserialize)]
352pub struct PoiDef {
353 pub id: String,
354 pub position: [f32; 3],
355 #[serde(default = "default_poi_radius")]
356 pub interaction_radius: f32,
357 #[serde(default)]
358 pub property_tag: String,
359 #[serde(default)]
360 pub animation_binding: Option<String>,
361 #[serde(default)]
362 pub transition_target: Option<TransitionTarget>,
363}
364
365fn default_poi_radius() -> f32 {
366 2.0
367}
368
369#[derive(Debug, Clone, Serialize, Deserialize)]
370pub struct TransitionTarget {
371 pub zone_id: String,
372 #[serde(default)]
373 pub layer: Option<String>,
374 #[serde(default)]
375 pub camera_profile: Option<String>,
376}
377
378#[derive(Debug, Clone, Serialize, Deserialize)]
383pub struct AssetRef {
384 pub key: String,
386 pub path: String,
388 #[serde(default = "default_asset_kind")]
390 pub kind: String,
391}
392
393fn default_asset_kind() -> String {
394 "gltf".to_string()
395}
396
397#[derive(Debug, Clone, Serialize, Deserialize)]
402pub struct FeatureFlags {
403 #[serde(default)]
404 pub quantum_culling: bool,
405 #[serde(default)]
406 pub rtx_gi: bool,
407 #[serde(default)]
408 pub rtx_shadows: bool,
409 #[serde(default)]
410 pub ssao: bool,
411 #[serde(default)]
412 pub ssr: bool,
413 #[serde(default)]
414 pub ssgi: bool,
415 #[serde(default)]
416 pub taa: bool,
417 #[serde(default)]
418 pub volumetric_fog: bool,
419 #[serde(default)]
420 pub dof: bool,
421 #[serde(default)]
422 pub bloom: bool,
423 #[serde(default)]
424 pub motion_vectors: bool,
425 #[serde(default)]
426 pub hiz_culling: bool,
427 #[serde(default)]
428 pub dream_tsr: bool,
429 #[serde(default)]
430 pub dreamphysics: bool,
431 #[serde(default)]
432 pub dreammatter: bool,
433 #[serde(default)]
434 pub procedural_terrain: bool,
435 #[serde(default)]
436 pub fbx_avatar: bool,
437}
438
439impl Default for FeatureFlags {
440 fn default() -> Self {
441 Self {
442 quantum_culling: true,
443 rtx_gi: false,
444 rtx_shadows: false,
445 ssao: true,
446 ssr: true,
447 ssgi: true,
448 taa: true,
449 volumetric_fog: false,
450 dof: false,
451 bloom: true,
452 motion_vectors: true,
453 hiz_culling: true,
454 dream_tsr: false,
455 dreamphysics: true,
456 dreammatter: true,
457 procedural_terrain: false,
458 fbx_avatar: false,
459 }
460 }
461}
462
463#[derive(Debug, Clone, Serialize, Deserialize)]
470pub struct TapestryV1 {
471 #[serde(default = "default_tapestry_schema")]
472 pub schema_version: String,
473 pub project_name: String,
474 pub project_id: String,
475 #[serde(default = "default_engine_version")]
476 pub engine_version: String,
477
478 #[serde(default)]
480 pub scenes: Vec<SceneEntry>,
481
482 #[serde(default)]
484 pub starting_scene: String,
485
486 #[serde(default)]
488 pub asset_roots: Vec<String>,
489
490 #[serde(default)]
492 pub waymark_packs: Vec<String>,
493
494 #[serde(default)]
496 pub profiles: Vec<BuildProfile>,
497
498 #[serde(default)]
500 pub simulation: Option<SimulationConfig>,
501}
502
503fn default_tapestry_schema() -> String {
504 TAPESTRY_SCHEMA_VERSION.to_string()
505}
506fn default_engine_version() -> String {
507 "1.0.0".to_string()
508}
509
510#[derive(Debug, Clone, Serialize, Deserialize)]
512pub struct SceneEntry {
513 pub scene_id: String,
514 pub name: String,
515 pub path: String,
517 #[serde(default)]
518 pub topology_layer: String,
519 #[serde(default)]
520 pub tags: Vec<String>,
521}
522
523#[derive(Debug, Clone, Serialize, Deserialize)]
525pub struct BuildProfile {
526 pub id: String,
527 pub scene_id: String,
528 #[serde(default = "default_render_path")]
529 pub render_path: String,
530 #[serde(default)]
531 pub quality_preset: Option<String>,
532 #[serde(default)]
533 pub cli_args: Vec<String>,
534}
535
536pub fn write_dream_file(path: &Path, scene: &DreamSceneV1) -> Result<(), String> {
545 let content = rmp_serde::to_vec(scene).map_err(|e| format!("dream_serialize:{e}"))?;
546 let hash = fnv1a_64(&content);
547 let mut file = std::fs::File::create(path).map_err(|e| format!("dream_create:{e}"))?;
548 file.write_all(DREAM_MAGIC).map_err(|e| format!("dream_write:{e}"))?;
549 file.write_all(&DREAM_VERSION.to_le_bytes())
550 .map_err(|e| format!("dream_write:{e}"))?;
551 file.write_all(&0u32.to_le_bytes())
552 .map_err(|e| format!("dream_write:{e}"))?;
553 file.write_all(&(content.len() as u64).to_le_bytes())
554 .map_err(|e| format!("dream_write:{e}"))?;
555 file.write_all(&hash.to_le_bytes())
556 .map_err(|e| format!("dream_write:{e}"))?;
557 file.write_all(&content).map_err(|e| format!("dream_write:{e}"))?;
558 Ok(())
559}
560
561pub fn read_dream_file(path: &Path) -> Result<DreamSceneV1, String> {
565 let data = std::fs::read(path).map_err(|e| format!("dream_read:{e}"))?;
566 deserialize_dream_scene(&data)
567}
568
569pub fn deserialize_dream_scene(data: &[u8]) -> Result<DreamSceneV1, String> {
571 if data.len() < 32 {
572 return Err("dream_validate:file too small (< 32 bytes)".into());
573 }
574 if &data[0..8] != DREAM_MAGIC {
575 return Err("dream_validate:invalid magic bytes (expected DREAMWL\\0)".into());
576 }
577 let version = u32::from_le_bytes(data[8..12].try_into().map_err(|_| "dream_validate:bad version bytes")?);
578 if version != DREAM_VERSION {
579 return Err(format!(
580 "dream_validate:unsupported version {version} (expected {DREAM_VERSION})"
581 ));
582 }
583 let content_len = u64::from_le_bytes(
584 data[16..24]
585 .try_into()
586 .map_err(|_| "dream_validate:bad content_len bytes")?,
587 ) as usize;
588 let expected_hash = u64::from_le_bytes(data[24..32].try_into().map_err(|_| "dream_validate:bad hash bytes")?);
589 if data.len() < 32 + content_len {
590 return Err(format!(
591 "dream_validate:content truncated (expected {content_len} bytes, got {})",
592 data.len() - 32
593 ));
594 }
595 let content = &data[32..32 + content_len];
596 let actual_hash = fnv1a_64(content);
597 if actual_hash != expected_hash {
598 return Err(format!(
599 "dream_validate:attestation hash mismatch (expected {expected_hash:#x}, got {actual_hash:#x})"
600 ));
601 }
602 rmp_serde::from_slice(content).map_err(|e| format!("dream_deserialize:{e}"))
603}
604
605pub fn write_tapestry(path: &Path, tapestry: &TapestryV1) -> Result<(), String> {
607 let content = rmp_serde::to_vec(tapestry).map_err(|e| format!("tapestry_serialize:{e}"))?;
608 let hash = fnv1a_64(&content);
609 let mut file = std::fs::File::create(path).map_err(|e| format!("tapestry_create:{e}"))?;
610 file.write_all(DREAM_MAGIC).map_err(|e| format!("tapestry_write:{e}"))?;
611 file.write_all(&DREAM_VERSION.to_le_bytes())
612 .map_err(|e| format!("tapestry_write:{e}"))?;
613 file.write_all(&FLAG_SIGNED.to_le_bytes())
614 .map_err(|e| format!("tapestry_write:{e}"))?;
615 file.write_all(&(content.len() as u64).to_le_bytes())
616 .map_err(|e| format!("tapestry_write:{e}"))?;
617 file.write_all(&hash.to_le_bytes())
618 .map_err(|e| format!("tapestry_write:{e}"))?;
619 file.write_all(&content).map_err(|e| format!("tapestry_write:{e}"))?;
620 Ok(())
621}
622
623pub fn read_tapestry(path: &Path) -> Result<TapestryV1, String> {
625 let data = std::fs::read(path).map_err(|e| format!("tapestry_read:{e}"))?;
626 if data.len() < 32 {
627 return Err("tapestry_validate:file too small".into());
628 }
629 if &data[0..8] != DREAM_MAGIC {
630 return Err("tapestry_validate:invalid magic bytes".into());
631 }
632 let version = u32::from_le_bytes(data[8..12].try_into().map_err(|_| "tapestry_validate:bad version")?);
633 if version != DREAM_VERSION {
634 return Err(format!("tapestry_validate:unsupported version {version}"));
635 }
636 let content_len = u64::from_le_bytes(
637 data[16..24]
638 .try_into()
639 .map_err(|_| "tapestry_validate:bad content_len")?,
640 ) as usize;
641 let expected_hash = u64::from_le_bytes(data[24..32].try_into().map_err(|_| "tapestry_validate:bad hash")?);
642 if data.len() < 32 + content_len {
643 return Err("tapestry_validate:content truncated".into());
644 }
645 let content = &data[32..32 + content_len];
646 let actual_hash = fnv1a_64(content);
647 if actual_hash != expected_hash {
648 return Err("tapestry_validate:attestation hash mismatch".into());
649 }
650 rmp_serde::from_slice(content).map_err(|e| format!("tapestry_deserialize:{e}"))
651}
652
653pub fn compile_scene_json(scene_json: &str) -> Result<DreamSceneV1, String> {
662 serde_json::from_str(scene_json).map_err(|e| format!("scene_compile:{e}"))
663}
664
665pub fn compile_tapestry_json(tapestry_json: &str) -> Result<TapestryV1, String> {
667 serde_json::from_str(tapestry_json).map_err(|e| format!("tapestry_compile:{e}"))
668}
669
670pub fn compile_scene_dir(scene_dir: &Path) -> Result<(), String> {
674 let json_path = scene_dir.join("scene.json");
675 let dream_path = scene_dir.join("scene.dream");
676 let json_str = std::fs::read_to_string(&json_path).map_err(|e| format!("scene_read:{json_path:?}:{e}"))?;
677 let scene = compile_scene_json(&json_str)?;
678 write_dream_file(&dream_path, &scene)?;
679 log::info!("Compiled {} → {}", json_path.display(), dream_path.display());
680 Ok(())
681}
682
683pub fn compile_project(project_dir: &Path) -> Result<(), String> {
685 let tapestry_json_path = project_dir.join("tapestry.json");
687 if tapestry_json_path.exists() {
688 let json_str = std::fs::read_to_string(&tapestry_json_path).map_err(|e| format!("tapestry_read:{e}"))?;
689 let tapestry = compile_tapestry_json(&json_str)?;
690 write_tapestry(&project_dir.join("tapestry.dream"), &tapestry)?;
691 log::info!("Compiled tapestry.dream");
692 }
693
694 let scenes_dir = project_dir.join("scenes");
696 if scenes_dir.is_dir() {
697 let entries = std::fs::read_dir(&scenes_dir).map_err(|e| format!("scenes_scan:{e}"))?;
698 for entry in entries.flatten() {
699 if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
700 let scene_json = entry.path().join("scene.json");
701 if scene_json.exists() {
702 compile_scene_dir(&entry.path())?;
703 }
704 }
705 }
706 }
707
708 Ok(())
709}
710
711pub fn reality_check(scene: &DreamSceneV1) -> (Vec<String>, Vec<String>) {
719 let mut warnings = Vec::new();
720 let mut errors = Vec::new();
721
722 if scene.scene_id.is_empty() {
724 errors.push("reality_check:scene_id is empty".into());
725 }
726 if scene.name.is_empty() {
727 warnings.push("reality_check:scene name is empty".into());
728 }
729 if scene.schema_version != SCENE_SCHEMA_VERSION {
730 warnings.push(format!(
731 "reality_check:schema_version mismatch (got '{}', expected '{SCENE_SCHEMA_VERSION}')",
732 scene.schema_version
733 ));
734 }
735
736 if scene.topology_layer > 9 {
738 errors.push(format!("reality_check:topology_layer {} > 9", scene.topology_layer));
739 }
740
741 let mut obj_ids = std::collections::HashSet::new();
743 for obj in &scene.objects {
744 if obj.id.is_empty() {
745 errors.push("reality_check:object with empty id".into());
746 }
747 if !obj_ids.insert(&obj.id) {
748 errors.push(format!("reality_check:duplicate object id '{}'", obj.id));
749 }
750 for &v in &obj.position {
752 if !v.is_finite() {
753 errors.push(format!("reality_check:object '{}' has non-finite position", obj.id));
754 break;
755 }
756 }
757 if let Some(ref key) = obj.asset_ref {
759 if !scene.asset_refs.iter().any(|a| &a.key == key) {
760 errors.push(format!(
761 "reality_check:object '{}' references unknown asset '{key}'",
762 obj.id
763 ));
764 }
765 }
766 }
767
768 for light in &scene.directional_lights {
770 let len_sq: f32 = light.direction.iter().map(|v| v * v).sum();
771 if len_sq < 0.001 {
772 warnings.push(format!(
773 "reality_check:directional light '{}' has near-zero direction",
774 light.id
775 ));
776 }
777 }
778
779 for poi in &scene.pois {
781 if poi.interaction_radius <= 0.0 {
782 errors.push(format!("reality_check:POI '{}' has non-positive radius", poi.id));
783 }
784 }
785
786 (warnings, errors)
787}
788
789#[cfg(test)]
794mod tests {
795 use super::*;
796
797 fn sample_scene() -> DreamSceneV1 {
798 DreamSceneV1 {
799 scene_id: "test-scene".into(),
800 name: "Test Scene".into(),
801 description: "Unit test scene".into(),
802 objects: vec![SceneObject {
803 id: "ground".into(),
804 name: "Ground Plane".into(),
805 kind: "Mesh".into(),
806 position: [0.0, 0.0, 0.0],
807 rotation: [0.0, 0.0, 0.0],
808 scale: [80.0, 1.0, 80.0],
809 parent_id: None,
810 asset_ref: None,
811 material: Some(MaterialDef {
812 base_color: [0.3, 0.5, 0.2, 1.0],
813 roughness: 0.8,
814 metallic: 0.0,
815 emissive: [0.0, 0.0, 0.0],
816 emissive_strength: 0.0,
817 double_sided: false,
818 }),
819 primitive: Some("Plane".into()),
820 properties: HashMap::new(),
821 }],
822 directional_lights: vec![DirectionalLightDef {
823 id: "sun".into(),
824 direction: [0.4, -0.7, 0.3],
825 color: [1.0, 0.95, 0.85],
826 intensity: 1.2,
827 cast_shadows: true,
828 }],
829 point_lights: vec![PointLightDef {
830 id: "fill".into(),
831 position: [5.0, 3.0, 0.0],
832 color: [0.6, 0.8, 1.0],
833 intensity: 0.5,
834 range: 15.0,
835 }],
836 pois: vec![PoiDef {
837 id: "demo-poi".into(),
838 position: [8.0, 0.0, 0.0],
839 interaction_radius: 2.0,
840 property_tag: "interact.demo.spawn".into(),
841 animation_binding: None,
842 transition_target: None,
843 }],
844 ..Default::default()
845 }
846 }
847
848 fn sample_tapestry() -> TapestryV1 {
849 TapestryV1 {
850 schema_version: TAPESTRY_SCHEMA_VERSION.into(),
851 project_name: "Test Project".into(),
852 project_id: "test-project".into(),
853 engine_version: "1.0.0".into(),
854 scenes: vec![SceneEntry {
855 scene_id: "test-scene".into(),
856 name: "Test Scene".into(),
857 path: "scenes/test/scene.dream".into(),
858 topology_layer: "Area".into(),
859 tags: vec!["test".into()],
860 }],
861 starting_scene: "test-scene".into(),
862 asset_roots: vec!["assets/dreamwell".into()],
863 waymark_packs: vec!["waymark/".into()],
864 profiles: vec![BuildProfile {
865 id: "default".into(),
866 scene_id: "test-scene".into(),
867 render_path: "Dreamwell".into(),
868 quality_preset: None,
869 cli_args: vec!["--dream".into()],
870 }],
871 simulation: None,
872 }
873 }
874
875 #[test]
876 fn dream_file_roundtrip() {
877 let scene = sample_scene();
878 let dir = std::env::temp_dir().join("dreamwell_test_roundtrip");
879 std::fs::create_dir_all(&dir).unwrap();
880 let path = dir.join("test.dream");
881
882 write_dream_file(&path, &scene).unwrap();
883 let loaded = read_dream_file(&path).unwrap();
884
885 assert_eq!(scene.scene_id, loaded.scene_id);
886 assert_eq!(scene.name, loaded.name);
887 assert_eq!(scene.objects.len(), loaded.objects.len());
888 assert_eq!(scene.directional_lights.len(), loaded.directional_lights.len());
889 assert_eq!(scene.point_lights.len(), loaded.point_lights.len());
890 assert_eq!(scene.pois.len(), loaded.pois.len());
891 assert_eq!(scene.objects[0].id, loaded.objects[0].id);
892 assert_eq!(scene.objects[0].scale, loaded.objects[0].scale);
893
894 std::fs::remove_dir_all(&dir).ok();
895 }
896
897 #[test]
898 fn dream_file_rejects_bad_magic() {
899 let mut data = vec![0u8; 64];
900 data[0..8].copy_from_slice(b"NOTDREAM");
901 let result = deserialize_dream_scene(&data);
902 assert!(result.is_err());
903 assert!(result.unwrap_err().contains("invalid magic bytes"));
904 }
905
906 #[test]
907 fn dream_file_rejects_bad_version() {
908 let scene = sample_scene();
909 let content = rmp_serde::to_vec(&scene).unwrap();
910 let hash = fnv1a_64(&content);
911 let mut data = Vec::new();
912 data.extend_from_slice(DREAM_MAGIC);
913 data.extend_from_slice(&99u32.to_le_bytes()); data.extend_from_slice(&0u32.to_le_bytes());
915 data.extend_from_slice(&(content.len() as u64).to_le_bytes());
916 data.extend_from_slice(&hash.to_le_bytes());
917 data.extend_from_slice(&content);
918
919 let result = deserialize_dream_scene(&data);
920 assert!(result.is_err());
921 assert!(result.unwrap_err().contains("unsupported version 99"));
922 }
923
924 #[test]
925 fn dream_file_rejects_bad_hash() {
926 let scene = sample_scene();
927 let content = rmp_serde::to_vec(&scene).unwrap();
928 let mut data = Vec::new();
929 data.extend_from_slice(DREAM_MAGIC);
930 data.extend_from_slice(&DREAM_VERSION.to_le_bytes());
931 data.extend_from_slice(&0u32.to_le_bytes());
932 data.extend_from_slice(&(content.len() as u64).to_le_bytes());
933 data.extend_from_slice(&0xDEADBEEFu64.to_le_bytes()); data.extend_from_slice(&content);
935
936 let result = deserialize_dream_scene(&data);
937 assert!(result.is_err());
938 assert!(result.unwrap_err().contains("attestation hash mismatch"));
939 }
940
941 #[test]
942 fn dream_file_rejects_too_small() {
943 let result = deserialize_dream_scene(&[0u8; 16]);
944 assert!(result.is_err());
945 assert!(result.unwrap_err().contains("too small"));
946 }
947
948 #[test]
949 fn tapestry_roundtrip() {
950 let tapestry = sample_tapestry();
951 let dir = std::env::temp_dir().join("dreamwell_test_tapestry");
952 std::fs::create_dir_all(&dir).unwrap();
953 let path = dir.join("tapestry.dream");
954
955 write_tapestry(&path, &tapestry).unwrap();
956 let loaded = read_tapestry(&path).unwrap();
957
958 assert_eq!(tapestry.project_name, loaded.project_name);
959 assert_eq!(tapestry.project_id, loaded.project_id);
960 assert_eq!(tapestry.scenes.len(), loaded.scenes.len());
961 assert_eq!(tapestry.profiles.len(), loaded.profiles.len());
962 assert_eq!(tapestry.starting_scene, loaded.starting_scene);
963
964 std::fs::remove_dir_all(&dir).ok();
965 }
966
967 #[test]
968 fn scene_json_compile() {
969 let scene = sample_scene();
970 let json = serde_json::to_string_pretty(&scene).unwrap();
971 let compiled = compile_scene_json(&json).unwrap();
972 assert_eq!(compiled.scene_id, "test-scene");
973 assert_eq!(compiled.objects.len(), 1);
974 }
975
976 #[test]
977 fn tapestry_json_compile() {
978 let tapestry = sample_tapestry();
979 let json = serde_json::to_string_pretty(&tapestry).unwrap();
980 let compiled = compile_tapestry_json(&json).unwrap();
981 assert_eq!(compiled.project_id, "test-project");
982 assert_eq!(compiled.scenes.len(), 1);
983 }
984
985 #[test]
986 fn reality_check_valid_scene() {
987 let scene = sample_scene();
988 let (warnings, errors) = reality_check(&scene);
989 assert!(errors.is_empty(), "Unexpected errors: {errors:?}");
990 assert!(warnings.is_empty(), "Unexpected warnings: {warnings:?}");
991 }
992
993 #[test]
994 fn reality_check_catches_empty_id() {
995 let mut scene = sample_scene();
996 scene.scene_id = String::new();
997 let (_, errors) = reality_check(&scene);
998 assert!(errors.iter().any(|e| e.contains("scene_id is empty")));
999 }
1000
1001 #[test]
1002 fn reality_check_catches_duplicate_object_ids() {
1003 let mut scene = sample_scene();
1004 scene.objects.push(scene.objects[0].clone());
1005 let (_, errors) = reality_check(&scene);
1006 assert!(errors.iter().any(|e| e.contains("duplicate object id")));
1007 }
1008
1009 #[test]
1010 fn reality_check_catches_bad_asset_ref() {
1011 let mut scene = sample_scene();
1012 scene.objects[0].asset_ref = Some("nonexistent".into());
1013 let (_, errors) = reality_check(&scene);
1014 assert!(errors.iter().any(|e| e.contains("unknown asset")));
1015 }
1016
1017 #[test]
1018 fn reality_check_catches_bad_topology() {
1019 let mut scene = sample_scene();
1020 scene.topology_layer = 15;
1021 let (_, errors) = reality_check(&scene);
1022 assert!(errors.iter().any(|e| e.contains("topology_layer 15 > 9")));
1023 }
1024
1025 #[test]
1026 fn default_feature_flags() {
1027 let flags = FeatureFlags::default();
1028 assert!(flags.quantum_culling);
1029 assert!(flags.ssao);
1030 assert!(flags.bloom);
1031 assert!(!flags.rtx_gi);
1032 assert!(!flags.rtx_shadows);
1033 }
1034
1035 #[test]
1036 fn default_physics() {
1037 let phys = PhysicsDefaults::default();
1038 assert_eq!(phys.gravity, [0.0, -9.81, 0.0]);
1039 }
1040
1041 #[test]
1042 fn compile_keynote_scene_from_project() {
1043 let project_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1045 .parent()
1046 .unwrap()
1047 .parent()
1048 .unwrap()
1049 .join("dreamwell-benchmark-project");
1050 let scene_json_path = project_root.join("scenes").join("keynote").join("scene.json");
1051 if !scene_json_path.exists() {
1052 return;
1054 }
1055 let json_str = std::fs::read_to_string(&scene_json_path).unwrap();
1056 let scene = compile_scene_json(&json_str).unwrap();
1057
1058 assert_eq!(scene.scene_id, "keynote");
1059 assert_eq!(scene.name, "Dreamwell Keynote Benchmark");
1060 assert_eq!(scene.topology_layer, 6); assert!(
1062 scene.objects.len() >= 15,
1063 "Expected 15+ objects, got {}",
1064 scene.objects.len()
1065 );
1066 assert_eq!(scene.directional_lights.len(), 2);
1067 assert_eq!(scene.point_lights.len(), 8);
1068 assert!(scene.colliders.len() >= 5);
1069 assert_eq!(scene.render_path, "Dreamwell");
1070 assert!(scene.feature_flags.quantum_culling);
1071 assert!(scene.feature_flags.bloom);
1072 assert!(scene.feature_flags.dreammatter);
1073
1074 let (warnings, errors) = reality_check(&scene);
1076 assert!(errors.is_empty(), "Keynote scene has errors: {errors:?}");
1077 for w in &warnings {
1078 eprintln!(" warning: {w}");
1079 }
1080
1081 let dir = std::env::temp_dir().join("dreamwell_keynote_compile_test");
1083 std::fs::create_dir_all(&dir).unwrap();
1084 let dream_path = dir.join("keynote.dream");
1085 write_dream_file(&dream_path, &scene).unwrap();
1086 let reloaded = read_dream_file(&dream_path).unwrap();
1087 assert_eq!(reloaded.scene_id, "keynote");
1088 assert_eq!(reloaded.objects.len(), scene.objects.len());
1089 assert_eq!(reloaded.directional_lights.len(), 2);
1090 assert_eq!(reloaded.point_lights.len(), 8);
1091 std::fs::remove_dir_all(&dir).ok();
1092 }
1093
1094 #[test]
1095 fn compile_all_scenes_from_project() {
1096 let project_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1097 .parent()
1098 .unwrap()
1099 .parent()
1100 .unwrap()
1101 .join("dreamwell-benchmark-project");
1102 let scenes_dir = project_root.join("scenes");
1103 if !scenes_dir.exists() {
1104 return;
1105 }
1106
1107 let expected = [
1108 "keynote",
1109 "spawn_test",
1110 "micro_dreamlet",
1111 "avatar_demo",
1112 "ray_tracing_demo",
1113 "ray_tracing_gallery",
1114 "stress_test",
1115 ];
1116 let mut compiled = 0;
1117 for scene_name in &expected {
1118 let json_path = scenes_dir.join(scene_name).join("scene.json");
1119 if !json_path.exists() {
1120 continue;
1121 }
1122 let json_str = std::fs::read_to_string(&json_path)
1123 .unwrap_or_else(|e| panic!("Failed to read {scene_name}/scene.json: {e}"));
1124 let scene = compile_scene_json(&json_str).unwrap_or_else(|e| panic!("Failed to compile {scene_name}: {e}"));
1125 assert_eq!(scene.scene_id, *scene_name, "scene_id mismatch for {scene_name}");
1126 assert!(!scene.name.is_empty(), "empty name for {scene_name}");
1127
1128 let (warnings, errors) = reality_check(&scene);
1129 assert!(errors.is_empty(), "{scene_name} reality check errors: {errors:?}");
1130
1131 let dir = std::env::temp_dir().join(format!("dreamwell_compile_{scene_name}"));
1133 std::fs::create_dir_all(&dir).unwrap();
1134 let dream_path = dir.join("scene.dream");
1135 write_dream_file(&dream_path, &scene).unwrap();
1136 let reloaded = read_dream_file(&dream_path).unwrap();
1137 assert_eq!(reloaded.scene_id, scene.scene_id);
1138 assert_eq!(reloaded.objects.len(), scene.objects.len());
1139 std::fs::remove_dir_all(&dir).ok();
1140
1141 compiled += 1;
1142 for w in &warnings {
1143 eprintln!(" {scene_name} warning: {w}");
1144 }
1145 }
1146 assert!(compiled >= 5, "Expected to compile at least 5 scenes, got {compiled}");
1147 }
1148
1149 #[test]
1150 fn compile_tapestry_from_project() {
1151 let project_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1152 .parent()
1153 .unwrap()
1154 .parent()
1155 .unwrap()
1156 .join("dreamwell-benchmark-project");
1157 let tapestry_json_path = project_root.join("tapestry.json");
1158 if !tapestry_json_path.exists() {
1159 return;
1160 }
1161 let json_str = std::fs::read_to_string(&tapestry_json_path).unwrap();
1162 let tapestry = compile_tapestry_json(&json_str).unwrap();
1163
1164 assert_eq!(tapestry.project_id, "dreamwell-benchmarks");
1165 assert_eq!(tapestry.starting_scene, "keynote");
1166 assert!(
1167 tapestry.scenes.len() >= 6,
1168 "Expected 6+ scenes, got {}",
1169 tapestry.scenes.len()
1170 );
1171 assert!(
1172 tapestry.profiles.len() >= 6,
1173 "Expected 6+ profiles, got {}",
1174 tapestry.profiles.len()
1175 );
1176
1177 for profile in &tapestry.profiles {
1179 assert!(
1180 tapestry.scenes.iter().any(|s| s.scene_id == profile.scene_id),
1181 "Profile '{}' references unknown scene '{}'",
1182 profile.id,
1183 profile.scene_id
1184 );
1185 }
1186
1187 let dir = std::env::temp_dir().join("dreamwell_tapestry_compile_test");
1189 std::fs::create_dir_all(&dir).unwrap();
1190 let dream_path = dir.join("tapestry.dream");
1191 write_tapestry(&dream_path, &tapestry).unwrap();
1192 let reloaded = read_tapestry(&dream_path).unwrap();
1193 assert_eq!(reloaded.project_id, "dreamwell-benchmarks");
1194 assert_eq!(reloaded.scenes.len(), tapestry.scenes.len());
1195 std::fs::remove_dir_all(&dir).ok();
1196 }
1197}