743 lines
28 KiB
Rust
743 lines
28 KiB
Rust
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<rbx_dom_weak::types::Ref>,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<gameplay_modes::StageId,gameplay_modes::StageId>,
|
|
}
|
|
#[derive(Default)]
|
|
struct ModesBuilder{
|
|
modes:HashMap<gameplay_modes::ModeId,gameplay_modes::Mode>,
|
|
stages:HashMap<gameplay_modes::ModeId,HashMap<gameplay_modes::StageId,gameplay_modes::Stage>>,
|
|
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<ModeBuilder>,HashMap<gameplay_modes::ModeId,gameplay_modes::ModeId>)
|
|
=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<model::ModelId,u32>,wormhole_id_to_out_model:&mut HashMap<u32,model::ModelId>)->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(gameplay_modes::StageId::FIRST,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::<u32>().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::<u32>().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_id=gameplay_modes::StageId::new(captures[3].parse::<u32>().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"=>{
|
|
modes_builder.insert_stage(
|
|
gameplay_modes::ModeId::MAIN,
|
|
stage_id,
|
|
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::<u32>().unwrap()
|
|
),
|
|
),
|
|
"WormholeIn"=>{
|
|
force_can_collide=false;
|
|
force_intersecting=true;
|
|
assert!(wormhole_in_model_to_id.insert(model_id,captures[2].parse::<u32>().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::<u32>().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::<u32>().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<Self,Self::Err>{
|
|
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::<u64>(){
|
|
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<H:std::hash::Hasher>(&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<H:std::hash::Hasher>(&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<RobloxFaceTextureDescription>;6];
|
|
type RobloxWedgeDescription=[Option<RobloxFaceTextureDescription>;5];
|
|
type RobloxCornerWedgeDescription=[Option<RobloxFaceTextureDescription>;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::<u64,RenderConfigId>::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::<RobloxAssetId>(){
|
|
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,
|
|
}
|
|
}
|