diff --git a/Cargo.lock b/Cargo.lock index 92e910b..180a3bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -98,6 +98,16 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "151665d9be52f9bb40fc7966565d39666f2d1e69233571b71b87791c7e0528b3" +[[package]] +name = "id" +version = "0.1.0" +source = "git+https://git.itzana.me/Quaternions/id?rev=1f710976cc786c8853dab73d6e1cee53158deeb0#1f710976cc786c8853dab73d6e1cee53158deeb0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "lazy-regex" version = "3.1.0" @@ -410,9 +420,10 @@ dependencies = [ [[package]] name = "strafesnet_common" version = "0.1.0" -source = "git+https://git.itzana.me/StrafesNET/common?rev=5ee826d9487b5e2bea4b3cf99a68ce9a95d72f72#5ee826d9487b5e2bea4b3cf99a68ce9a95d72f72" +source = "git+https://git.itzana.me/StrafesNET/common?rev=47cdea0c8a5d10a2440ca6270a975d560aa3642d#47cdea0c8a5d10a2440ca6270a975d560aa3642d" dependencies = [ "glam", + "id", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 3972497..0897aab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,4 +12,4 @@ rbx_binary = "0.7.4" rbx_dom_weak = "2.7.0" rbx_reflection_database = "0.2.10" rbx_xml = "0.13.3" -strafesnet_common = { git = "https://git.itzana.me/StrafesNET/common", rev = "5ee826d9487b5e2bea4b3cf99a68ce9a95d72f72" } +strafesnet_common = { git = "https://git.itzana.me/StrafesNET/common", rev = "47cdea0c8a5d10a2440ca6270a975d560aa3642d" } diff --git a/src/lib.rs b/src/lib.rs index 57a6919..3b35ed8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,2 +1,26 @@ +use std::io::Read; + +mod rbx; mod primitives; -pub mod rbx; \ No newline at end of file + +#[derive(Debug)] +pub enum Error{ + RbxBinary(rbx_binary::DecodeError), + RbxXml(rbx_xml::DecodeError), + Io(std::io::Error), + UnknownFileFormat, +} + +fn load_dom(input:R)->Result{ + let mut buf=std::io::BufReader::new(input); + let peek=std::io::BufRead::fill_buf(&mut buf).map_err(Error::Io)?; + match &peek[0..8]{ + b"return rbx_binary::from_reader(buf).map_err(Error::RbxBinary), + b"return rbx_xml::from_reader_default(buf).map_err(Error::RbxXml), + _=>Err(Error::UnknownFileFormat), + } +} + +pub fn readOption>(input:R,acquire_id:F)->Result{ + Ok(rbx::convert(load_dom(input)?,acquire_id)) +} \ No newline at end of file diff --git a/src/primitives.rs b/src/primitives.rs index 3c98259..518841c 100644 --- a/src/primitives.rs +++ b/src/primitives.rs @@ -1,4 +1,4 @@ -use strafesnet_common::model::{Color4,TextureCoordinate,IndexedModel,IndexedPolygon,IndexedGroup,IndexedVertex}; +use strafesnet_common::model::{Color4,TextureCoordinate,Mesh,IndexedGraphicsGroup,IndexedPhysicsGroup,IndexedVertex,PolygonGroupId,PolygonGroup,PolygonList,IndexedVertexList,PositionId,TextureCoordinateId,NormalId,ColorId,VertexId,RenderConfigId}; use strafesnet_common::integer::Planar64Vec3; #[derive(Debug)] @@ -126,8 +126,8 @@ const CORNERWEDGE_DEFAULT_NORMALS:[Planar64Vec3;5]=[ Planar64Vec3::int( 0,-1, 0),//CornerWedge::Bottom Planar64Vec3::int( 0, 0,-1),//CornerWedge::Front ]; -pub fn unit_sphere()->crate::model::IndexedModel{ - unit_cube() +pub fn unit_sphere(render:RenderConfigId)->Mesh{ + unit_cube(render) } #[derive(Default)] pub struct CubeFaceDescription([Option;6]); @@ -139,18 +139,19 @@ impl CubeFaceDescription{ self.0.into_iter().enumerate().filter_map(|v|v.1.map(|u|(v.0,u))) } } -pub fn unit_cube()->crate::model::IndexedModel{ +pub fn unit_cube(render:RenderConfigId)->Mesh{ let mut t=CubeFaceDescription::default(); - t.insert(CubeFace::Right,FaceDescription::default()); - t.insert(CubeFace::Top,FaceDescription::default()); - t.insert(CubeFace::Back,FaceDescription::default()); - t.insert(CubeFace::Left,FaceDescription::default()); - t.insert(CubeFace::Bottom,FaceDescription::default()); - t.insert(CubeFace::Front,FaceDescription::default()); + t.insert(CubeFace::Right,FaceDescription::new_with_render_id(render)); + t.insert(CubeFace::Top,FaceDescription::new_with_render_id(render)); + t.insert(CubeFace::Back,FaceDescription::new_with_render_id(render)); + t.insert(CubeFace::Left,FaceDescription::new_with_render_id(render)); + t.insert(CubeFace::Bottom,FaceDescription::new_with_render_id(render)); + t.insert(CubeFace::Front,FaceDescription::new_with_render_id(render)); generate_partial_unit_cube(t) } -pub fn unit_cylinder()->crate::model::IndexedModel{ - unit_cube() +pub fn unit_cylinder(render:RenderConfigId)->Mesh{ + //lmao + unit_cube(render) } #[derive(Default)] pub struct WedgeFaceDescription([Option;5]); @@ -162,13 +163,13 @@ impl WedgeFaceDescription{ self.0.into_iter().enumerate().filter_map(|v|v.1.map(|u|(v.0,u))) } } -pub fn unit_wedge()->crate::model::IndexedModel{ +pub fn unit_wedge(render:RenderConfigId)->Mesh{ let mut t=WedgeFaceDescription::default(); - t.insert(WedgeFace::Right,FaceDescription::default()); - t.insert(WedgeFace::TopFront,FaceDescription::default()); - t.insert(WedgeFace::Back,FaceDescription::default()); - t.insert(WedgeFace::Left,FaceDescription::default()); - t.insert(WedgeFace::Bottom,FaceDescription::default()); + t.insert(WedgeFace::Right,FaceDescription::new_with_render_id(render)); + t.insert(WedgeFace::TopFront,FaceDescription::new_with_render_id(render)); + t.insert(WedgeFace::Back,FaceDescription::new_with_render_id(render)); + t.insert(WedgeFace::Left,FaceDescription::new_with_render_id(render)); + t.insert(WedgeFace::Bottom,FaceDescription::new_with_render_id(render)); generate_partial_unit_wedge(t) } #[derive(Default)] @@ -181,26 +182,26 @@ impl CornerWedgeFaceDescription{ self.0.into_iter().enumerate().filter_map(|v|v.1.map(|u|(v.0,u))) } } -pub fn unit_cornerwedge()->crate::model::IndexedModel{ +pub fn unit_cornerwedge(render:RenderConfigId)->Mesh{ let mut t=CornerWedgeFaceDescription::default(); - t.insert(CornerWedgeFace::Right,FaceDescription::default()); - t.insert(CornerWedgeFace::TopBack,FaceDescription::default()); - t.insert(CornerWedgeFace::TopLeft,FaceDescription::default()); - t.insert(CornerWedgeFace::Bottom,FaceDescription::default()); - t.insert(CornerWedgeFace::Front,FaceDescription::default()); + t.insert(CornerWedgeFace::Right,FaceDescription::new_with_render_id(render)); + t.insert(CornerWedgeFace::TopBack,FaceDescription::new_with_render_id(render)); + t.insert(CornerWedgeFace::TopLeft,FaceDescription::new_with_render_id(render)); + t.insert(CornerWedgeFace::Bottom,FaceDescription::new_with_render_id(render)); + t.insert(CornerWedgeFace::Front,FaceDescription::new_with_render_id(render)); generate_partial_unit_cornerwedge(t) } #[derive(Clone)] pub struct FaceDescription{ - pub texture:Option, + pub render:RenderConfigId, pub transform:glam::Affine2, pub color:Color4, } -impl std::default::Default for FaceDescription{ - fn default()->Self { +impl FaceDescription{ + pub fn new_with_render_id(render:RenderConfigId)->Self { Self{ - texture:None, + render, transform:glam::Affine2::IDENTITY, color:Color4::new(1.0,1.0,1.0,0.0),//zero alpha to hide the default texture } @@ -208,13 +209,15 @@ impl std::default::Default for FaceDescription{ } //TODO: it's probably better to use a shared vertex buffer between all primitives and use indexed rendering instead of generating a unique vertex buffer for each primitive. //implementation: put all roblox primitives into one model.groups <- this won't work but I forget why -pub fn generate_partial_unit_cube(face_descriptions:CubeFaceDescription)->crate::model::IndexedModel{ +pub fn generate_partial_unit_cube(face_descriptions:CubeFaceDescription)->Mesh{ let mut generated_pos=Vec::new(); let mut generated_tex=Vec::new(); let mut generated_normal=Vec::new(); let mut generated_color=Vec::new(); let mut generated_vertices=Vec::new(); - let mut groups=Vec::new(); + let mut polygon_groups=Vec::new(); + let mut graphics_groups=Vec::new(); + let mut physics_groups=vec![IndexedPhysicsGroup::default()]; let mut transforms=Vec::new(); //note that on a cube every vertex is guaranteed to be unique, so there's no need to hash them against existing vertices. for (face_id,face_description) in face_descriptions.pairs(){ @@ -242,46 +245,50 @@ pub fn generate_partial_unit_cube(face_descriptions:CubeFaceDescription)->crate: let normal_index=generated_normal.len() as u32; generated_normal.push(CUBE_DEFAULT_NORMALS[face_id]); //push vertices as they are needed - groups.push(IndexedGroup{ - texture:face_description.texture, - polys:vec![IndexedPolygon{ - vertices:CUBE_DEFAULT_POLYS[face_id].map(|tup|{ - let pos=CUBE_DEFAULT_VERTICES[tup[0] as usize]; - let pos_index=if let Some(pos_index)=generated_pos.iter().position(|&p|p==pos){ - pos_index - }else{ - //create new pos_index - let pos_index=generated_pos.len(); - generated_pos.push(pos); - pos_index - } as u32; - //always push vertex - let vertex=IndexedVertex{ - pos:pos_index, - tex:tup[1]+4*transform_index, - normal:normal_index, - color:color_index, - }; - let vert_index=generated_vertices.len(); - generated_vertices.push(vertex); - vert_index as u32 - }).to_vec(), - }], + let group_id=PolygonGroupId::new(polygon_groups.len() as u32); + polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(vec![ + CUBE_DEFAULT_POLYS[face_id].map(|tup|{ + let pos=CUBE_DEFAULT_VERTICES[tup[0] as usize]; + let pos_index=if let Some(pos_index)=generated_pos.iter().position(|&p|p==pos){ + pos_index + }else{ + //create new pos_index + let pos_index=generated_pos.len(); + generated_pos.push(pos); + pos_index + } as u32; + //always push vertex + let vertex=IndexedVertex{ + pos:PositionId::new(pos_index), + tex:TextureCoordinateId::new(tup[1]+4*transform_index), + normal:NormalId::new(normal_index), + color:ColorId::new(color_index), + }; + let vert_index=generated_vertices.len(); + generated_vertices.push(vertex); + VertexId::new(vert_index as u32) + }).to_vec(), + ]))); + graphics_groups.push(IndexedGraphicsGroup{ + render:face_description.render, + groups:vec![group_id], }); + physics_groups[0].groups.push(group_id); } - IndexedModel{ + Mesh{ unique_pos:generated_pos, unique_tex:generated_tex, unique_normal:generated_normal, unique_color:generated_color, unique_vertices:generated_vertices, - groups, - instances:Vec::new(), + polygon_groups, + graphics_groups, + physics_groups, } } //don't think too hard about the copy paste because this is all going into the map tool eventually... -pub fn generate_partial_unit_wedge(face_descriptions:WedgeFaceDescription)->crate::model::IndexedModel{ - let wedge_default_polys=vec![ +pub fn generate_partial_unit_wedge(face_descriptions:WedgeFaceDescription)->Mesh{ + let wedge_default_polys=[ // right (1, 0, 0) vec![ [6,2,0],//[vertex,tex,norm] @@ -321,7 +328,9 @@ pub fn generate_partial_unit_wedge(face_descriptions:WedgeFaceDescription)->crat let mut generated_normal=Vec::new(); let mut generated_color=Vec::new(); let mut generated_vertices=Vec::new(); - let mut groups=Vec::new(); + let mut polygon_groups=Vec::new(); + let mut graphics_groups=Vec::new(); + let mut physics_groups=vec![IndexedPhysicsGroup::default()]; let mut transforms=Vec::new(); //note that on a cube every vertex is guaranteed to be unique, so there's no need to hash them against existing vertices. for (face_id,face_description) in face_descriptions.pairs(){ @@ -349,46 +358,50 @@ pub fn generate_partial_unit_wedge(face_descriptions:WedgeFaceDescription)->crat let normal_index=generated_normal.len() as u32; generated_normal.push(WEDGE_DEFAULT_NORMALS[face_id]); //push vertices as they are needed - groups.push(IndexedGroup{ - texture:face_description.texture, - polys:vec![IndexedPolygon{ - vertices:wedge_default_polys[face_id].iter().map(|tup|{ - let pos=CUBE_DEFAULT_VERTICES[tup[0] as usize]; - let pos_index=if let Some(pos_index)=generated_pos.iter().position(|&p|p==pos){ - pos_index - }else{ - //create new pos_index - let pos_index=generated_pos.len(); - generated_pos.push(pos); - pos_index - } as u32; - //always push vertex - let vertex=IndexedVertex{ - pos:pos_index, - tex:tup[1]+4*transform_index, - normal:normal_index, - color:color_index, - }; - let vert_index=generated_vertices.len(); - generated_vertices.push(vertex); - vert_index as u32 - }).collect(), - }], + let group_id=PolygonGroupId::new(polygon_groups.len() as u32); + polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(vec![ + wedge_default_polys[face_id].iter().map(|tup|{ + let pos=CUBE_DEFAULT_VERTICES[tup[0] as usize]; + let pos_index=if let Some(pos_index)=generated_pos.iter().position(|&p|p==pos){ + pos_index + }else{ + //create new pos_index + let pos_index=generated_pos.len(); + generated_pos.push(pos); + pos_index + } as u32; + //always push vertex + let vertex=IndexedVertex{ + pos:PositionId::new(pos_index), + tex:TextureCoordinateId::new(tup[1]+4*transform_index), + normal:NormalId::new(normal_index), + color:ColorId::new(color_index), + }; + let vert_index=generated_vertices.len(); + generated_vertices.push(vertex); + VertexId::new(vert_index as u32) + }).collect() + ]))); + graphics_groups.push(IndexedGraphicsGroup{ + render:face_description.render, + groups:vec![group_id], }); + physics_groups[0].groups.push(group_id); } - IndexedModel{ + Mesh{ unique_pos:generated_pos, unique_tex:generated_tex, unique_normal:generated_normal, unique_color:generated_color, unique_vertices:generated_vertices, - groups, - instances:Vec::new(), + polygon_groups, + graphics_groups, + physics_groups, } } -pub fn generate_partial_unit_cornerwedge(face_descriptions:CornerWedgeFaceDescription)->crate::model::IndexedModel{ - let cornerwedge_default_polys=vec![ +pub fn generate_partial_unit_cornerwedge(face_descriptions:CornerWedgeFaceDescription)->Mesh{ + let cornerwedge_default_polys=[ // right (1, 0, 0) vec![ [6,2,0],//[vertex,tex,norm] @@ -426,7 +439,9 @@ pub fn generate_partial_unit_cornerwedge(face_descriptions:CornerWedgeFaceDescri let mut generated_normal=Vec::new(); let mut generated_color=Vec::new(); let mut generated_vertices=Vec::new(); - let mut groups=Vec::new(); + let mut polygon_groups=Vec::new(); + let mut graphics_groups=Vec::new(); + let mut physics_groups=vec![IndexedPhysicsGroup::default()]; let mut transforms=Vec::new(); //note that on a cube every vertex is guaranteed to be unique, so there's no need to hash them against existing vertices. for (face_id,face_description) in face_descriptions.pairs(){ @@ -454,40 +469,44 @@ pub fn generate_partial_unit_cornerwedge(face_descriptions:CornerWedgeFaceDescri let normal_index=generated_normal.len() as u32; generated_normal.push(CORNERWEDGE_DEFAULT_NORMALS[face_id]); //push vertices as they are needed - groups.push(IndexedGroup{ - texture:face_description.texture, - polys:vec![IndexedPolygon{ - vertices:cornerwedge_default_polys[face_id].iter().map(|tup|{ - let pos=CUBE_DEFAULT_VERTICES[tup[0] as usize]; - let pos_index=if let Some(pos_index)=generated_pos.iter().position(|&p|p==pos){ - pos_index - }else{ - //create new pos_index - let pos_index=generated_pos.len(); - generated_pos.push(pos); - pos_index - } as u32; - //always push vertex - let vertex=IndexedVertex{ - pos:pos_index, - tex:tup[1]+4*transform_index, - normal:normal_index, - color:color_index, - }; - let vert_index=generated_vertices.len(); - generated_vertices.push(vertex); - vert_index as u32 - }).collect(), - }], + let group_id=PolygonGroupId::new(polygon_groups.len() as u32); + polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(vec![ + cornerwedge_default_polys[face_id].iter().map(|tup|{ + let pos=CUBE_DEFAULT_VERTICES[tup[0] as usize]; + let pos_index=if let Some(pos_index)=generated_pos.iter().position(|&p|p==pos){ + pos_index + }else{ + //create new pos_index + let pos_index=generated_pos.len(); + generated_pos.push(pos); + pos_index + } as u32; + //always push vertex + let vertex=IndexedVertex{ + pos:PositionId::new(pos_index), + tex:TextureCoordinateId::new(tup[1]+4*transform_index), + normal:NormalId::new(normal_index), + color:ColorId::new(color_index), + }; + let vert_index=generated_vertices.len(); + generated_vertices.push(vertex); + VertexId::new(vert_index as u32) + }).collect(), + ]))); + graphics_groups.push(IndexedGraphicsGroup{ + render:face_description.render, + groups:vec![group_id], }); + physics_groups[0].groups.push(group_id); } - IndexedModel{ + Mesh{ unique_pos:generated_pos, unique_tex:generated_tex, unique_normal:generated_normal, unique_color:generated_color, unique_vertices:generated_vertices, - groups, - instances:Vec::new(), + polygon_groups, + graphics_groups, + physics_groups, } } diff --git a/src/rbx.rs b/src/rbx.rs index 8a76a87..ec8c051 100644 --- a/src/rbx.rs +++ b/src/rbx.rs @@ -1,6 +1,14 @@ +use std::collections::HashMap; use crate::primitives; -use strafesnet_common::gameplay_attributes; +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 { @@ -40,91 +48,265 @@ fn planar64_affine3_from_roblox(cf:&rbx_dom_weak::types::CFrame,size:&rbx_dom_we Planar64Vec3::try_from([cf.position.x,cf.position.y,cf.position.z]).unwrap() ) } -fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,force_intersecting:bool)->model::CollisionAttributes{ - let mut general=model::GameMechanicAttributes::default(); - let mut intersecting=model::IntersectingAttributes::default(); - let mut contacting=model::ContactingAttributes::default(); +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(model::IntersectingWater{density:Planar64::ONE,viscosity:Planar64::ONE/10,velocity}); + 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(model::GameMechanicAccelerator{acceleration:velocity}); + general.accelerator=Some(attr::Accelerator{acceleration:velocity}); }, - // "UnorderedCheckpoint"=>general.teleport_behaviour=Some(model::TeleportBehaviour::StageElement(model::GameMechanicStageElement{ + // "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(model::GameMechanicSetTrajectory::Velocity(velocity)), - "MapFinish"=>{force_can_collide=false;general.zone=Some(model::GameMechanicZone{mode_id:0,behaviour:model::ZoneBehaviour::Finish})}, - "MapAnticheat"=>{force_can_collide=false;general.zone=Some(model::GameMechanicZone{mode_id:0,behaviour:model::ZoneBehaviour::Anitcheat})}, - "Platform"=>general.teleport_behaviour=Some(model::TeleportBehaviour::StageElement(model::GameMechanicStageElement{ - mode_id:0, - stage_id:0, - force:false, - behaviour:model::StageElementBehaviour::Platform, - })), + "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(gameplay_modes::StageId::FIRST,false,gameplay_modes::StageElementBehaviour::Platform),//roblox does not know which stage the platform belongs to + ), + ); + }, other=>{ - if let Some(captures)=lazy_regex::regex!(r"^(Force)?(Spawn|SpawnAt|Trigger|Teleport|Platform)(\d+)$") + 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"=>{ + //the PhysicsModelId has to exist for it to be teleported to! + force_intersecting=true; + //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){ - general.teleport_behaviour=Some(model::TeleportBehaviour::StageElement(model::GameMechanicStageElement{ - mode_id:0, - stage_id:captures[3].parse::().unwrap(), - force:match captures.get(1){ + force_intersecting=true; + let stage_id=gameplay_modes::StageId::new(captures[3].parse::().unwrap()); + let stage_element=gameplay_modes::StageElement::new( + //stage_id: + stage_id, + //force: + match captures.get(1){ Some(m)=>m.as_str()=="Force", None=>false, }, - behaviour:match &captures[2]{ - "Spawn"|"SpawnAt"=>model::StageElementBehaviour::SpawnAt, + //behaviour: + match &captures[2]{ + "Spawn"=>{ + modes_builder.insert_stage( + gameplay_modes::ModeId::MAIN, + stage_id, + gameplay_modes::Stage::new(model_id), + ); + //TODO: let denormalize handle this + 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;model::StageElementBehaviour::Trigger}, - "Teleport"=>{force_can_collide=false;model::StageElementBehaviour::Teleport}, - "Platform"=>model::StageElementBehaviour::Platform, + "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"), - } - })); - }else if let Some(captures)=lazy_regex::regex!(r"^(Force)?(Jump)(\d+)$") - .captures(other){ - general.teleport_behaviour=Some(model::TeleportBehaviour::StageElement(model::GameMechanicStageElement{ - mode_id:0, - stage_id:0, - force:match captures.get(1){ - Some(m)=>m.as_str()=="Force", - None=>false, }, - behaviour:match &captures[2]{ - "Jump"=>model::StageElementBehaviour::JumpLimit(captures[3].parse::().unwrap()), - _=>panic!("regex4[1] 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; - match &captures[1]{ - "Finish"=>general.zone=Some(model::GameMechanicZone{mode_id:captures[2].parse::().unwrap(),behaviour:model::ZoneBehaviour::Finish}), - "Anticheat"=>general.zone=Some(model::GameMechanicZone{mode_id:captures[2].parse::().unwrap(),behaviour:model::ZoneBehaviour::Anitcheat}), - _=>panic!("regex2[1] messed up bad"), - } - }else if let Some(captures)=lazy_regex::regex!(r"^(WormholeIn)(\d+)$") - .captures(other){ - force_can_collide=false; - match &captures[1]{ - "WormholeIn"=>general.teleport_behaviour=Some(model::TeleportBehaviour::Wormhole(model::GameMechanicWormhole{destination_model_id:captures[2].parse::().unwrap()})), - _=>panic!("regex3[1] messed up bad"), - } + 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"^(OrderedCheckpoint)(\d+)$") + // else if let Some(captures)=lazy_regex::regex!(r"^Stage(\d+)OrderedCheckpoint(\d+)$") // .captures(other){ // match &captures[1]{ - // "OrderedCheckpoint"=>general.checkpoint=Some(model::GameMechanicCheckpoint::Ordered{mode_id:0,checkpoint_id:captures[2].parse::().unwrap()}), + // "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"), // } // } @@ -132,45 +314,29 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,force_interse } //need some way to skip this if velocity!=Planar64Vec3::ZERO{ - general.booster=Some(model::GameMechanicBooster::Velocity(velocity)); + general.booster=Some(attr::Booster::Velocity(velocity)); } match force_can_collide{ true=>{ match name{ - "Bounce"=>contacting.contact_behaviour=Some(model::ContactingBehaviour::Elastic(u32::MAX)), - "Surf"=>contacting.contact_behaviour=Some(model::ContactingBehaviour::Surf), - "Ladder"=>contacting.contact_behaviour=Some(model::ContactingBehaviour::Ladder(model::ContactingLadder{sticky:true})), + "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})), _=>(), } - model::CollisionAttributes::Contact{contacting,general} + attr::CollisionAttributes::Contact{contacting,general} }, false=>if force_intersecting ||general.any() ||intersecting.any() { - model::CollisionAttributes::Intersect{intersecting,general} + attr::CollisionAttributes::Intersect{intersecting,general} }else{ - model::CollisionAttributes::Decoration + 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, @@ -180,12 +346,12 @@ struct RobloxTextureTransform{ } impl std::cmp::Eq for RobloxTextureTransform{}//???? impl std::default::Default for RobloxTextureTransform{ - fn default() -> Self { + 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) { +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); @@ -194,16 +360,16 @@ impl std::hash::Hash for RobloxTextureTransform { } #[derive(Clone,PartialEq)] struct RobloxFaceTextureDescription{ - texture:u32, + 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.texture.hash(state); +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() { + for &el in self.color.as_ref().iter(){ el.to_ne_bytes().hash(state); } } @@ -211,7 +377,7 @@ impl std::hash::Hash for RobloxFaceTextureDescription { impl RobloxFaceTextureDescription{ fn to_face_description(&self)->primitives::FaceDescription{ primitives::FaceDescription{ - texture:Some(self.texture), + render:self.render, transform:glam::Affine2::from_translation( glam::vec2(self.transform.offset_u,self.transform.offset_v) ) @@ -233,15 +399,29 @@ enum RobloxBasePartDescription{ Wedge(RobloxWedgeDescription), CornerWedge(RobloxCornerWedgeDescription), } -pub fn generate_indexed_models(dom:rbx_dom_weak::WeakDom) -> model::IndexedModelInstances{ - //IndexedModelInstances includes textures - let mut spawn_point=Planar64Vec3::ZERO; +struct ModelOwnedAttributes{ + mesh:model::MeshId, + attributes:attr::CollisionAttributes, + color:model::Color4,//transparency is in here + transform:Planar64Affine3, +} +pub fn convertOption>(dom:rbx_dom_weak::WeakDom,mut acquire_id:F)->map::CompleteMap{ + let mut modes_builder=ModesBuilder::default(); - let mut indexed_models=Vec::new(); - let mut model_id_from_description=std::collections::HashMap::::new(); + let mut models1=Vec::new(); + let mut meshes=Vec::new(); + let mut indexed_model_id_from_description=HashMap::new(); - let mut texture_id_from_asset_id=std::collections::HashMap::::new(); - let mut asset_id_from_texture_id=Vec::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 textureless_render_group=RenderConfigId::new(0); let mut object_refs=Vec::new(); let mut temp_objects=Vec::new(); @@ -278,31 +458,8 @@ pub fn generate_indexed_models(dom:rbx_dom_weak::WeakDom) -> model::IndexedModel continue; } - //push TempIndexedAttributes - let mut force_intersecting=false; - let mut temp_indexing_attributes=Vec::new(); - if let Some(attr)=match &object.name[..]{ - "MapStart"=>{ - spawn_point=model_transform.transform_point3(Planar64Vec3::ZERO)+Planar64Vec3::Y*5/2; - Some(model::TempIndexedAttributes::Start(model::TempAttrStart{mode_id:0})) - }, - other=>{ - let regman=lazy_regex::regex!(r"^(BonusStart|Spawn|ForceSpawn|WormholeOut)(\d+)$"); - if let Some(captures) = regman.captures(other) { - match &captures[1]{ - "BonusStart"=>Some(model::TempIndexedAttributes::Start(model::TempAttrStart{mode_id:captures[2].parse::().unwrap()})), - "Spawn"|"ForceSpawn"=>Some(model::TempIndexedAttributes::Spawn(model::TempAttrSpawn{mode_id:0,stage_id:captures[2].parse::().unwrap()})), - "WormholeOut"=>Some(model::TempIndexedAttributes::Wormhole(model::TempAttrWormhole{wormhole_id:captures[2].parse::().unwrap()})), - _=>None, - } - }else{ - None - } - } - }{ - force_intersecting=true; - temp_indexing_attributes.push(attr); - } + //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[..]{ @@ -346,15 +503,14 @@ pub fn generate_indexed_models(dom:rbx_dom_weak::WeakDom) -> model::IndexedModel decal.properties.get("Color3"), decal.properties.get("Transparency"), ) { - if let Ok(asset_id)=content.clone().into_string().parse::(){ - let texture_id=if let Some(&texture_id)=texture_id_from_asset_id.get(&asset_id.0){ - texture_id - }else{ - let texture_id=asset_id_from_texture_id.len() as u32; - texture_id_from_asset_id.insert(asset_id.0,texture_id); - asset_id_from_texture_id.push(asset_id.0); - texture_id + if let Some(texture_id)=acquire_id(content.as_ref()){ + //this is equivalent to a get_or_create pattern because there is a singular no-texture RenderId + //so RenderId==TextureId+1 + //not the most failsafe code but this is just for the map tool lmao + if unique_render_groups.len()==texture_id.get() as usize+1{ + unique_render_groups.push(RenderConfig::texture(texture_id)); }; + let render_id=RenderConfigId::new(texture_id.get()+1); let normal_id=normalid.to_u32(); if normal_id<6{ let (roblox_texture_color,roblox_texture_transform)=if decal.class=="Texture"{ @@ -394,7 +550,7 @@ pub fn generate_indexed_models(dom:rbx_dom_weak::WeakDom) -> model::IndexedModel (glam::Vec4::ONE,RobloxTextureTransform::default()) }; part_texture_description[normal_id as usize]=Some(RobloxFaceTextureDescription{ - texture:texture_id, + render:render_id, color:roblox_texture_color, transform:roblox_texture_transform, }); @@ -436,13 +592,13 @@ pub fn generate_indexed_models(dom:rbx_dom_weak::WeakDom) -> model::IndexedModel ]), }; //make new model if unit cube has not been created before - let model_id=if let Some(&model_id)=model_id_from_description.get(&basepart_texture_description){ + let indexed_model_id=if let Some(&indexed_model_id)=indexed_model_id_from_description.get(&basepart_texture_description){ //push to existing texture model - model_id + indexed_model_id }else{ - let model_id=indexed_models.len(); - model_id_from_description.insert(basepart_texture_description.clone(),model_id);//borrow checker going crazy - indexed_models.push(match basepart_texture_description{ + 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)=>{ @@ -460,7 +616,7 @@ pub fn generate_indexed_models(dom:rbx_dom_weak::WeakDom) -> model::IndexedModel }, match roblox_face_description{ Some(roblox_texture_transform)=>roblox_texture_transform.to_face_description(), - None=>primitives::FaceDescription::default(), + None=>primitives::FaceDescription::new_with_render_id(textureless_render_group), }); } primitives::generate_partial_unit_cube(cube_face_description) @@ -479,7 +635,7 @@ pub fn generate_indexed_models(dom:rbx_dom_weak::WeakDom) -> model::IndexedModel }, match roblox_face_description{ Some(roblox_texture_transform)=>roblox_texture_transform.to_face_description(), - None=>primitives::FaceDescription::default(), + None=>primitives::FaceDescription::new_with_render_id(textureless_render_group), }); } primitives::generate_partial_unit_wedge(wedge_face_description) @@ -498,27 +654,68 @@ pub fn generate_indexed_models(dom:rbx_dom_weak::WeakDom) -> model::IndexedModel }, match roblox_face_description{ Some(roblox_texture_transform)=>roblox_texture_transform.to_face_description(), - None=>primitives::FaceDescription::default(), + None=>primitives::FaceDescription::new_with_render_id(textureless_render_group), }); } primitives::generate_partial_unit_cornerwedge(cornerwedge_face_description) }, }); - model_id + indexed_model_id }; - indexed_models[model_id].instances.push(model::ModelInstance { + 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:get_attributes(&object.name,*can_collide,Planar64Vec3::try_from([velocity.x,velocity.y,velocity.z]).unwrap(),force_intersecting), - temp_indexing:temp_indexing_attributes, + attributes, }); } } } - model::IndexedModelInstances{ - textures:asset_id_from_texture_id.iter().map(|t|t.to_string()).collect(), - models:indexed_models, - spawn_point, - modes:Vec::new(), + 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{ + render_configs:unique_render_groups,//asset_id_from_texture_id.iter().map(|t|t.to_string()).collect(), + meshes, + models, + modes:modes_builder.build(), + attributes:unique_attributes, } }