2023-09-28 00:24:25 +00:00
|
|
|
use crate::model::{IndexedModelInstances,ModelInstance};
|
2023-09-23 02:41:27 +00:00
|
|
|
|
|
|
|
use crate::primitives;
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
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>{
|
|
|
|
let regman=regex::Regex::new(r"(\d+)$").unwrap();
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
2023-09-27 03:25:56 +00:00
|
|
|
#[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);
|
|
|
|
}
|
|
|
|
}
|
2023-09-28 22:43:45 +00:00
|
|
|
#[derive(Clone,PartialEq)]
|
|
|
|
struct RobloxFaceTextureDescription{
|
2023-09-27 03:25:56 +00:00
|
|
|
texture:u32,
|
2023-09-28 00:02:56 +00:00
|
|
|
color:glam::Vec4,
|
2023-09-27 03:25:56 +00:00
|
|
|
transform:RobloxTextureTransform,
|
|
|
|
}
|
2023-09-28 22:43:45 +00:00
|
|
|
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);
|
2023-09-28 22:43:45 +00:00
|
|
|
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,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-09-28 22:43:45 +00:00
|
|
|
type RobloxPartDescription=[Option<RobloxFaceTextureDescription>;6];
|
2023-09-30 23:18:23 +00:00
|
|
|
type RobloxWedgeDescription=[Option<RobloxFaceTextureDescription>;5];
|
|
|
|
type RobloxCornerWedgeDescription=[Option<RobloxFaceTextureDescription>;4];
|
2023-09-28 22:43:45 +00:00
|
|
|
#[derive(Clone,Eq,Hash,PartialEq)]
|
|
|
|
enum RobloxBasePartDescription{
|
2023-09-30 23:18:23 +00:00
|
|
|
Sphere,
|
2023-09-28 22:43:45 +00:00
|
|
|
Part(RobloxPartDescription),
|
2023-09-30 23:18:23 +00:00
|
|
|
Cylinder,
|
|
|
|
Wedge(RobloxWedgeDescription),
|
|
|
|
CornerWedge(RobloxCornerWedgeDescription),
|
2023-09-23 02:41:27 +00:00
|
|
|
}
|
2023-09-28 22:43:45 +00:00
|
|
|
pub fn generate_indexed_models_roblox(dom:rbx_dom_weak::WeakDom) -> Result<(IndexedModelInstances,glam::Vec3), Box<dyn std::error::Error>>{
|
|
|
|
//IndexedModelInstances includes textures
|
2023-09-23 02:41:27 +00:00
|
|
|
let mut spawn_point=glam::Vec3::ZERO;
|
|
|
|
|
2023-09-28 00:24:25 +00:00
|
|
|
let mut indexed_models=Vec::new();
|
2023-09-28 22:43:45 +00:00
|
|
|
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
|
|
|
|
2023-09-27 03:25:56 +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)),
|
|
|
|
Some(rbx_dom_weak::types::Variant::Float32(transparency)),
|
|
|
|
Some(rbx_dom_weak::types::Variant::Color3uint8(color3)),
|
|
|
|
Some(rbx_dom_weak::types::Variant::Enum(shape)),
|
|
|
|
) = (
|
|
|
|
object.properties.get("CFrame"),
|
|
|
|
object.properties.get("Size"),
|
|
|
|
object.properties.get("Transparency"),
|
|
|
|
object.properties.get("Color"),
|
|
|
|
object.properties.get("Shape"),//this will also skip unions
|
|
|
|
)
|
|
|
|
{
|
2023-09-26 23:56:19 +00:00
|
|
|
let model_transform=glam::Affine3A::from_translation(
|
|
|
|
glam::Vec3::new(cf.position.x,cf.position.y,cf.position.z)
|
|
|
|
)
|
|
|
|
* glam::Affine3A::from_mat3(
|
|
|
|
glam::Mat3::from_cols(
|
|
|
|
glam::Vec3::new(cf.orientation.x.x,cf.orientation.y.x,cf.orientation.z.x),
|
|
|
|
glam::Vec3::new(cf.orientation.x.y,cf.orientation.y.y,cf.orientation.z.y),
|
|
|
|
glam::Vec3::new(cf.orientation.x.z,cf.orientation.y.z,cf.orientation.z.z),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
* glam::Affine3A::from_scale(
|
|
|
|
glam::Vec3::new(size.x,size.y,size.z)/2.0
|
|
|
|
);
|
2023-09-23 02:41:27 +00:00
|
|
|
if object.name=="MapStart"{
|
2023-09-26 23:56:19 +00:00
|
|
|
spawn_point=model_transform.transform_point3(glam::Vec3::Y)+glam::vec3(0.0,2.5,0.0);
|
2023-09-23 02:41:27 +00:00
|
|
|
println!("Found MapStart{:?}",spawn_point);
|
|
|
|
}
|
2023-09-30 23:18:23 +00:00
|
|
|
if *transparency==1.0 {
|
2023-09-23 02:41:27 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2023-09-30 23:18:23 +00:00
|
|
|
let 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()),
|
|
|
|
};
|
|
|
|
|
|
|
|
//TODO: also detect "CylinderMesh" etc here
|
|
|
|
let mut face_map=std::collections::HashMap::new();
|
|
|
|
match shape{
|
|
|
|
primitives::Primitives::Cube => {
|
|
|
|
face_map.insert(0,0);//Right
|
|
|
|
face_map.insert(1,1);//Top
|
|
|
|
face_map.insert(2,2);//Back
|
|
|
|
face_map.insert(3,3);//Left
|
|
|
|
face_map.insert(4,4);//Bottom
|
|
|
|
face_map.insert(5,5);//Front
|
|
|
|
},
|
|
|
|
primitives::Primitives::Wedge => {
|
|
|
|
face_map.insert(0,0);//Right
|
|
|
|
face_map.insert(1,1);//Top -> TopFront (some surf maps put surf textures on the Top face)
|
|
|
|
face_map.insert(2,1);//Front -> TopFront
|
|
|
|
face_map.insert(3,2);//Back
|
|
|
|
face_map.insert(4,3);//Left
|
|
|
|
face_map.insert(5,4);//Bottom
|
|
|
|
},
|
|
|
|
primitives::Primitives::CornerWedge => {
|
|
|
|
//Right -> None
|
|
|
|
face_map.insert(1,0);//Top
|
|
|
|
//Back -> None
|
|
|
|
face_map.insert(3,1);//Right
|
|
|
|
face_map.insert(4,2);//Bottom
|
|
|
|
face_map.insert(5,3);//Front
|
|
|
|
},
|
|
|
|
//do not support textured spheres/cylinders imported from roblox
|
|
|
|
//this can be added later, there are some maps that use it
|
|
|
|
primitives::Primitives::Sphere
|
|
|
|
|primitives::Primitives::Cylinder => (),
|
|
|
|
}
|
|
|
|
//use the biggest one and cut it down later...
|
2023-09-28 22:43:45 +00:00
|
|
|
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){
|
2023-09-27 03:25:56 +00:00
|
|
|
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)),
|
2023-09-27 03:25:56 +00:00
|
|
|
) = (
|
|
|
|
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-27 03:25:56 +00:00
|
|
|
) {
|
2023-09-23 02:41:27 +00:00
|
|
|
if let Ok(asset_id)=content.clone().into_string().parse::<RobloxAssetId>(){
|
2023-09-27 03:25:56 +00:00
|
|
|
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{
|
2023-09-27 03:25:56 +00:00
|
|
|
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);
|
2023-09-27 03:25:56 +00:00
|
|
|
texture_id
|
|
|
|
};
|
2023-09-30 23:18:23 +00:00
|
|
|
let normal_id=normalid.to_u32();
|
|
|
|
if let Some(&face)=face_map.get(&normal_id){
|
2023-09-27 03:25:56 +00:00
|
|
|
let mut roblox_texture_transform=RobloxTextureTransform::default();
|
2023-09-28 00:02:56 +00:00
|
|
|
let mut roblox_texture_color=glam::Vec4::ONE;
|
2023-09-27 03:25:56 +00:00
|
|
|
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{
|
2023-09-27 22:01:18 +00:00
|
|
|
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
|
|
|
|
_=>(1.,1.),
|
|
|
|
};
|
2023-09-27 03:25:56 +00:00
|
|
|
roblox_texture_transform=RobloxTextureTransform{
|
2023-09-27 22:01:18 +00:00
|
|
|
offset_u:*ox/(*sx),offset_v:*oy/(*sy),
|
|
|
|
scale_u:size_u/(*sx),scale_v:size_v/(*sy),
|
|
|
|
};
|
2023-09-28 00:02:56 +00:00
|
|
|
roblox_texture_color=glam::vec4(decal_color3.r,decal_color3.g,decal_color3.b,1.0-*decal_transparency);
|
2023-09-27 03:25:56 +00:00
|
|
|
}
|
|
|
|
}
|
2023-09-30 23:18:23 +00:00
|
|
|
part_texture_description[face]=Some(RobloxFaceTextureDescription{
|
2023-09-27 03:25:56 +00:00
|
|
|
texture:texture_id,
|
2023-09-28 00:02:56 +00:00
|
|
|
color:roblox_texture_color,
|
2023-09-27 03:25:56 +00:00
|
|
|
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"
|
|
|
|
let [f0,f1,f2,f3,f4,f5]=part_texture_description;
|
|
|
|
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,
|
|
|
|
//HAHAHA
|
|
|
|
primitives::Primitives::Wedge=>RobloxBasePartDescription::Wedge([f0,f1,f2,f3,f4]),
|
|
|
|
primitives::Primitives::CornerWedge=>RobloxBasePartDescription::CornerWedge([f0,f1,f2,f3]),
|
|
|
|
};
|
2023-09-28 22:43:45 +00:00
|
|
|
//make new model if unit cube has not been crated 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(),
|
2023-09-28 22:43:45 +00:00
|
|
|
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-28 22:43:45 +00:00
|
|
|
},
|
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{
|
|
|
|
0=>primitives::CornerWedgeFace::Top,
|
|
|
|
1=>primitives::CornerWedgeFace::Right,
|
|
|
|
2=>primitives::CornerWedgeFace::Bottom,
|
|
|
|
3=>primitives::CornerWedgeFace::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_cornerwedge(cornerwedge_face_description)
|
|
|
|
},
|
|
|
|
});
|
|
|
|
model_id
|
2023-09-28 22:43:45 +00:00
|
|
|
};
|
|
|
|
indexed_models[model_id].instances.push(ModelInstance {
|
2023-09-30 20:00:01 +00:00
|
|
|
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-28 22:43:45 +00:00
|
|
|
});
|
2023-09-23 02:41:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-09-28 22:43:45 +00:00
|
|
|
Ok((IndexedModelInstances{
|
|
|
|
textures:asset_id_from_texture_id.iter().map(|t|t.to_string()).collect(),
|
|
|
|
models:indexed_models,
|
|
|
|
},spawn_point))
|
2023-09-06 00:45:34 +00:00
|
|
|
}
|