use std::collections::HashMap; use crate::primitives; use strafesnet_common::map; use strafesnet_common::model; use strafesnet_common::gameplay_modes; use strafesnet_common::gameplay_style; use strafesnet_common::gameplay_attributes as attr; use strafesnet_common::integer::{Planar64,Planar64Vec3,Planar64Mat3,Planar64Affine3}; use strafesnet_common::model::RenderConfig; use strafesnet_common::model::RenderConfigId; use strafesnet_common::updatable::Updatable; fn class_is_a(class: &str, superclass: &str) -> bool { if class==superclass { return true } let class_descriptor=rbx_reflection_database::get().classes.get(class); if let Some(descriptor) = &class_descriptor { if let Some(class_super) = &descriptor.superclass { return class_is_a(&class_super, superclass) } } return false } fn recursive_collect_superclass(objects: &mut std::vec::Vec,dom: &rbx_dom_weak::WeakDom, instance: &rbx_dom_weak::Instance, superclass: &str){ let mut stack=vec![instance]; while let Some(item)=stack.pop(){ for &referent in item.children(){ if let Some(c)=dom.get_by_ref(referent){ if class_is_a(c.class.as_str(),superclass){ objects.push(c.referent());//copy ref } stack.push(c); } } } } fn planar64_affine3_from_roblox(cf:&rbx_dom_weak::types::CFrame,size:&rbx_dom_weak::types::Vector3)->Planar64Affine3{ Planar64Affine3::new( Planar64Mat3::from_cols( Planar64Vec3::try_from([cf.orientation.x.x,cf.orientation.y.x,cf.orientation.z.x]).unwrap() *Planar64::try_from(size.x/2.0).unwrap(), Planar64Vec3::try_from([cf.orientation.x.y,cf.orientation.y.y,cf.orientation.z.y]).unwrap() *Planar64::try_from(size.y/2.0).unwrap(), Planar64Vec3::try_from([cf.orientation.x.z,cf.orientation.y.z,cf.orientation.z.z]).unwrap() *Planar64::try_from(size.z/2.0).unwrap(), ), Planar64Vec3::try_from([cf.position.x,cf.position.y,cf.position.z]).unwrap() ) } struct ModeBuilder{ mode:gameplay_modes::Mode, final_stage_id_from_builder_stage_id:HashMap, } #[derive(Default)] struct ModesBuilder{ modes:HashMap, stages:HashMap>, mode_updates:Vec<(gameplay_modes::ModeId,gameplay_modes::ModeUpdate)>, stage_updates:Vec<(gameplay_modes::ModeId,gameplay_modes::StageId,gameplay_modes::StageUpdate)>, } impl ModesBuilder{ fn build(mut self)->gameplay_modes::Modes{ //collect modes and stages into contiguous arrays let mut unique_modes:Vec<(gameplay_modes::ModeId,gameplay_modes::Mode)> =self.modes.into_iter().collect(); unique_modes.sort_by(|a,b|a.0.cmp(&b.0)); let (mut modes,final_mode_id_from_builder_mode_id):(Vec,HashMap) =unique_modes.into_iter().enumerate() .map(|(final_mode_id,(builder_mode_id,mut mode))|{ ( ModeBuilder{ final_stage_id_from_builder_stage_id:self.stages.remove(&builder_mode_id).map_or_else(||HashMap::new(),|stages|{ let mut unique_stages:Vec<(gameplay_modes::StageId,gameplay_modes::Stage)> =stages.into_iter().collect(); unique_stages.sort_by(|a,b|a.0.cmp(&b.0)); unique_stages.into_iter().enumerate() .map(|(final_stage_id,(builder_stage_id,stage))|{ mode.push_stage(stage); (builder_stage_id,gameplay_modes::StageId::new(final_stage_id as u32)) }).collect() }), mode, }, ( builder_mode_id, gameplay_modes::ModeId::new(final_mode_id as u32) ) ) }).unzip(); //TODO: failure messages or errors or something //push stage updates for (builder_mode_id,builder_stage_id,stage_update) in self.stage_updates{ if let Some(final_mode_id)=final_mode_id_from_builder_mode_id.get(&builder_mode_id){ if let Some(mode)=modes.get_mut(final_mode_id.get() as usize){ if let Some(&final_stage_id)=mode.final_stage_id_from_builder_stage_id.get(&builder_stage_id){ if let Some(stage)=mode.mode.get_stage_mut(final_stage_id){ stage.update(stage_update); } } } } } //push mode updates for (builder_mode_id,mut mode_update) in self.mode_updates{ if let Some(final_mode_id)=final_mode_id_from_builder_mode_id.get(&builder_mode_id){ if let Some(mode)=modes.get_mut(final_mode_id.get() as usize){ //map stage id on stage elements mode_update.map_stage_element_ids(|stage_id| //walk down one stage id at a time until a stage is found //TODO use better logic like BTreeMap::upper_bound instead of walking // final_stage_id_from_builder_stage_id.upper_bound(Bound::Included(&stage_id)) // .value().copied().unwrap_or(gameplay_modes::StageId::FIRST) (0..=stage_id.get()).rev().find_map(|builder_stage_id| //map the stage element to that stage mode.final_stage_id_from_builder_stage_id.get(&gameplay_modes::StageId::new(builder_stage_id)).copied() ).unwrap_or(gameplay_modes::StageId::FIRST) ); mode.mode.update(mode_update); } } } gameplay_modes::Modes::new(modes.into_iter().map(|mode_builder|mode_builder.mode).collect()) } fn insert_mode(&mut self,mode_id:gameplay_modes::ModeId,mode:gameplay_modes::Mode){ assert!(self.modes.insert(mode_id,mode).is_none(),"Cannot replace existing mode"); } fn insert_stage(&mut self,mode_id:gameplay_modes::ModeId,stage_id:gameplay_modes::StageId,stage:gameplay_modes::Stage){ assert!(self.stages.entry(mode_id).or_insert(HashMap::new()).insert(stage_id,stage).is_none(),"Cannot replace existing stage"); } fn push_mode_update(&mut self,mode_id:gameplay_modes::ModeId,mode_update:gameplay_modes::ModeUpdate){ self.mode_updates.push((mode_id,mode_update)); } fn push_stage_update(&mut self,mode_id:gameplay_modes::ModeId,stage_id:gameplay_modes::StageId,stage_update:gameplay_modes::StageUpdate){ self.stage_updates.push((mode_id,stage_id,stage_update)); } } fn get_attributes(object:&rbx_dom_weak::Instance,can_collide:bool,velocity:Planar64Vec3,model_id:model::ModelId,modes_builder:&mut ModesBuilder,wormhole_in_model_to_id:&mut HashMap,wormhole_id_to_out_model:&mut HashMap)->attr::CollisionAttributes{ let name=object.name.as_str(); let mut general=attr::GeneralAttributes::default(); let mut intersecting=attr::IntersectingAttributes::default(); let mut contacting=attr::ContactingAttributes::default(); let mut force_can_collide=can_collide; let mut force_intersecting=false; match name{ "Water"=>{ force_can_collide=false; //TODO: read stupid CustomPhysicalProperties intersecting.water=Some(attr::IntersectingWater{density:Planar64::ONE,viscosity:Planar64::ONE/10,velocity}); }, "Accelerator"=>{ //although the new game supports collidable accelerators, this is a roblox compatability map loader force_can_collide=false; general.accelerator=Some(attr::Accelerator{acceleration:velocity}); }, // "UnorderedCheckpoint"=>general.teleport_behaviour=Some(model::TeleportBehaviour::StageElement(attr::StageElement{ // mode_id:0, // stage_id:0, // force:false, // behaviour:model::StageElementBehaviour::Unordered // })), "SetVelocity"=>general.trajectory=Some(attr::SetTrajectory::Velocity(velocity)), "MapStart"=>{ force_can_collide=false; force_intersecting=true; modes_builder.insert_mode( gameplay_modes::ModeId::MAIN, gameplay_modes::Mode::new( gameplay_style::StyleModifiers::roblox_bhop(), model_id ) ); }, "MapFinish"=>{ force_can_collide=false; force_intersecting=true; modes_builder.push_mode_update( gameplay_modes::ModeId::MAIN, gameplay_modes::ModeUpdate::zone( model_id, gameplay_modes::Zone::Finish, ), ); }, "MapAnticheat"=>{ force_can_collide=false; force_intersecting=true; modes_builder.push_mode_update( gameplay_modes::ModeId::MAIN, gameplay_modes::ModeUpdate::zone( model_id, gameplay_modes::Zone::Anticheat, ), ); }, "Platform"=>{ modes_builder.push_mode_update( gameplay_modes::ModeId::MAIN, gameplay_modes::ModeUpdate::element( model_id, gameplay_modes::StageElement::new(0,false,gameplay_modes::StageElementBehaviour::Platform),//roblox does not know which stage the platform belongs to ), ); }, other=>{ let regman=lazy_regex::regex!(r"^(BonusStart|WormholeOut)(\d+)$"); if let Some(captures)=regman.captures(other){ match &captures[1]{ "BonusStart"=>{ force_can_collide=false; force_intersecting=true; modes_builder.insert_mode( gameplay_modes::ModeId::new(captures[2].parse::().unwrap()), gameplay_modes::Mode::new( gameplay_style::StyleModifiers::roblox_bhop(), model_id ) ); }, "WormholeOut"=>{ //this object is not special in strafe client, but the roblox mapping needs to be converted to model id assert!(wormhole_id_to_out_model.insert(captures[2].parse::().unwrap(),model_id).is_none(),"Cannot have multiple WormholeOut with same id"); }, _=>(), } }else if let Some(captures)=lazy_regex::regex!(r"^(Force)?(Spawn|SpawnAt|Trigger|Teleport|Platform)(\d+)$") .captures(other){ force_intersecting=true; let stage_element=gameplay_modes::StageElement::new( //stage_id: captures[3].parse::().unwrap(), //force: match captures.get(1){ Some(m)=>m.as_str()=="Force", None=>false, }, //behaviour: match &captures[2]{ "Spawn"=>{ modes_builder.insert_stage( gameplay_modes::ModeId::MAIN, gameplay_modes::StageId::new(captures[2].parse::().unwrap()), gameplay_modes::Stage::new(model_id), ); gameplay_modes::StageElementBehaviour::SpawnAt }, "SpawnAt"=>gameplay_modes::StageElementBehaviour::SpawnAt, //cancollide false so you don't hit the side //NOT a decoration "Trigger"=>{force_can_collide=false;gameplay_modes::StageElementBehaviour::Trigger}, "Teleport"=>{force_can_collide=false;gameplay_modes::StageElementBehaviour::Teleport}, "Platform"=>gameplay_modes::StageElementBehaviour::Platform, _=>panic!("regex1[2] messed up bad"), }, ); modes_builder.push_mode_update( gameplay_modes::ModeId::MAIN, gameplay_modes::ModeUpdate::element( model_id, stage_element, ), ); }else if let Some(captures)=lazy_regex::regex!(r"^(Jump|WormholeIn)(\d+)$") .captures(other){ match &captures[1]{ "Jump"=>modes_builder.push_mode_update( gameplay_modes::ModeId::MAIN, gameplay_modes::ModeUpdate::jump_limit( model_id, //jump_limit: captures[2].parse::().unwrap() ), ), "WormholeIn"=>{ force_can_collide=false; force_intersecting=true; assert!(wormhole_in_model_to_id.insert(model_id,captures[2].parse::().unwrap()).is_none(),"Impossible"); }, _=>panic!("regex2[1] messed up bad"), } }else if let Some(captures)=lazy_regex::regex!(r"^Bonus(Finish|Anticheat)(\d+)$") .captures(other){ force_can_collide=false; force_intersecting=true; modes_builder.push_mode_update( gameplay_modes::ModeId::new(captures[2].parse::().unwrap()), gameplay_modes::ModeUpdate::zone( model_id, //zone: match &captures[1]{ "Finish"=>gameplay_modes::Zone::Finish, "Anticheat"=>gameplay_modes::Zone::Anticheat, _=>panic!("regex3[1] messed up bad"), }, ), ); } // else if let Some(captures)=lazy_regex::regex!(r"^Stage(\d+)OrderedCheckpoint(\d+)$") // .captures(other){ // match &captures[1]{ // "OrderedCheckpoint"=>modes_builder.push_stage_update( // gameplay_modes::ModeId::MAIN, // gameplay_modes::StageId::new(0), // gameplay_modes::StageUpdate::ordered_checkpoint(captures[2].parse::().unwrap()), // ), // _=>panic!("regex3[1] messed up bad"), // } // } } } //need some way to skip this if velocity!=Planar64Vec3::ZERO{ general.booster=Some(attr::Booster::Velocity(velocity)); } match force_can_collide{ true=>{ match name{ "Bounce"=>contacting.contact_behaviour=Some(attr::ContactingBehaviour::Elastic(u32::MAX)), "Surf"=>contacting.contact_behaviour=Some(attr::ContactingBehaviour::Surf), "Ladder"=>contacting.contact_behaviour=Some(attr::ContactingBehaviour::Ladder(attr::ContactingLadder{sticky:true})), _=>(), } attr::CollisionAttributes::Contact{contacting,general} }, false=>if force_intersecting ||general.any() ||intersecting.any() { attr::CollisionAttributes::Intersect{intersecting,general} }else{ attr::CollisionAttributes::Decoration }, } } struct RobloxAssetId(u64); struct RobloxAssetIdParseErr; impl std::str::FromStr for RobloxAssetId{ type Err=RobloxAssetIdParseErr; fn from_str(s:&str)->Result{ let regman=lazy_regex::regex!(r"(\d+)$"); if let Some(captures) = regman.captures(s) { if captures.len()==2{//captures[0] is all captures concatenated, and then each individual capture if let Ok(id) = captures[0].parse::(){ return Ok(Self(id)); } } } Err(RobloxAssetIdParseErr) } } #[derive(Clone,Copy,PartialEq)] struct RobloxTextureTransform{ offset_u:f32, offset_v:f32, scale_u:f32, scale_v:f32, } impl std::cmp::Eq for RobloxTextureTransform{}//???? impl std::default::Default for RobloxTextureTransform{ fn default()->Self{ Self{offset_u:0.0,offset_v:0.0,scale_u:1.0,scale_v:1.0} } } impl std::hash::Hash for RobloxTextureTransform{ fn hash(&self,state:&mut H) { self.offset_u.to_ne_bytes().hash(state); self.offset_v.to_ne_bytes().hash(state); self.scale_u.to_ne_bytes().hash(state); self.scale_v.to_ne_bytes().hash(state); } } #[derive(Clone,PartialEq)] struct RobloxFaceTextureDescription{ render:RenderConfigId, color:glam::Vec4, transform:RobloxTextureTransform, } impl std::cmp::Eq for RobloxFaceTextureDescription{}//???? impl std::hash::Hash for RobloxFaceTextureDescription{ fn hash(&self,state:&mut H){ self.render.hash(state); self.transform.hash(state); for &el in self.color.as_ref().iter(){ el.to_ne_bytes().hash(state); } } } impl RobloxFaceTextureDescription{ fn to_face_description(&self)->primitives::FaceDescription{ primitives::FaceDescription{ render:self.render, transform:glam::Affine2::from_translation( glam::vec2(self.transform.offset_u,self.transform.offset_v) ) *glam::Affine2::from_scale( glam::vec2(self.transform.scale_u,self.transform.scale_v) ), color:self.color, } } } type RobloxPartDescription=[Option;6]; type RobloxWedgeDescription=[Option;5]; type RobloxCornerWedgeDescription=[Option;5]; #[derive(Clone,Eq,Hash,PartialEq)] enum RobloxBasePartDescription{ Sphere(RobloxPartDescription), Part(RobloxPartDescription), Cylinder(RobloxPartDescription), Wedge(RobloxWedgeDescription), CornerWedge(RobloxCornerWedgeDescription), } struct ModelOwnedAttributes{ mesh:model::MeshId, attributes:attr::CollisionAttributes, color:model::Color4,//transparency is in here transform:Planar64Affine3, } pub fn convert(dom:rbx_dom_weak::WeakDom)->map::CompleteMap{ let mut modes_builder=ModesBuilder::default(); let mut models1=Vec::new(); let mut meshes=Vec::new(); let mut indexed_model_id_from_description=HashMap::new(); let mut unique_attributes=Vec::new(); let mut attributes_id_from_attributes=HashMap::new(); let mut wormhole_in_model_to_id=HashMap::new(); let mut wormhole_id_to_out_model=HashMap::new(); //TODO: some sort of thing like RobloxResources that describes where to get each resource //this would be another dependency built for downloading resources to keep this one clean let mut unique_render_groups=vec![RenderConfig::default()]; let mut render_id_from_asset_id=HashMap::::new(); let textureless_render_group=RenderConfigId::new(0); let mut object_refs=Vec::new(); let mut temp_objects=Vec::new(); recursive_collect_superclass(&mut object_refs, &dom, dom.root(),"BasePart"); for object_ref in object_refs { if let Some(object)=dom.get_by_ref(object_ref){ if let ( Some(rbx_dom_weak::types::Variant::CFrame(cf)), Some(rbx_dom_weak::types::Variant::Vector3(size)), Some(rbx_dom_weak::types::Variant::Vector3(velocity)), Some(rbx_dom_weak::types::Variant::Float32(transparency)), Some(rbx_dom_weak::types::Variant::Color3uint8(color3)), Some(rbx_dom_weak::types::Variant::Bool(can_collide)), ) = ( object.properties.get("CFrame"), object.properties.get("Size"), object.properties.get("Velocity"), object.properties.get("Transparency"), object.properties.get("Color"), object.properties.get("CanCollide"), ) { let model_transform=planar64_affine3_from_roblox(cf,size); if model_transform.matrix3.determinant()==Planar64::ZERO{ let mut parent_ref=object.parent(); let mut full_path=object.name.clone(); while let Some(parent)=dom.get_by_ref(parent_ref){ full_path=format!("{}.{}",parent.name,full_path); parent_ref=parent.parent(); } println!("Zero determinant CFrame at location {}",full_path); println!("matrix3:{}",model_transform.matrix3); continue; } //at this point a new model is going to be generated for sure. let model_id=model::ModelId::new(models1.len() as u32); //TODO: also detect "CylinderMesh" etc here let shape=match &object.class[..]{ "Part"=>{ if let Some(rbx_dom_weak::types::Variant::Enum(shape))=object.properties.get("Shape"){ match shape.to_u32(){ 0=>primitives::Primitives::Sphere, 1=>primitives::Primitives::Cube, 2=>primitives::Primitives::Cylinder, 3=>primitives::Primitives::Wedge, 4=>primitives::Primitives::CornerWedge, _=>panic!("Funky roblox PartType={};",shape.to_u32()), } }else{ panic!("Part has no Shape!"); } }, "TrussPart"=>primitives::Primitives::Cube, "WedgePart"=>primitives::Primitives::Wedge, "CornerWedgePart"=>primitives::Primitives::CornerWedge, _=>{ println!("Unsupported BasePart ClassName={}; defaulting to cube",object.class); primitives::Primitives::Cube } }; //use the biggest one and cut it down later... let mut part_texture_description:RobloxPartDescription=[None,None,None,None,None,None]; temp_objects.clear(); recursive_collect_superclass(&mut temp_objects, &dom, object,"Decal"); for &decal_ref in &temp_objects{ if let Some(decal)=dom.get_by_ref(decal_ref){ if let ( Some(rbx_dom_weak::types::Variant::Content(content)), Some(rbx_dom_weak::types::Variant::Enum(normalid)), Some(rbx_dom_weak::types::Variant::Color3(decal_color3)), Some(rbx_dom_weak::types::Variant::Float32(decal_transparency)), ) = ( decal.properties.get("Texture"), decal.properties.get("Face"), decal.properties.get("Color3"), decal.properties.get("Transparency"), ) { if let Ok(asset_id)=content.clone().into_string().parse::(){ let render_id=textureless_render_group; /* TODO: textures let render_id=if let Some(&render_id)=render_id_from_asset_id.get(&asset_id.0){ render_id }else{ let render_id=RenderConfigId::new(unique_render_groups.len() as u32); render_id_from_asset_id.insert(asset_id.0,render_id); unique_render_groups.push(RenderConfig::texture(asset_id.0)); render_id }; */ let normal_id=normalid.to_u32(); if normal_id<6{ let (roblox_texture_color,roblox_texture_transform)=if decal.class=="Texture"{ //generate tranform if let ( Some(rbx_dom_weak::types::Variant::Float32(ox)), Some(rbx_dom_weak::types::Variant::Float32(oy)), Some(rbx_dom_weak::types::Variant::Float32(sx)), Some(rbx_dom_weak::types::Variant::Float32(sy)), ) = ( decal.properties.get("OffsetStudsU"), decal.properties.get("OffsetStudsV"), decal.properties.get("StudsPerTileU"), decal.properties.get("StudsPerTileV"), ) { let (size_u,size_v)=match normal_id{ 0=>(size.z,size.y),//right 1=>(size.x,size.z),//top 2=>(size.x,size.y),//back 3=>(size.z,size.y),//left 4=>(size.x,size.z),//bottom 5=>(size.x,size.y),//front _=>panic!("unreachable"), }; ( glam::vec4(decal_color3.r,decal_color3.g,decal_color3.b,1.0-*decal_transparency), RobloxTextureTransform{ offset_u:*ox/(*sx),offset_v:*oy/(*sy), scale_u:size_u/(*sx),scale_v:size_v/(*sy), } ) }else{ (glam::Vec4::ONE,RobloxTextureTransform::default()) } }else{ (glam::Vec4::ONE,RobloxTextureTransform::default()) }; part_texture_description[normal_id as usize]=Some(RobloxFaceTextureDescription{ render:render_id, color:roblox_texture_color, transform:roblox_texture_transform, }); }else{ println!("NormalId={} unsupported for shape={:?}",normal_id,shape); } } } } } //obscure rust syntax "slice pattern" let [ f0,//Cube::Right f1,//Cube::Top f2,//Cube::Back f3,//Cube::Left f4,//Cube::Bottom f5,//Cube::Front ]=part_texture_description; let basepart_texture_description=match shape{ primitives::Primitives::Sphere=>RobloxBasePartDescription::Sphere([f0,f1,f2,f3,f4,f5]), primitives::Primitives::Cube=>RobloxBasePartDescription::Part([f0,f1,f2,f3,f4,f5]), primitives::Primitives::Cylinder=>RobloxBasePartDescription::Cylinder([f0,f1,f2,f3,f4,f5]), //use front face texture first and use top face texture as a fallback primitives::Primitives::Wedge=>RobloxBasePartDescription::Wedge([ f0,//Cube::Right->Wedge::Right if f5.is_some(){f5}else{f1},//Cube::Front|Cube::Top->Wedge::TopFront f2,//Cube::Back->Wedge::Back f3,//Cube::Left->Wedge::Left f4,//Cube::Bottom->Wedge::Bottom ]), //TODO: fix Left+Back texture coordinates to match roblox when not overwridden by Top primitives::Primitives::CornerWedge=>RobloxBasePartDescription::CornerWedge([ f0,//Cube::Right->CornerWedge::Right if f2.is_some(){f2}else{f1.clone()},//Cube::Back|Cube::Top->CornerWedge::TopBack if f3.is_some(){f3}else{f1},//Cube::Left|Cube::Top->CornerWedge::TopLeft f4,//Cube::Bottom->CornerWedge::Bottom f5,//Cube::Front->CornerWedge::Front ]), }; //make new model if unit cube has not been created before let indexed_model_id=if let Some(&indexed_model_id)=indexed_model_id_from_description.get(&basepart_texture_description){ //push to existing texture model indexed_model_id }else{ let indexed_model_id=model::MeshId::new(meshes.len() as u32); indexed_model_id_from_description.insert(basepart_texture_description.clone(),indexed_model_id);//borrow checker going crazy meshes.push(match basepart_texture_description{ RobloxBasePartDescription::Sphere(part_texture_description) |RobloxBasePartDescription::Cylinder(part_texture_description) |RobloxBasePartDescription::Part(part_texture_description)=>{ let mut cube_face_description=primitives::CubeFaceDescription::default(); for (face_id,roblox_face_description) in part_texture_description.iter().enumerate(){ cube_face_description.insert( match face_id{ 0=>primitives::CubeFace::Right, 1=>primitives::CubeFace::Top, 2=>primitives::CubeFace::Back, 3=>primitives::CubeFace::Left, 4=>primitives::CubeFace::Bottom, 5=>primitives::CubeFace::Front, _=>panic!("unreachable"), }, match roblox_face_description{ Some(roblox_texture_transform)=>roblox_texture_transform.to_face_description(), None=>primitives::FaceDescription::new_with_render_id(textureless_render_group), }); } primitives::generate_partial_unit_cube(cube_face_description) }, RobloxBasePartDescription::Wedge(wedge_texture_description)=>{ let mut wedge_face_description=primitives::WedgeFaceDescription::default(); for (face_id,roblox_face_description) in wedge_texture_description.iter().enumerate(){ wedge_face_description.insert( match face_id{ 0=>primitives::WedgeFace::Right, 1=>primitives::WedgeFace::TopFront, 2=>primitives::WedgeFace::Back, 3=>primitives::WedgeFace::Left, 4=>primitives::WedgeFace::Bottom, _=>panic!("unreachable"), }, match roblox_face_description{ Some(roblox_texture_transform)=>roblox_texture_transform.to_face_description(), None=>primitives::FaceDescription::new_with_render_id(textureless_render_group), }); } primitives::generate_partial_unit_wedge(wedge_face_description) }, RobloxBasePartDescription::CornerWedge(cornerwedge_texture_description)=>{ let mut cornerwedge_face_description=primitives::CornerWedgeFaceDescription::default(); for (face_id,roblox_face_description) in cornerwedge_texture_description.iter().enumerate(){ cornerwedge_face_description.insert( match face_id{ 0=>primitives::CornerWedgeFace::Right, 1=>primitives::CornerWedgeFace::TopBack, 2=>primitives::CornerWedgeFace::TopLeft, 3=>primitives::CornerWedgeFace::Bottom, 4=>primitives::CornerWedgeFace::Front, _=>panic!("unreachable"), }, match roblox_face_description{ Some(roblox_texture_transform)=>roblox_texture_transform.to_face_description(), None=>primitives::FaceDescription::new_with_render_id(textureless_render_group), }); } primitives::generate_partial_unit_cornerwedge(cornerwedge_face_description) }, }); indexed_model_id }; let attributes=get_attributes( &object, *can_collide, Planar64Vec3::try_from([velocity.x,velocity.y,velocity.z]).unwrap(), model_id, &mut modes_builder, &mut wormhole_in_model_to_id, &mut wormhole_id_to_out_model, ); models1.push(ModelOwnedAttributes{ mesh:indexed_model_id, transform:model_transform, color:glam::vec4(color3.r as f32/255f32, color3.g as f32/255f32, color3.b as f32/255f32, 1.0-*transparency), attributes, }); } } } let models=models1.into_iter().enumerate().map(|(model_id,mut model1)|{ let model_id=model::ModelId::new(model_id as u32); //update attributes with wormhole id //TODO: errors/prints if let Some(wormhole_id)=wormhole_in_model_to_id.get(&model_id){ if let Some(&wormhole_out_model_id)=wormhole_id_to_out_model.get(wormhole_id){ match &mut model1.attributes{ attr::CollisionAttributes::Contact{contacting:_,general} |attr::CollisionAttributes::Intersect{intersecting:_,general} =>general.wormhole=Some(attr::Wormhole{destination_model:wormhole_out_model_id}), attr::CollisionAttributes::Decoration=>println!("Not a wormhole"), } } } //index the attributes let attributes_id=if let Some(&attributes_id)=attributes_id_from_attributes.get(&model1.attributes){ attributes_id }else{ let attributes_id=attr::CollisionAttributesId::new(unique_attributes.len() as u32); attributes_id_from_attributes.insert(model1.attributes.clone(),attributes_id); unique_attributes.push(model1.attributes); attributes_id }; model::Model{ mesh:model1.mesh, transform:model1.transform, color:model1.color, attributes:attributes_id, } }).collect(); map::CompleteMap{ textures:Vec::new(), render_configs:vec![RenderConfig::default()],//asset_id_from_texture_id.iter().map(|t|t.to_string()).collect(), meshes, models, modes:modes_builder.build(), attributes:unique_attributes, } }