strafe-client-jed/src/load_roblox.rs

481 lines
19 KiB
Rust
Raw Normal View History

2023-09-23 02:41:27 +00:00
use crate::primitives;
2023-09-27 09:12:20 +00:00
use crate::integer::{Planar64,Planar64Vec3,Planar64Mat3,Planar64Affine3};
2023-09-23 02:41:27 +00:00
2023-09-06 00:45:34 +00:00
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
}
2023-09-23 02:41:27 +00:00
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){
for &referent in instance.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
}
recursive_collect_superclass(objects,dom,c,superclass);
2023-09-06 00:45:34 +00:00
}
}
2023-09-23 02:41:27 +00:00
}
fn get_texture_refs(dom:&rbx_dom_weak::WeakDom) -> Vec<rbx_dom_weak::types::Ref>{
let mut objects = std::vec::Vec::new();
recursive_collect_superclass(&mut objects, dom, dom.root(),"Decal");
//get ids
//clear vec
//next class
objects
}
2023-09-27 09:12:20 +00:00
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()
)
}
fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,force_intersecting:bool)->crate::model::CollisionAttributes{
2023-10-03 23:34:54 +00:00
let mut general=crate::model::GameMechanicAttributes::default();
let mut intersecting=crate::model::IntersectingAttributes::default();
let mut contacting=crate::model::ContactingAttributes::default();
let mut force_can_collide=can_collide;
2023-10-03 23:34:54 +00:00
match name{
2023-10-04 02:45:50 +00:00
//"Water"=>intersecting.water=Some(crate::model::IntersectingWater{density:1.0,drag:1.0}),
"Accelerator"=>{force_can_collide=false;intersecting.accelerator=Some(crate::model::IntersectingAccelerator{acceleration:velocity})},
"MapFinish"=>{force_can_collide=false;general.zone=Some(crate::model::GameMechanicZone{mode_id:0,behaviour:crate::model::ZoneBehaviour::Finish})},
"MapAnticheat"=>{force_can_collide=false;general.zone=Some(crate::model::GameMechanicZone{mode_id:0,behaviour:crate::model::ZoneBehaviour::Anitcheat})},
2023-10-03 23:34:54 +00:00
"Platform"=>general.stage_element=Some(crate::model::GameMechanicStageElement{
mode_id:0,
stage_id:0,
force:false,
behaviour:crate::model::StageElementBehaviour::Platform,
}),
other=>{
2023-10-04 21:07:20 +00:00
if let Some(captures)=lazy_regex::regex!(r"^(Force)?(Spawn|SpawnAt|Trigger|Teleport|Platform)(\d+)$")
2023-10-04 02:45:50 +00:00
.captures(other){
2023-10-03 23:34:54 +00:00
general.stage_element=Some(crate::model::GameMechanicStageElement{
mode_id:0,
stage_id:captures[3].parse::<u32>().unwrap(),
force:match captures.get(1){
Some(m)=>m.as_str()=="Force",
None=>false,
},
behaviour:match &captures[2]{
2023-10-04 21:07:20 +00:00
"Spawn"|"SpawnAt"=>crate::model::StageElementBehaviour::SpawnAt,
"Trigger"=>{force_can_collide=false;crate::model::StageElementBehaviour::Trigger},
"Teleport"=>{force_can_collide=false;crate::model::StageElementBehaviour::Teleport},
2023-10-03 23:34:54 +00:00
"Platform"=>crate::model::StageElementBehaviour::Platform,
2023-10-04 02:45:50 +00:00
_=>panic!("regex1[2] messed up bad"),
2023-10-03 23:34:54 +00:00
}
})
2023-10-04 02:45:50 +00:00
}else if let Some(captures)=lazy_regex::regex!(r"^Bonus(Finish|Anticheat)(\d+)$")
.captures(other){
force_can_collide=false;
2023-10-04 02:45:50 +00:00
match &captures[1]{
"Finish"=>general.zone=Some(crate::model::GameMechanicZone{mode_id:captures[2].parse::<u32>().unwrap(),behaviour:crate::model::ZoneBehaviour::Finish}),
"Anticheat"=>general.zone=Some(crate::model::GameMechanicZone{mode_id:captures[2].parse::<u32>().unwrap(),behaviour:crate::model::ZoneBehaviour::Anitcheat}),
_=>panic!("regex2[1] messed up bad"),
}
2023-10-03 23:34:54 +00:00
}
}
}
//need some way to skip this
2023-09-27 09:12:20 +00:00
if velocity!=Planar64Vec3::ZERO{
2023-10-03 23:34:54 +00:00
general.booster=Some(crate::model::GameMechanicBooster{velocity});
}
match force_can_collide{
2023-10-03 23:34:54 +00:00
true=>{
match name{
//"Bounce"=>(),
"Surf"=>contacting.surf=Some(crate::model::ContactingSurf{}),
"Ladder"=>contacting.ladder=Some(crate::model::ContactingLadder{sticky:true}),
other=>{
//REGEX!!!!
//Jump#
//WormholeIn#
}
}
2023-10-04 21:05:53 +00:00
crate::model::CollisionAttributes::Contact{contacting,general}
},
false=>if force_intersecting
||general.jump_limit.is_some()
||general.booster.is_some()
||general.zone.is_some()
||general.stage_element.is_some()
||general.wormhole.is_some()
||intersecting.water.is_some()
||intersecting.accelerator.is_some()
{
crate::model::CollisionAttributes::Intersect{intersecting,general}
}else{
crate::model::CollisionAttributes::Decoration
2023-10-03 23:34:54 +00:00
},
}
}
2023-09-23 02:41:27 +00:00
struct RobloxAssetId(u64);
struct RobloxAssetIdParseErr;
impl std::str::FromStr for RobloxAssetId {
2023-09-27 03:23:59 +00:00
type Err=RobloxAssetIdParseErr;
fn from_str(s: &str) -> Result<Self, Self::Err>{
2023-10-03 23:28:35 +00:00
let regman=lazy_regex::regex!(r"(\d+)$");
2023-09-27 03:23:59 +00:00
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{
texture:u32,
2023-09-28 00:02:56 +00:00
color:glam::Vec4,
transform:RobloxTextureTransform,
}
impl std::cmp::Eq for RobloxFaceTextureDescription{}//????
impl std::hash::Hash for RobloxFaceTextureDescription {
2023-09-28 00:02:56 +00:00
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.texture.hash(state);
self.transform.hash(state);
2023-09-28 00:02:56 +00:00
for &el in self.color.as_ref().iter() {
el.to_ne_bytes().hash(state);
}
}
}
2023-09-30 23:18:23 +00:00
impl RobloxFaceTextureDescription{
fn to_face_description(&self)->primitives::FaceDescription{
primitives::FaceDescription{
texture:Some(self.texture),
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];
2023-09-30 23:18:23 +00:00
type RobloxWedgeDescription=[Option<RobloxFaceTextureDescription>;5];
2023-10-08 04:46:40 +00:00
type RobloxCornerWedgeDescription=[Option<RobloxFaceTextureDescription>;5];
#[derive(Clone,Eq,Hash,PartialEq)]
enum RobloxBasePartDescription{
2023-09-30 23:18:23 +00:00
Sphere,
Part(RobloxPartDescription),
2023-09-30 23:18:23 +00:00
Cylinder,
Wedge(RobloxWedgeDescription),
CornerWedge(RobloxCornerWedgeDescription),
2023-09-23 02:41:27 +00:00
}
2023-10-02 22:27:41 +00:00
pub fn generate_indexed_models(dom:rbx_dom_weak::WeakDom) -> crate::model::IndexedModelInstances{
//IndexedModelInstances includes textures
2023-09-27 09:12:20 +00:00
let mut spawn_point=Planar64Vec3::ZERO;
2023-09-23 02:41:27 +00:00
2023-09-28 00:24:25 +00:00
let mut indexed_models=Vec::new();
let mut model_id_from_description=std::collections::HashMap::<RobloxBasePartDescription,usize>::new();
2023-09-23 02:41:27 +00:00
let mut texture_id_from_asset_id=std::collections::HashMap::<u64,u32>::new();
let mut asset_id_from_texture_id=Vec::new();
2023-09-06 00:45:34 +00:00
let mut object_refs=Vec::new();
let mut temp_objects=Vec::new();
2023-09-23 02:41:27 +00:00
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)),
2023-10-03 23:34:54 +00:00
Some(rbx_dom_weak::types::Variant::Vector3(velocity)),
2023-09-23 02:41:27 +00:00
Some(rbx_dom_weak::types::Variant::Float32(transparency)),
Some(rbx_dom_weak::types::Variant::Color3uint8(color3)),
2023-10-03 23:34:54 +00:00
Some(rbx_dom_weak::types::Variant::Bool(can_collide)),
2023-09-23 02:41:27 +00:00
) = (
object.properties.get("CFrame"),
object.properties.get("Size"),
2023-10-03 23:34:54 +00:00
object.properties.get("Velocity"),
2023-09-23 02:41:27 +00:00
object.properties.get("Transparency"),
object.properties.get("Color"),
2023-10-03 23:34:54 +00:00
object.properties.get("CanCollide"),
2023-09-23 02:41:27 +00:00
)
{
2023-09-27 09:12:20 +00:00
let model_transform=planar64_affine3_from_roblox(cf,size);
//push TempIndexedAttributes
2023-10-04 21:05:53 +00:00
let mut force_intersecting=false;
let mut temp_indexing_attributes=Vec::new();
if let Some(attr)=match &object.name[..]{
"MapStart"=>{
2023-09-27 09:12:20 +00:00
spawn_point=model_transform.transform_point3(Planar64Vec3::ZERO)+Planar64Vec3::Y*5/2;
Some(crate::model::TempIndexedAttributes::Start{mode_id:0})
},
"UnorderedCheckpoint"=>Some(crate::model::TempIndexedAttributes::UnorderedCheckpoint{mode_id:0}),
other=>{
2023-10-04 21:07:20 +00:00
let regman=lazy_regex::regex!(r"^(BonusStart|Spawn|ForceSpawn|OrderedCheckpoint)(\d+)$");
if let Some(captures) = regman.captures(other) {
match &captures[1]{
"BonusStart"=>Some(crate::model::TempIndexedAttributes::Start{mode_id:captures[2].parse::<u32>().unwrap()}),
2023-10-04 21:07:20 +00:00
"Spawn"|"ForceSpawn"=>Some(crate::model::TempIndexedAttributes::Spawn{mode_id:0,stage_id:captures[2].parse::<u32>().unwrap()}),
"OrderedCheckpoint"=>Some(crate::model::TempIndexedAttributes::OrderedCheckpoint{mode_id:0,checkpoint_id:captures[2].parse::<u32>().unwrap()}),
_=>None,
}
}else{
None
}
}
}{
2023-10-04 21:05:53 +00:00
force_intersecting=true;
temp_indexing_attributes.push(attr);
2023-09-23 02:41:27 +00:00
}
2023-10-02 08:57:56 +00:00
//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,
2023-10-02 08:58:30 +00:00
_=>panic!("Funky roblox PartType={};",shape.to_u32()),
}
}else{
2023-10-02 08:58:30 +00:00
panic!("Part has no Shape!");
}
},
"WedgePart"=>primitives::Primitives::Wedge,
"CornerWedgePart"=>primitives::Primitives::CornerWedge,
_=>{
println!("Unsupported BasePart ClassName={}; defaulting to cube",object.class);
primitives::Primitives::Cube
}
2023-09-30 23:18:23 +00:00
};
//use the biggest one and cut it down later...
let mut part_texture_description:RobloxPartDescription=[None,None,None,None,None,None];
2023-09-30 23:18:23 +00:00
temp_objects.clear();
recursive_collect_superclass(&mut temp_objects, &dom, object,"Decal");
2023-09-23 02:41:27 +00:00
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)),
2023-09-28 00:02:56 +00:00
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"),
2023-09-28 00:02:56 +00:00
decal.properties.get("Color3"),
decal.properties.get("Transparency"),
) {
2023-09-23 02:41:27 +00:00
if let Ok(asset_id)=content.clone().into_string().parse::<RobloxAssetId>(){
let texture_id=if let Some(&texture_id)=texture_id_from_asset_id.get(&asset_id.0){
texture_id
2023-09-23 02:41:27 +00:00
}else{
let texture_id=asset_id_from_texture_id.len() as u32;
texture_id_from_asset_id.insert(asset_id.0,texture_id);
2023-09-23 02:41:27 +00:00
asset_id_from_texture_id.push(asset_id.0);
texture_id
};
2023-09-30 23:18:23 +00:00
let normal_id=normalid.to_u32();
2023-10-02 08:57:56 +00:00
if normal_id<6{
2023-10-07 21:12:39 +00:00
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"),
)
{
2023-09-30 23:18:23 +00:00
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
2023-10-02 08:57:56 +00:00
_=>panic!("unreachable"),
};
2023-10-07 21:12:39 +00:00
(
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())
}
2023-10-07 21:12:39 +00:00
}else{
(glam::Vec4::ONE,RobloxTextureTransform::default())
};
2023-10-02 08:57:56 +00:00
part_texture_description[normal_id as usize]=Some(RobloxFaceTextureDescription{
texture:texture_id,
2023-09-28 00:02:56 +00:00
color:roblox_texture_color,
transform:roblox_texture_transform,
});
}else{
2023-09-30 23:18:23 +00:00
println!("NormalId={} unsupported for shape={:?}",normal_id,shape);
2023-09-23 02:41:27 +00:00
}
}
}
}
}
2023-09-30 23:18:23 +00:00
//obscure rust syntax "slice pattern"
2023-10-04 00:20:52 +00:00
let [
f0,//Cube::Right
f1,//Cube::Top
f2,//Cube::Back
f3,//Cube::Left
f4,//Cube::Bottom
f5,//Cube::Front
]=part_texture_description;
2023-09-30 23:18:23 +00:00
let basepart_texture_description=match shape{
primitives::Primitives::Sphere=>RobloxBasePartDescription::Sphere,
primitives::Primitives::Cube=>RobloxBasePartDescription::Part([f0,f1,f2,f3,f4,f5]),
primitives::Primitives::Cylinder=>RobloxBasePartDescription::Cylinder,
2023-10-02 08:57:56 +00:00
//use front face texture first and use top face texture as a fallback
2023-10-04 00:20:52 +00:00
primitives::Primitives::Wedge=>RobloxBasePartDescription::Wedge([
2023-10-04 21:16:04 +00:00
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
2023-10-04 00:20:52 +00:00
]),
2023-10-08 04:46:40 +00:00
//TODO: fix Left+Back texture coordinates to match roblox when not overwridden by Top
2023-10-04 00:20:52 +00:00
primitives::Primitives::CornerWedge=>RobloxBasePartDescription::CornerWedge([
2023-10-04 21:16:04 +00:00
f0,//Cube::Right->CornerWedge::Right
2023-10-08 04:46:40 +00:00
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
2023-10-04 21:16:04 +00:00
f4,//Cube::Bottom->CornerWedge::Bottom
f5,//Cube::Front->CornerWedge::Front
2023-10-04 00:20:52 +00:00
]),
2023-09-30 23:18:23 +00:00
};
2023-10-02 08:58:35 +00:00
//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){
//push to existing texture model
model_id
}else{
let model_id=indexed_models.len();
model_id_from_description.insert(basepart_texture_description.clone(),model_id);//borrow checker going crazy
2023-09-30 23:18:23 +00:00
indexed_models.push(match basepart_texture_description{
RobloxBasePartDescription::Sphere=>primitives::unit_sphere(),
RobloxBasePartDescription::Part(part_texture_description)=>{
2023-09-30 23:18:23 +00:00
let mut cube_face_description=primitives::CubeFaceDescription::new();
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::default(),
});
}
primitives::generate_partial_unit_cube(cube_face_description)
},
2023-09-30 23:18:23 +00:00
RobloxBasePartDescription::Cylinder=>primitives::unit_cylinder(),
RobloxBasePartDescription::Wedge(wedge_texture_description)=>{
let mut wedge_face_description=primitives::WedgeFaceDescription::new();
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::default(),
});
}
primitives::generate_partial_unit_wedge(wedge_face_description)
},
RobloxBasePartDescription::CornerWedge(cornerwedge_texture_description)=>{
let mut cornerwedge_face_description=primitives::CornerWedgeFaceDescription::new();
for (face_id,roblox_face_description) in cornerwedge_texture_description.iter().enumerate(){
cornerwedge_face_description.insert(
match face_id{
2023-10-08 04:46:40 +00:00
0=>primitives::CornerWedgeFace::Right,
1=>primitives::CornerWedgeFace::TopBack,
2=>primitives::CornerWedgeFace::TopLeft,
3=>primitives::CornerWedgeFace::Bottom,
4=>primitives::CornerWedgeFace::Front,
2023-09-30 23:18:23 +00:00
_=>panic!("unreachable"),
},
match roblox_face_description{
Some(roblox_texture_transform)=>roblox_texture_transform.to_face_description(),
None=>primitives::FaceDescription::default(),
});
}
primitives::generate_partial_unit_cornerwedge(cornerwedge_face_description)
},
});
model_id
};
2023-10-02 22:27:41 +00:00
indexed_models[model_id].instances.push(crate::model::ModelInstance {
transform:model_transform,
2023-09-29 08:58:31 +00:00
color:glam::vec4(color3.r as f32/255f32, color3.g as f32/255f32, color3.b as f32/255f32, 1.0-*transparency),
2023-09-27 09:12:20 +00:00
attributes:get_attributes(&object.name,*can_collide,Planar64Vec3::try_from([velocity.x,velocity.y,velocity.z]).unwrap(),force_intersecting),
temp_indexing:temp_indexing_attributes,
});
2023-09-23 02:41:27 +00:00
}
}
}
2023-10-02 22:27:41 +00:00
crate::model::IndexedModelInstances{
textures:asset_id_from_texture_id.iter().map(|t|t.to_string()).collect(),
models:indexed_models,
2023-10-02 22:27:41 +00:00
spawn_point,
2023-10-04 02:42:07 +00:00
modes:Vec::new(),
2023-10-02 22:27:41 +00:00
}
2023-09-06 00:45:34 +00:00
}