//! A blockstate-driven inspection gallery, separate from gameplay simulation. pub mod catalog; mod cube; use crate::{Result, sprites::Sprites}; pub use catalog::StateModels; use catalog::descriptor; pub use cube::CubeStyle; use horizon_core::position::BlockPos; use horizon_world::registry::{BlockRegistry, BlockStateId}; use serde_json::{Value, json}; use std::path::Path; type Placement = (&'static str, &'static [(&'static str, &'static str)]); const GALLERY: [Placement; 16] = [ ( "oak_stairs", &[ ("facing", "north"), ("half", "top"), ("shape", "inner_left"), ], ), ("stone_slab", &[("type", "top")]), ("anvil", &[("facing", "east")]), ("torch", &[]), ("lantern", &[]), ("oak_fence", &[("north", "true"), ("east", "true")]), ("oak_fence_gate", &[("facing", "west"), ("open", "true")]), ("iron_bars", &[("north", "true"), ("south", "true")]), ("cobweb", &[]), ("grindstone", &[("face", "wall"), ("facing", "west")]), ("ladder", &[("facing", "west")]), ("amethyst_cluster", &[("facing", "east")]), ("stone", &[]), ("stone", &[]), ("stone", &[]), ("stone", &[]), ]; #[repr(C)] #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] pub struct ModelInstance { pub positions: [[f32; 3]; 4], pub uv: [[f32; 2]; 4], pub layer: u32, pub shade: f32, } pub struct PreparedModels { pub catalog: StateModels, pub placements: Vec<(BlockPos, BlockStateId)>, pub states: Vec, } pub fn prepare( root: &Path, registry: &BlockRegistry, sprites: &mut Sprites, ) -> Result { prepare_with_models( root, registry, sprites, &crate::model_library::ParsedModels::default(), ) } pub(crate) fn prepare_with_models( root: &Path, registry: &BlockRegistry, sprites: &mut Sprites, models: &crate::model_library::ParsedModels, ) -> Result { let facts: std::collections::BTreeMap = serde_json::from_slice(&std::fs::read(root.join("model-states.json"))?)?; let blocks: Vec<&str> = facts .keys() .filter(|name| registry.default_state(name).is_some()) .map(|name| name.strip_prefix("minecraft:").unwrap_or(name)) .collect(); let catalog = StateModels::from_facts_with_models(root, registry, &blocks, sprites, &facts, models)?; let mut placements = Vec::new(); let mut states = Vec::new(); for (index, (name, properties)) in GALLERY.iter().enumerate() { let state = registry.resolve(&descriptor(name, properties))?; let actual = registry .state(state) .ok_or("missing gallery registry state")?; let position = BlockPos::new((index % 4) as i32 * 4 - 7, 4, (index / 4) as i32 * 4 + 14); let selected = catalog.collect(state, position)?; states.push(json!({"block":actual.name,"properties":actual.properties,"state_id":state.0, "position":[position.x,position.y,position.z], "models":selected.iter().map(|(v,_)| json!({"model":v.model,"rotations":v.rotations,"uv_lock":v.uv_lock})).collect::>()})); placements.push((position, state)); } Ok(PreparedModels { catalog, placements, states, }) } pub fn instance(face: &catalog::BakedFace, position: BlockPos) -> ModelInstance { let origin = [position.x as f32, position.y as f32, position.z as f32]; ModelInstance { positions: face .quad .positions .map(|v| std::array::from_fn(|i| v[i] + origin[i])), uv: face.quad.uv, layer: face.layer, shade: cube::shade(face), } } pub fn load_visibility( root: &Path, registry: &BlockRegistry, ) -> Result { use horizon_world::{ block_render::{FaceVisibility, RenderDefinition, SkipRendering}, collision::CachedCollisionRegistry, registry::BlockTableFile, }; use std::{collections::BTreeMap, fs}; let file = BlockTableFile::open(&root.join("occlusion.nbt"), registry)?; let occlusion = CachedCollisionRegistry::from_table(file.table("occlusion")?)?; let raw: BTreeMap = serde_json::from_slice(&fs::read(root.join("render-definitions.json"))?)?; let definitions = raw .into_iter() .map(|(name, v)| { let skip = match v["rule"].as_str().ok_or("missing render rule")? { "never" => SkipRendering::Never, "same_block" => SkipRendering::SameBlock, "bars" => SkipRendering::Bars, "leaves" => SkipRendering::Leaves, "liquid" => SkipRendering::Liquid, "mangrove_roots" => SkipRendering::MangroveRoots, _ => return Err("unrecognized render rule"), }; Ok(( name, RenderDefinition { skip, can_occlude: v["can_occlude"].as_bool().ok_or("missing occlusion flag")?, bars_tag: v["bars_tag"].as_bool().ok_or("missing bars tag")?, }, )) }) .collect::>()?; Ok(FaceVisibility::new(&occlusion, &definitions)?) }