Compare commits

..

12 Commits

Author SHA1 Message Date
af86be029b new bug who dis 2025-03-14 14:48:25 -07:00
7f20e73588 ma 2025-03-14 14:43:41 -07:00
9308cc8a5c debug 2025-03-14 14:43:41 -07:00
2f574b297f debug 2025-03-14 14:43:41 -07:00
7c306da7b0 physics debug view plan 2025-03-14 14:43:41 -07:00
6e9d38604e debug 2025-03-14 14:43:41 -07:00
a52d46b7cc debug 2025-03-14 14:43:41 -07:00
9ecb494748 isolate bug 2025-03-14 14:43:41 -07:00
1f73a8b5c6 it: physics bug 2 2025-03-14 14:43:41 -07:00
dbae80b1d2 fixme 2025-03-14 14:37:31 -07:00
8cc79304dc bsp_loader: max area triangulation 2025-03-14 14:34:16 -07:00
8e4792269d bsp_loader: truncate vertex precision to 16 bits
The physics algorithm expects vertices to align exactly with faces.  Since the face normal is calculated via the cross product of vertex positions, this allows the face normals to be exact with respect to the vertex positions.
2025-03-14 14:34:16 -07:00
71 changed files with 2026 additions and 3573 deletions

1053
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -24,7 +24,3 @@ resolver = "2"
#lto = true #lto = true
strip = true strip = true
codegen-units = 1 codegen-units = 1
[profile.dev]
strip = false
opt-level = 3

@ -11,4 +11,4 @@ id = { version = "0.1.0", registry = "strafesnet" }
strafesnet_common = { path = "../../lib/common", registry = "strafesnet" } strafesnet_common = { path = "../../lib/common", registry = "strafesnet" }
strafesnet_session = { path = "../session", registry = "strafesnet" } strafesnet_session = { path = "../session", registry = "strafesnet" }
strafesnet_settings = { path = "../settings", registry = "strafesnet" } strafesnet_settings = { path = "../settings", registry = "strafesnet" }
wgpu = "25.0.0" wgpu = "24.0.0"

@ -11,6 +11,15 @@ pub fn required_limits()->wgpu::Limits{
wgpu::Limits::default() wgpu::Limits::default()
} }
// physics debug view:
// render two meshes adjacent according to Minkowski FEV
// render additional mesh at path point
// render path parabola
// highlight meshes according to Minkowski FEV
//
// debug process:
// press a button to step physics by one transition
struct Indices{ struct Indices{
count:u32, count:u32,
buf:wgpu::Buffer, buf:wgpu::Buffer,
@ -103,26 +112,6 @@ impl std::default::Default for GraphicsCamera{
} }
} }
const MODEL_BUFFER_SIZE:usize=4*4 + 12 + 4;//let size=std::mem::size_of::<ModelInstance>();
const MODEL_BUFFER_SIZE_BYTES:usize=MODEL_BUFFER_SIZE*4;
fn get_instances_buffer_data(instances:&[GraphicsModelOwned])->Vec<f32>{
let mut raw=Vec::with_capacity(MODEL_BUFFER_SIZE*instances.len());
for mi in instances{
//model transform
raw.extend_from_slice(&AsRef::<[f32; 4*4]>::as_ref(&mi.transform)[..]);
//normal transform
raw.extend_from_slice(AsRef::<[f32; 3]>::as_ref(&mi.normal_transform.x_axis));
raw.extend_from_slice(&[0.0]);
raw.extend_from_slice(AsRef::<[f32; 3]>::as_ref(&mi.normal_transform.y_axis));
raw.extend_from_slice(&[0.0]);
raw.extend_from_slice(AsRef::<[f32; 3]>::as_ref(&mi.normal_transform.z_axis));
raw.extend_from_slice(&[0.0]);
//color
raw.extend_from_slice(AsRef::<[f32; 4]>::as_ref(&mi.color.get()));
}
raw
}
pub struct GraphicsState{ pub struct GraphicsState{
pipelines:GraphicsPipelines, pipelines:GraphicsPipelines,
bind_groups:GraphicsBindGroups, bind_groups:GraphicsBindGroups,
@ -987,3 +976,22 @@ impl GraphicsState{
self.staging_belt.recall(); self.staging_belt.recall();
} }
} }
const MODEL_BUFFER_SIZE:usize=4*4 + 12 + 4;//let size=std::mem::size_of::<ModelInstance>();
const MODEL_BUFFER_SIZE_BYTES:usize=MODEL_BUFFER_SIZE*4;
fn get_instances_buffer_data(instances:&[GraphicsModelOwned])->Vec<f32>{
let mut raw=Vec::with_capacity(MODEL_BUFFER_SIZE*instances.len());
for mi in instances{
//model transform
raw.extend_from_slice(&AsRef::<[f32; 4*4]>::as_ref(&mi.transform)[..]);
//normal transform
raw.extend_from_slice(AsRef::<[f32; 3]>::as_ref(&mi.normal_transform.x_axis));
raw.extend_from_slice(&[0.0]);
raw.extend_from_slice(AsRef::<[f32; 3]>::as_ref(&mi.normal_transform.y_axis));
raw.extend_from_slice(&[0.0]);
raw.extend_from_slice(AsRef::<[f32; 3]>::as_ref(&mi.normal_transform.z_axis));
raw.extend_from_slice(&[0.0]);
//color
raw.extend_from_slice(AsRef::<[f32; 4]>::as_ref(&mi.color.get()));
}
raw
}

@ -18,17 +18,6 @@ impl<T> std::ops::Neg for Body<T>{
} }
} }
} }
impl<T:Copy> std::ops::Neg for &Body<T>{
type Output=Body<T>;
fn neg(self)->Self::Output{
Body{
position:self.position,
velocity:-self.velocity,
acceleration:self.acceleration,
time:-self.time,
}
}
}
impl<T> Body<T> impl<T> Body<T>
where Time<T>:Copy, where Time<T>:Copy,

@ -1,9 +1,8 @@
use crate::model::{into_giga_time,GigaTime,FEV,MeshQuery,DirectedEdge}; use crate::model::{GigaTime,FEV,MeshQuery,DirectedEdge};
use strafesnet_common::integer::{Fixed,Ratio,vec3::Vector3}; use strafesnet_common::integer::{Fixed,Ratio,vec3::Vector3};
use crate::physics::{Time,Body}; use crate::physics::{Time,Body};
use core::ops::Bound; #[derive(Debug)]
enum Transition<M:MeshQuery>{ enum Transition<M:MeshQuery>{
Miss, Miss,
Next(FEV<M>,GigaTime), Next(FEV<M>,GigaTime),
@ -29,58 +28,18 @@ impl<M:MeshQuery> CrawlResult<M>{
} }
} }
// TODO: move predict_collision_face_out algorithm in here or something impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>+std::fmt::Debug> FEV<M>
/// check_lower_bound
pub fn low<LhsNum,LhsDen,RhsNum,RhsDen,T>(lower_bound:&Bound<Ratio<LhsNum,LhsDen>>,dt:&Ratio<RhsNum,RhsDen>)->bool
where
RhsNum:Copy,
RhsDen:Copy,
LhsNum:Copy,
LhsDen:Copy,
LhsDen:strafesnet_common::integer::Parity,
RhsDen:strafesnet_common::integer::Parity,
LhsNum:core::ops::Mul<RhsDen,Output=T>,
LhsDen:core::ops::Mul<RhsNum,Output=T>,
T:Ord+Copy,
{
match lower_bound{
Bound::Included(time)=>time.le_ratio(*dt),
Bound::Excluded(time)=>time.lt_ratio(*dt),
Bound::Unbounded=>true,
}
}
/// check_upper_bound
pub fn upp<LhsNum,LhsDen,RhsNum,RhsDen,T>(dt:&Ratio<LhsNum,LhsDen>,upper_bound:&Bound<Ratio<RhsNum,RhsDen>>)->bool
where
RhsNum:Copy,
RhsDen:Copy,
LhsNum:Copy,
LhsDen:Copy,
LhsDen:strafesnet_common::integer::Parity,
RhsDen:strafesnet_common::integer::Parity,
LhsNum:core::ops::Mul<RhsDen,Output=T>,
LhsDen:core::ops::Mul<RhsNum,Output=T>,
T:Ord+Copy,
{
match upper_bound{
Bound::Included(time)=>dt.le_ratio(*time),
Bound::Excluded(time)=>dt.lt_ratio(*time),
Bound::Unbounded=>true,
}
}
impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
where where
// This is hardcoded for MinkowskiMesh lol // This is hardcoded for MinkowskiMesh lol
M::Face:Copy, M::Face:Copy,
M::Edge:Copy, M::Edge:Copy,
M::Vert:Copy, M::Vert:Copy,
F:core::ops::Mul<Fixed<1,32>,Output=Fixed<4,128>>, F:core::ops::Mul<Fixed<1,32>,Output=Fixed<4,128>>+std::fmt::Debug+std::fmt::Display,
<F as core::ops::Mul<Fixed<1,32>>>::Output:core::iter::Sum, <F as core::ops::Mul<Fixed<1,32>>>::Output:core::iter::Sum,
M::Offset:core::ops::Sub<<F as std::ops::Mul<Fixed<1,32>>>::Output>, <M as MeshQuery>::Offset:core::ops::Sub<<F as std::ops::Mul<Fixed<1,32>>>::Output>,
{ {
fn next_transition(&self,mesh:&M,body:&Body,lower_bound:Bound<GigaTime>,mut upper_bound:Bound<GigaTime>)->Transition<M>{ fn next_transition(&self,body_time:GigaTime,mesh:&M,body:&Body,mut best_time:GigaTime)->Transition<M>{
println!("next_transition fev={self:?}");
//conflicting derivative means it crosses in the wrong direction. //conflicting derivative means it crosses in the wrong direction.
//if the transition time is equal to an already tested transition, do not replace the current best. //if the transition time is equal to an already tested transition, do not replace the current best.
let mut best_transition=Transition::Miss; let mut best_transition=Transition::Miss;
@ -93,8 +52,8 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
//TODO: use higher precision d value? //TODO: use higher precision d value?
//use the mesh transform translation instead of baking it into the d value. //use the mesh transform translation instead of baking it into the d value.
for dt in Fixed::<4,128>::zeroes2((n.dot(body.position)-d)*2,n.dot(body.velocity)*2,n.dot(body.acceleration)){ for dt in Fixed::<4,128>::zeroes2((n.dot(body.position)-d)*2,n.dot(body.velocity)*2,n.dot(body.acceleration)){
if low(&lower_bound,&dt)&&upp(&dt,&upper_bound)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){ if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
upper_bound=Bound::Included(dt); best_time=dt;
best_transition=Transition::Hit(face_id,dt); best_transition=Transition::Hit(face_id,dt);
break; break;
} }
@ -108,8 +67,8 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
//WARNING: precision is swept under the rug! //WARNING: precision is swept under the rug!
//wrap for speed //wrap for speed
for dt in Fixed::<4,128>::zeroes2(n.dot(body.position*2-(mesh.vert(v0)+mesh.vert(v1))).wrap_4(),n.dot(body.velocity).wrap_4()*2,n.dot(body.acceleration).wrap_4()){ for dt in Fixed::<4,128>::zeroes2(n.dot(body.position*2-(mesh.vert(v0)+mesh.vert(v1))).wrap_4(),n.dot(body.velocity).wrap_4()*2,n.dot(body.acceleration).wrap_4()){
if low(&lower_bound,&dt)&&upp(&dt,&upper_bound)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){ if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
upper_bound=Bound::Included(dt); best_time=dt;
best_transition=Transition::Next(FEV::Edge(directed_edge_id.as_undirected()),dt); best_transition=Transition::Next(FEV::Edge(directed_edge_id.as_undirected()),dt);
break; break;
} }
@ -118,21 +77,22 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
//if none: //if none:
}, },
&FEV::Edge(edge_id)=>{ &FEV::Edge(edge_id)=>{
println!("test edge={edge_id:?}");
//test each face collision time, ignoring roots with zero or conflicting derivative //test each face collision time, ignoring roots with zero or conflicting derivative
let edge_n=mesh.edge_n(edge_id);
let edge_verts=mesh.edge_verts(edge_id); let edge_verts=mesh.edge_verts(edge_id);
let &[ev0,ev1]=edge_verts.as_ref(); let &[ev0,ev1]=edge_verts.as_ref();
let (v0,v1)=(mesh.vert(ev0),mesh.vert(ev1)); let delta_pos=body.position*2-(mesh.vert(ev0)+mesh.vert(ev1));
let edge_n=v1-v0;
let delta_pos=body.position*2-(v0+v1);
for (i,&edge_face_id) in mesh.edge_faces(edge_id).as_ref().iter().enumerate(){ for (i,&edge_face_id) in mesh.edge_faces(edge_id).as_ref().iter().enumerate(){
let face_n=mesh.face_nd(edge_face_id).0; let face_n=mesh.face_nd(edge_face_id).0;
//edge_n gets parity from the order of edge_faces //edge_n gets parity from the order of edge_faces
let n=face_n.cross(edge_n)*((i as i64)*2-1); let n=face_n.cross(edge_n)*((i as i64)*2-1);
println!("edge_face={edge_face_id:?} face_n={face_n} n={n}");
//WARNING yada yada d *2 //WARNING yada yada d *2
//wrap for speed //wrap for speed
for dt in Fixed::<4,128>::zeroes2(n.dot(delta_pos).wrap_4(),n.dot(body.velocity).wrap_4()*2,n.dot(body.acceleration).wrap_4()){ for dt in Fixed::<4,128>::zeroes2(n.dot(delta_pos).wrap_4(),n.dot(body.velocity).wrap_4()*2,n.dot(body.acceleration).wrap_4()){
if low(&lower_bound,&dt)&&upp(&dt,&upper_bound)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){ if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
upper_bound=Bound::Included(dt); best_time=dt;
best_transition=Transition::Next(FEV::Face(edge_face_id),dt); best_transition=Transition::Next(FEV::Face(edge_face_id),dt);
break; break;
} }
@ -143,9 +103,9 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
//vertex normal gets parity from vert index //vertex normal gets parity from vert index
let n=edge_n*(1-2*(i as i64)); let n=edge_n*(1-2*(i as i64));
for dt in Fixed::<2,64>::zeroes2((n.dot(body.position-mesh.vert(vert_id)))*2,n.dot(body.velocity)*2,n.dot(body.acceleration)){ for dt in Fixed::<2,64>::zeroes2((n.dot(body.position-mesh.vert(vert_id)))*2,n.dot(body.velocity)*2,n.dot(body.acceleration)){
if low(&lower_bound,&dt)&&upp(&dt,&upper_bound)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){ if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
let dt=Ratio::new(dt.num.widen_4(),dt.den.widen_4()); let dt=Ratio::new(dt.num.widen_4(),dt.den.widen_4());
upper_bound=Bound::Included(dt); best_time=dt;
best_transition=Transition::Next(FEV::Vert(vert_id),dt); best_transition=Transition::Next(FEV::Vert(vert_id),dt);
break; break;
} }
@ -159,9 +119,9 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
//edge is directed away from vertex, but we want the dot product to turn out negative //edge is directed away from vertex, but we want the dot product to turn out negative
let n=-mesh.directed_edge_n(directed_edge_id); let n=-mesh.directed_edge_n(directed_edge_id);
for dt in Fixed::<2,64>::zeroes2((n.dot(body.position-mesh.vert(vert_id)))*2,n.dot(body.velocity)*2,n.dot(body.acceleration)){ for dt in Fixed::<2,64>::zeroes2((n.dot(body.position-mesh.vert(vert_id)))*2,n.dot(body.velocity)*2,n.dot(body.acceleration)){
if low(&lower_bound,&dt)&&upp(&dt,&upper_bound)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){ if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
let dt=Ratio::new(dt.num.widen_4(),dt.den.widen_4()); let dt=Ratio::new(dt.num.widen_4(),dt.den.widen_4());
upper_bound=Bound::Included(dt); best_time=dt;
best_transition=Transition::Next(FEV::Edge(directed_edge_id.as_undirected()),dt); best_transition=Transition::Next(FEV::Edge(directed_edge_id.as_undirected()),dt);
break; break;
} }
@ -172,13 +132,21 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
} }
best_transition best_transition
} }
pub fn crawl(mut self,mesh:&M,relative_body:&Body,lower_bound:Bound<&Time>,upper_bound:Bound<&Time>)->CrawlResult<M>{ pub fn crawl(mut self,mesh:&M,relative_body:&Body,start_time:Time,time_limit:Time)->CrawlResult<M>{
let mut lower_bound=lower_bound.map(|&t|into_giga_time(t,relative_body.time)); let mut body_time={
let upper_bound=upper_bound.map(|&t|into_giga_time(t,relative_body.time)); let r=(start_time-relative_body.time).to_ratio();
Ratio::new(r.num.widen_4(),r.den.widen_4())
};
let time_limit={
let r=(time_limit-relative_body.time).to_ratio();
Ratio::new(r.num.widen_4(),r.den.widen_4())
};
for _ in 0..20{ for _ in 0..20{
match self.next_transition(mesh,relative_body,lower_bound,upper_bound){ let transition=self.next_transition(body_time,mesh,relative_body,time_limit);
println!("transition={transition:?}");
match transition{
Transition::Miss=>return CrawlResult::Miss(self), Transition::Miss=>return CrawlResult::Miss(self),
Transition::Next(next_fev,next_time)=>(self,lower_bound)=(next_fev,Bound::Included(next_time)), Transition::Next(next_fev,next_time)=>(self,body_time)=(next_fev,next_time),
Transition::Hit(face,time)=>return CrawlResult::Hit(face,time), Transition::Hit(face,time)=>return CrawlResult::Hit(face,time),
} }
} }

@ -1,5 +1,5 @@
use std::collections::{HashSet,HashMap}; use std::collections::{HashSet,HashMap};
use core::ops::{Bound,RangeBounds}; use core::ops::Range;
use strafesnet_common::integer::vec3::Vector3; use strafesnet_common::integer::vec3::Vector3;
use strafesnet_common::model::{self,MeshId,PolygonIter}; use strafesnet_common::model::{self,MeshId,PolygonIter};
use strafesnet_common::integer::{self,vec3,Fixed,Planar64,Planar64Vec3,Ratio}; use strafesnet_common::integer::{self,vec3,Fixed,Planar64,Planar64Vec3,Ratio};
@ -68,7 +68,7 @@ pub enum FEV<M:MeshQuery>{
} }
//use Unit32 #[repr(C)] for map files //use Unit32 #[repr(C)] for map files
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq)] #[derive(Clone,Debug,Hash,Eq,PartialEq)]
struct Face{ struct Face{
normal:Planar64Vec3, normal:Planar64Vec3,
dot:Planar64, dot:Planar64,
@ -76,9 +76,9 @@ struct Face{
#[derive(Debug)] #[derive(Debug)]
struct Vert(Planar64Vec3); struct Vert(Planar64Vec3);
pub trait MeshQuery{ pub trait MeshQuery{
type Face:Copy; type Face:Copy+std::fmt::Debug;
type Edge:Copy+DirectedEdge; type Edge:Copy+DirectedEdge+std::fmt::Debug;
type Vert:Copy; type Vert:Copy+std::fmt::Debug;
// Vertex must be Planar64Vec3 because it represents an actual position // Vertex must be Planar64Vec3 because it represents an actual position
type Normal; type Normal;
type Offset; type Offset;
@ -149,12 +149,10 @@ impl From<MeshId> for PhysicsMeshId{
pub struct PhysicsSubmeshId(u32); pub struct PhysicsSubmeshId(u32);
pub struct PhysicsMesh{ pub struct PhysicsMesh{
data:PhysicsMeshData, data:PhysicsMeshData,
// The complete mesh is unused at this time. complete_mesh:PhysicsMeshTopology,
// complete_mesh:PhysicsMeshTopology, //Most objects in roblox maps are already convex, so the list length is 0
// Submeshes are guaranteed to be convex and may contain //as soon as the mesh is divided into 2 submeshes, the list length jumps to 2.
// "virtual" faces which are not part of the complete mesh. //length 1 is unnecessary since the complete mesh would be a duplicate of the only submesh, but would still function properly
// Physics calculations should never resolve to hitting
// a virtual face.
submeshes:Vec<PhysicsMeshTopology>, submeshes:Vec<PhysicsMeshTopology>,
} }
impl PhysicsMesh{ impl PhysicsMesh{
@ -218,24 +216,19 @@ impl PhysicsMesh{
}; };
Self{ Self{
data, data,
// complete_mesh:mesh_topology.clone(), complete_mesh:mesh_topology,
submeshes:vec![mesh_topology], submeshes:Vec::new(),
} }
} }
pub fn unit_cylinder()->Self{ pub fn unit_cylinder()->Self{
Self::unit_cube() Self::unit_cube()
} }
#[inline] #[inline]
pub fn complete_mesh(&self)->&PhysicsMeshTopology{ pub const fn complete_mesh(&self)->&PhysicsMeshTopology{
// If there is exactly one submesh, then the complete mesh is identical to it. &self.complete_mesh
if self.submeshes.len()==1{
self.submeshes.first().unwrap()
}else{
panic!("PhysicsMesh complete mesh is not known");
}
} }
#[inline] #[inline]
pub fn complete_mesh_view(&self)->PhysicsMeshView{ pub const fn complete_mesh_view(&self)->PhysicsMeshView{
PhysicsMeshView{ PhysicsMeshView{
data:&self.data, data:&self.data,
topology:self.complete_mesh(), topology:self.complete_mesh(),
@ -243,7 +236,12 @@ impl PhysicsMesh{
} }
#[inline] #[inline]
pub fn submeshes(&self)->&[PhysicsMeshTopology]{ pub fn submeshes(&self)->&[PhysicsMeshTopology]{
&self.submeshes //the complete mesh is already a convex mesh when len()==0, len()==1 is invalid but will still work
if self.submeshes.len()==0{
std::slice::from_ref(&self.complete_mesh)
}else{
&self.submeshes.as_slice()
}
} }
#[inline] #[inline]
pub fn submesh_view(&self,submesh_id:PhysicsSubmeshId)->PhysicsMeshView{ pub fn submesh_view(&self,submesh_id:PhysicsSubmeshId)->PhysicsMeshView{
@ -321,20 +319,17 @@ impl TryFrom<&model::Mesh> for PhysicsMesh{
if mesh.unique_pos.len()==0{ if mesh.unique_pos.len()==0{
return Err(PhysicsMeshError::ZeroVertices); return Err(PhysicsMeshError::ZeroVertices);
} }
// An empty physics mesh is a waste of resources
if mesh.physics_groups.len()==0{
return Err(PhysicsMeshError::NoPhysicsGroups);
}
let verts=mesh.unique_pos.iter().copied().map(Vert).collect(); let verts=mesh.unique_pos.iter().copied().map(Vert).collect();
// TODO: do not hash faces to get face id
// meshes can have multiple identical nd representations while still being distinct faces,
// especially when the complete mesh is a non-convex mesh.
//TODO: fix submeshes //TODO: fix submeshes
//flat map mesh.physics_groups[$1].groups.polys()[$2] as face_id //flat map mesh.physics_groups[$1].groups.polys()[$2] as face_id
//lower face_id points to upper face_id //lower face_id points to upper face_id
//the same face is not allowed to be in multiple polygon groups //the same face is not allowed to be in multiple polygon groups
// because SubmeshFaceId -> CompleteMeshFaceId -> SubmeshFaceId is ambiguous
// when multiple SubmeshFaceId point to one MeshFaceId
let mut faces=Vec::new(); let mut faces=Vec::new();
let mut face_id_from_face=HashMap::new(); let mut face_id_from_face=HashMap::new();
let mesh_topologies:Vec<PhysicsMeshTopology>=mesh.physics_groups.iter().map(|physics_group|{ let mut mesh_topologies:Vec<PhysicsMeshTopology>=mesh.physics_groups.iter().map(|physics_group|{
//construct submesh //construct submesh
let mut submesh_faces=Vec::new();//these contain a map from submeshId->meshId let mut submesh_faces=Vec::new();//these contain a map from submeshId->meshId
let mut submesh_verts=Vec::new(); let mut submesh_verts=Vec::new();
@ -395,11 +390,15 @@ impl TryFrom<&model::Mesh> for PhysicsMesh{
normal:(normal/len as i64).divide().narrow_1().unwrap(), normal:(normal/len as i64).divide().narrow_1().unwrap(),
dot:(dot/(len*len) as i64).narrow_1().unwrap(), dot:(dot/(len*len) as i64).narrow_1().unwrap(),
}; };
let face_id=*face_id_from_face.entry(face).or_insert_with(||{ let face_id=match face_id_from_face.get(&face){
let face_id=MeshFaceId::new(faces.len() as u32); Some(&face_id)=>face_id,
faces.push(face); None=>{
face_id let face_id=MeshFaceId::new(faces.len() as u32);
}); face_id_from_face.insert(face.clone(),face_id);
faces.push(face);
face_id
}
};
submesh_faces.push(face_id); submesh_faces.push(face_id);
face_ref_guys.push(FaceRefEdges(face_edges)); face_ref_guys.push(FaceRefEdges(face_edges));
} }
@ -407,16 +406,16 @@ impl TryFrom<&model::Mesh> for PhysicsMesh{
PhysicsMeshTopology{ PhysicsMeshTopology{
faces:submesh_faces, faces:submesh_faces,
verts:submesh_verts, verts:submesh_verts,
face_topology:face_ref_guys.into_iter().map(|FaceRefEdges(edges)|{ face_topology:face_ref_guys.into_iter().map(|face_ref_guy|{
FaceRefs{edges} FaceRefs{edges:face_ref_guy.0}
}).collect(), }).collect(),
edge_topology:edge_pool.edge_guys.into_iter().map(|(EdgeRefVerts(verts),EdgeRefFaces(faces))| edge_topology:edge_pool.edge_guys.into_iter().map(|(edge_ref_verts,edge_ref_faces)|
EdgeRefs{faces,verts} EdgeRefs{faces:edge_ref_faces.0,verts:edge_ref_verts.0}
).collect(), ).collect(),
vert_topology:vert_ref_guys.into_iter().map(|VertRefGuy{edges,faces}| vert_topology:vert_ref_guys.into_iter().map(|vert_ref_guy|
VertRefs{ VertRefs{
edges:edges.into_iter().collect(), edges:vert_ref_guy.edges.into_iter().collect(),
faces:faces.into_iter().collect(), faces:vert_ref_guy.faces.into_iter().collect(),
} }
).collect(), ).collect(),
} }
@ -426,7 +425,7 @@ impl TryFrom<&model::Mesh> for PhysicsMesh{
faces, faces,
verts, verts,
}, },
// complete_mesh:None, complete_mesh:mesh_topologies.pop().ok_or(PhysicsMeshError::NoPhysicsGroups)?,
submeshes:mesh_topologies, submeshes:mesh_topologies,
}) })
} }
@ -629,10 +628,6 @@ enum EV{
} }
pub type GigaTime=Ratio<Fixed<4,128>,Fixed<4,128>>; pub type GigaTime=Ratio<Fixed<4,128>,Fixed<4,128>>;
pub fn into_giga_time(time:Time,relative_to:Time)->GigaTime{
let r=(time-relative_to).to_ratio();
Ratio::new(r.num.widen_4(),r.den.widen_4())
}
impl MinkowskiMesh<'_>{ impl MinkowskiMesh<'_>{
pub fn minkowski_sum<'a>(mesh0:TransformedMesh<'a>,mesh1:TransformedMesh<'a>)->MinkowskiMesh<'a>{ pub fn minkowski_sum<'a>(mesh0:TransformedMesh<'a>,mesh1:TransformedMesh<'a>)->MinkowskiMesh<'a>{
@ -748,44 +743,46 @@ impl MinkowskiMesh<'_>{
// //
// Most of the calculation time is just calculating the starting point // Most of the calculation time is just calculating the starting point
// for the "actual" crawling algorithm below (predict_collision_{in|out}). // for the "actual" crawling algorithm below (predict_collision_{in|out}).
fn closest_fev_not_inside(&self,mut infinity_body:Body,start_time:Bound<&Time>)->Option<FEV<MinkowskiMesh>>{ fn closest_fev_not_inside(&self,mut infinity_body:Body,start_time:Time)->Option<FEV<MinkowskiMesh>>{
infinity_body.infinity_dir().and_then(|dir|{ infinity_body.infinity_dir().and_then(|dir|{
let infinity_fev=self.infinity_fev(-dir,infinity_body.position); let infinity_fev=self.infinity_fev(-dir,infinity_body.position);
//a line is simpler to solve than a parabola //a line is simpler to solve than a parabola
infinity_body.velocity=dir; infinity_body.velocity=dir;
infinity_body.acceleration=vec3::ZERO; infinity_body.acceleration=vec3::ZERO;
//crawl in from negative infinity along a tangent line to get the closest fev //crawl in from negative infinity along a tangent line to get the closest fev
infinity_fev.crawl(self,&infinity_body,Bound::Unbounded,start_time).miss() // TODO: change crawl_fev args to delta time? Optional values?
infinity_fev.crawl(self,&infinity_body,Time::MIN/4,start_time).miss()
}) })
} }
pub fn predict_collision_in(&self,relative_body:&Body,range:impl RangeBounds<Time>)->Option<(MinkowskiFace,GigaTime)>{ pub fn predict_collision_in(&self,relative_body:&Body,Range{start:start_time,end:time_limit}:Range<Time>)->Option<(MinkowskiFace,GigaTime)>{
self.closest_fev_not_inside(*relative_body,range.start_bound()).and_then(|fev|{ println!("=== physics setup ===");
self.closest_fev_not_inside(relative_body.clone(),start_time).and_then(|fev|{
println!("=== physics crawl ===");
//continue forwards along the body parabola //continue forwards along the body parabola
fev.crawl(self,relative_body,range.start_bound(),range.end_bound()).hit() fev.crawl(self,relative_body,start_time,time_limit).hit()
}) })
} }
pub fn predict_collision_out(&self,relative_body:&Body,range:impl RangeBounds<Time>)->Option<(MinkowskiFace,GigaTime)>{ pub fn predict_collision_out(&self,relative_body:&Body,Range{start:start_time,end:time_limit}:Range<Time>)->Option<(MinkowskiFace,GigaTime)>{
let (lower_bound,upper_bound)=(range.start_bound(),range.end_bound()); //create an extrapolated body at time_limit
// swap and negate bounds to do a time inversion let infinity_body=-relative_body.clone();
let (lower_bound,upper_bound)=(upper_bound.map(|&t|-t),lower_bound.map(|&t|-t)); self.closest_fev_not_inside(infinity_body,-time_limit).and_then(|fev|{
let infinity_body=-relative_body;
self.closest_fev_not_inside(infinity_body,lower_bound.as_ref()).and_then(|fev|{
//continue backwards along the body parabola //continue backwards along the body parabola
fev.crawl(self,&infinity_body,lower_bound.as_ref(),upper_bound.as_ref()).hit() fev.crawl(self,&infinity_body,-time_limit,-start_time).hit()
//no need to test -time<time_limit because of the first step //no need to test -time<time_limit because of the first step
.map(|(face,time)|(face,-time)) .map(|(face,time)|(face,-time))
}) })
} }
pub fn predict_collision_face_out(&self,relative_body:&Body,range:impl RangeBounds<Time>,contact_face_id:MinkowskiFace)->Option<(MinkowskiDirectedEdge,GigaTime)>{ pub fn predict_collision_face_out(&self,relative_body:&Body,Range{start:start_time,end:time_limit}:Range<Time>,contact_face_id:MinkowskiFace)->Option<(MinkowskiEdge,GigaTime)>{
// TODO: make better
use crate::face_crawler::{low,upp};
//no algorithm needed, there is only one state and two cases (Edge,None) //no algorithm needed, there is only one state and two cases (Edge,None)
//determine when it passes an edge ("sliding off" case) //determine when it passes an edge ("sliding off" case)
let start_time=range.start_bound().map(|&t|{ let start_time={
let r=(t-relative_body.time).to_ratio(); let r=(start_time-relative_body.time).to_ratio();
Ratio::new(r.num,r.den) Ratio::new(r.num,r.den)
}); };
let mut best_time=range.end_bound().map(|&t|into_giga_time(t,relative_body.time)); let mut best_time={
let r=(time_limit-relative_body.time).to_ratio();
Ratio::new(r.num.widen_4(),r.den.widen_4())
};
let mut best_edge=None; let mut best_edge=None;
let face_n=self.face_nd(contact_face_id).0; let face_n=self.face_nd(contact_face_id).0;
for &directed_edge_id in self.face_edges(contact_face_id).as_ref(){ for &directed_edge_id in self.face_edges(contact_face_id).as_ref(){
@ -798,19 +795,18 @@ impl MinkowskiMesh<'_>{
//WARNING: truncated precision //WARNING: truncated precision
//wrap for speed //wrap for speed
for dt in Fixed::<4,128>::zeroes2(((n.dot(relative_body.position))*2-d).wrap_4(),n.dot(relative_body.velocity).wrap_4()*2,n.dot(relative_body.acceleration).wrap_4()){ for dt in Fixed::<4,128>::zeroes2(((n.dot(relative_body.position))*2-d).wrap_4(),n.dot(relative_body.velocity).wrap_4()*2,n.dot(relative_body.acceleration).wrap_4()){
if low(&start_time,&dt)&&upp(&dt,&best_time)&&n.dot(relative_body.extrapolated_velocity_ratio_dt(dt)).is_negative(){ if start_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(relative_body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
best_time=Bound::Included(dt); best_time=dt;
best_edge=Some((directed_edge_id,dt)); best_edge=Some(directed_edge_id);
break; break;
} }
} }
} }
best_edge best_edge.map(|e|(e.as_undirected(),best_time))
} }
fn infinity_in(&self,infinity_body:Body)->Option<(MinkowskiFace,GigaTime)>{ fn infinity_in(&self,infinity_body:Body)->Option<(MinkowskiFace,GigaTime)>{
let infinity_fev=self.infinity_fev(-infinity_body.velocity,infinity_body.position); let infinity_fev=self.infinity_fev(-infinity_body.velocity,infinity_body.position);
// Bound::Included means that the surface of the mesh is included in the mesh infinity_fev.crawl(self,&infinity_body,Time::MIN/4,infinity_body.time).hit()
infinity_fev.crawl(self,&infinity_body,Bound::Unbounded,Bound::Included(&infinity_body.time)).hit()
} }
pub fn is_point_in_mesh(&self,point:Planar64Vec3)->bool{ pub fn is_point_in_mesh(&self,point:Planar64Vec3)->bool{
let infinity_body=Body::new(point,vec3::Y,vec3::ZERO,Time::ZERO); let infinity_body=Body::new(point,vec3::Y,vec3::ZERO,Time::ZERO);
@ -918,6 +914,7 @@ impl MeshQuery for MinkowskiMesh<'_>{
})) }))
}, },
MinkowskiEdge::EdgeVert(e0,v1)=>{ MinkowskiEdge::EdgeVert(e0,v1)=>{
println!("MinkowskiEdge::EdgeVert({e0:?},{v1:?})");
//tracking index with an external variable because .enumerate() is not available //tracking index with an external variable because .enumerate() is not available
let v1e=self.mesh1.vert_edges(v1); let v1e=self.mesh1.vert_edges(v1);
let &[e0f0,e0f1]=self.mesh0.edge_faces(e0).as_ref(); let &[e0f0,e0f1]=self.mesh0.edge_faces(e0).as_ref();
@ -925,14 +922,18 @@ impl MeshQuery for MinkowskiMesh<'_>{
let mut best_edge=None; let mut best_edge=None;
let mut best_d:Ratio<Fixed<8,256>,Fixed<8,256>>=Ratio::new(Fixed::ZERO,Fixed::ONE); let mut best_d:Ratio<Fixed<8,256>,Fixed<8,256>>=Ratio::new(Fixed::ZERO,Fixed::ONE);
let edge_face0_n=self.mesh0.face_nd(edge_face_id0).0; let edge_face0_n=self.mesh0.face_nd(edge_face_id0).0;
println!("edge_face0_n={edge_face0_n}");
let edge_face0_nn=edge_face0_n.dot(edge_face0_n); let edge_face0_nn=edge_face0_n.dot(edge_face0_n);
for &directed_edge_id1 in v1e.as_ref(){ for &directed_edge_id1 in v1e.as_ref(){
let edge1_n=self.mesh1.directed_edge_n(directed_edge_id1); let edge1_n=self.mesh1.directed_edge_n(directed_edge_id1);
println!("edge1_n={edge1_n}");
let d=edge_face0_n.dot(edge1_n); let d=edge_face0_n.dot(edge1_n);
println!("d={d} {d:?}");
if d.is_negative(){ if d.is_negative(){
let edge1_nn=edge1_n.dot(edge1_n); let edge1_nn=edge1_n.dot(edge1_n);
let dd=(d*d)/(edge_face0_nn*edge1_nn); let dd=(d*d)/(edge_face0_nn*edge1_nn);
if best_d<dd{ if !dd.den.is_zero()&&best_d<dd{
println!("dd={dd:?}");
best_d=dd; best_d=dd;
best_edge=Some(directed_edge_id1); best_edge=Some(directed_edge_id1);
} }
@ -948,10 +949,10 @@ impl MeshQuery for MinkowskiMesh<'_>{
} }
fn edge_verts(&self,edge_id:MinkowskiEdge)->impl AsRef<[MinkowskiVert;2]>{ fn edge_verts(&self,edge_id:MinkowskiEdge)->impl AsRef<[MinkowskiVert;2]>{
AsRefHelper(match edge_id{ AsRefHelper(match edge_id{
MinkowskiEdge::VertEdge(v0,e1)=>self.mesh1.edge_verts(e1).as_ref().map(|vert_id1| MinkowskiEdge::VertEdge(v0,e1)=>(*self.mesh1.edge_verts(e1).as_ref()).map(|vert_id1|
MinkowskiVert::VertVert(v0,vert_id1) MinkowskiVert::VertVert(v0,vert_id1)
), ),
MinkowskiEdge::EdgeVert(e0,v1)=>self.mesh0.edge_verts(e0).as_ref().map(|vert_id0| MinkowskiEdge::EdgeVert(e0,v1)=>(*self.mesh0.edge_verts(e0).as_ref()).map(|vert_id0|
MinkowskiVert::VertVert(vert_id0,v1) MinkowskiVert::VertVert(vert_id0,v1)
), ),
}) })
@ -961,43 +962,38 @@ impl MeshQuery for MinkowskiMesh<'_>{
MinkowskiVert::VertVert(v0,v1)=>{ MinkowskiVert::VertVert(v0,v1)=>{
let mut edges=Vec::new(); let mut edges=Vec::new();
//detect shared volume when the other mesh is mirrored along a test edge dir //detect shared volume when the other mesh is mirrored along a test edge dir
let v0f_thing=self.mesh0.vert_faces(v0); let v0f=self.mesh0.vert_faces(v0);
let v1f_thing=self.mesh1.vert_faces(v1); let v1f=self.mesh1.vert_faces(v1);
let v0f=v0f_thing.as_ref(); let v0f_n:Vec<_>=v0f.as_ref().iter().map(|&face_id|self.mesh0.face_nd(face_id).0).collect();
let v1f=v1f_thing.as_ref(); let v1f_n:Vec<_>=v1f.as_ref().iter().map(|&face_id|self.mesh1.face_nd(face_id).0).collect();
let v0f_n:Vec<_>=v0f.iter().map(|&face_id|self.mesh0.face_nd(face_id).0).collect(); let the_len=v0f.as_ref().len()+v1f.as_ref().len();
let v1f_n:Vec<_>=v1f.iter().map(|&face_id|self.mesh1.face_nd(face_id).0).collect();
// scratch vector
let mut face_normals=Vec::with_capacity(v0f.len()+v1f.len());
face_normals.clone_from(&v0f_n);
for &directed_edge_id in self.mesh0.vert_edges(v0).as_ref(){ for &directed_edge_id in self.mesh0.vert_edges(v0).as_ref(){
let n=self.mesh0.directed_edge_n(directed_edge_id); let n=self.mesh0.directed_edge_n(directed_edge_id);
let nn=n.dot(n); let nn=n.dot(n);
// TODO: there's gotta be a better way to do this // TODO: there's gotta be a better way to do this
// drop faces beyond v0f_n //make a set of faces
face_normals.truncate(v0f.len()); let mut face_normals=Vec::with_capacity(the_len);
// make a set of faces from mesh0's perspective //add mesh0 faces as-is
face_normals.clone_from(&v0f_n);
for face_n in &v1f_n{ for face_n in &v1f_n{
//add reflected mesh1 faces //add reflected mesh1 faces
//wrap for speed //wrap for speed
face_normals.push(*face_n-(n*face_n.dot(n)*2/nn).divide().wrap_3()); face_normals.push(*face_n-(n*face_n.dot(n)*2/nn).divide().wrap_3());
} }
if is_empty_volume(&face_normals){ if is_empty_volume(face_normals){
edges.push(MinkowskiDirectedEdge::EdgeVert(directed_edge_id,v1)); edges.push(MinkowskiDirectedEdge::EdgeVert(directed_edge_id,v1));
} }
} }
face_normals.clone_from(&v1f_n);
for &directed_edge_id in self.mesh1.vert_edges(v1).as_ref(){ for &directed_edge_id in self.mesh1.vert_edges(v1).as_ref(){
let n=self.mesh1.directed_edge_n(directed_edge_id); let n=self.mesh1.directed_edge_n(directed_edge_id);
let nn=n.dot(n); let nn=n.dot(n);
// drop faces beyond v1f_n let mut face_normals=Vec::with_capacity(the_len);
face_normals.truncate(v1f.len()); face_normals.clone_from(&v1f_n);
// make a set of faces from mesh1's perspective
for face_n in &v0f_n{ for face_n in &v0f_n{
//wrap for speed //wrap for speed
face_normals.push(*face_n-(n*face_n.dot(n)*2/nn).divide().wrap_3()); face_normals.push(*face_n-(n*face_n.dot(n)*2/nn).divide().wrap_3());
} }
if is_empty_volume(&face_normals){ if is_empty_volume(face_normals){
edges.push(MinkowskiDirectedEdge::VertEdge(v0,directed_edge_id)); edges.push(MinkowskiDirectedEdge::VertEdge(v0,directed_edge_id));
} }
} }
@ -1011,7 +1007,7 @@ impl MeshQuery for MinkowskiMesh<'_>{
} }
} }
fn is_empty_volume(normals:&[Vector3<Fixed<3,96>>])->bool{ fn is_empty_volume(normals:Vec<Vector3<Fixed<3,96>>>)->bool{
let len=normals.len(); let len=normals.len();
for i in 0..len-1{ for i in 0..len-1{
for j in i+1..len{ for j in i+1..len{
@ -1037,6 +1033,6 @@ fn is_empty_volume(normals:&[Vector3<Fixed<3,96>>])->bool{
#[test] #[test]
fn test_is_empty_volume(){ fn test_is_empty_volume(){
assert!(!is_empty_volume(&[vec3::X.widen_3(),vec3::Y.widen_3(),vec3::Z.widen_3()])); assert!(!is_empty_volume([vec3::X.widen_3(),vec3::Y.widen_3(),vec3::Z.widen_3()].to_vec()));
assert!(is_empty_volume(&[vec3::X.widen_3(),vec3::Y.widen_3(),vec3::Z.widen_3(),vec3::NEG_X.widen_3()])); assert!(is_empty_volume([vec3::X.widen_3(),vec3::Y.widen_3(),vec3::Z.widen_3(),vec3::NEG_X.widen_3()].to_vec()));
} }

@ -107,7 +107,6 @@ enum TransientAcceleration{
#[derive(Clone,Debug)] #[derive(Clone,Debug)]
struct ContactMoveState{ struct ContactMoveState{
jump_direction:JumpDirection, jump_direction:JumpDirection,
// TODO: this is bad data normalization, remove this
contact:ContactCollision, contact:ContactCollision,
target:TransientAcceleration, target:TransientAcceleration,
} }
@ -417,7 +416,7 @@ impl HitboxMesh{
} }
} }
#[inline] #[inline]
fn transformed_mesh(&self)->TransformedMesh{ const fn transformed_mesh(&self)->TransformedMesh{
TransformedMesh::new(self.mesh.complete_mesh_view(),&self.transform) TransformedMesh::new(self.mesh.complete_mesh_view(),&self.transform)
} }
} }
@ -602,17 +601,12 @@ impl MoveState{
fn cull_velocity(&mut self,velocity:Planar64Vec3,body:&mut Body,touching:&mut TouchingState,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,style:&StyleModifiers,camera:&PhysicsCamera,input_state:&InputState){ fn cull_velocity(&mut self,velocity:Planar64Vec3,body:&mut Body,touching:&mut TouchingState,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,style:&StyleModifiers,camera:&PhysicsCamera,input_state:&InputState){
//TODO: be more precise about contacts //TODO: be more precise about contacts
if set_velocity_cull(body,touching,models,hitbox_mesh,velocity){ if set_velocity_cull(body,touching,models,hitbox_mesh,velocity){
// TODO do better //TODO do better
// TODO: unduplicate this code
match self.get_walk_state(){ match self.get_walk_state(){
// did you stop touching the thing you were walking on? //did you stop touching the thing you were walking on?
Some(walk_state)=>if !touching.contacts.contains(&walk_state.contact){ Some(walk_state)=>if !touching.contacts.contains(&walk_state.contact){
self.set_move_state(MoveState::Air,body,touching,models,hitbox_mesh,style,camera,input_state); self.set_move_state(MoveState::Air,body,touching,models,hitbox_mesh,style,camera,input_state);
}else{
// stopped touching something else while walking
self.apply_enum_and_input_and_body(body,touching,models,hitbox_mesh,style,camera,input_state);
}, },
// not walking, but stopped touching something
None=>self.apply_enum_and_body(body,touching,models,hitbox_mesh,style,camera,input_state), None=>self.apply_enum_and_body(body,touching,models,hitbox_mesh,style,camera,input_state),
} }
} }
@ -863,9 +857,9 @@ impl Default for PhysicsState{
} }
impl PhysicsState{ impl PhysicsState{
pub fn new_with_body(body:Body)->Self{ pub fn new_at_position(position:Planar64Vec3)->Self{
Self{ Self{
body, body:Body::new(position,vec3::int(0,0,0),vec3::int(0,-100,0),Time::ZERO),
..Self::default() ..Self::default()
} }
} }
@ -994,7 +988,7 @@ impl PhysicsContext<'_>{
impl PhysicsData{ impl PhysicsData{
/// use with caution, this is the only non-instruction way to mess with physics /// use with caution, this is the only non-instruction way to mess with physics
pub fn generate_models(&mut self,map:&map::CompleteMap){ pub fn generate_models(&mut self,map:&map::CompleteMap){
let modes=map.modes.clone().denormalize(); let mut modes=map.modes.clone().denormalize();
let mut used_contact_attributes=Vec::new(); let mut used_contact_attributes=Vec::new();
let mut used_intersect_attributes=Vec::new(); let mut used_intersect_attributes=Vec::new();
@ -1012,30 +1006,31 @@ impl PhysicsData{
let mut used_meshes=Vec::new(); let mut used_meshes=Vec::new();
let mut physics_mesh_id_from_model_mesh_id=HashMap::<MeshId,PhysicsMeshId>::new(); let mut physics_mesh_id_from_model_mesh_id=HashMap::<MeshId,PhysicsMeshId>::new();
for (model_id,model) in map.models.iter().enumerate(){ for (model_id,model) in map.models.iter().enumerate(){
let attr_id=match physics_attr_id_from_model_attr_id.entry(model.attributes){ //TODO: use .entry().or_insert_with(||{
std::collections::hash_map::Entry::Occupied(entry)=>*entry.get(), let attr_id=if let Some(&attr_id)=physics_attr_id_from_model_attr_id.get(&model.attributes){
std::collections::hash_map::Entry::Vacant(entry)=>{ attr_id
//check if it's real }else{
match map.attributes.get(model.attributes.get() as usize).and_then(|m_attr|{ //check if it's real
PhysicsCollisionAttributes::try_from(m_attr).map_or(None,|p_attr|{ match map.attributes.get(model.attributes.get() as usize).and_then(|m_attr|{
let attr_id=match p_attr{ PhysicsCollisionAttributes::try_from(m_attr).map_or(None,|p_attr|{
PhysicsCollisionAttributes::Contact(attr)=>{ let attr_id=match p_attr{
let attr_id=ContactAttributesId::new(used_contact_attributes.len() as u32); PhysicsCollisionAttributes::Contact(attr)=>{
used_contact_attributes.push(attr); let attr_id=ContactAttributesId::new(used_contact_attributes.len() as u32);
PhysicsAttributesId::Contact(attr_id) used_contact_attributes.push(attr);
}, PhysicsAttributesId::Contact(attr_id)
PhysicsCollisionAttributes::Intersect(attr)=>{ },
let attr_id=IntersectAttributesId::new(used_intersect_attributes.len() as u32); PhysicsCollisionAttributes::Intersect(attr)=>{
used_intersect_attributes.push(attr); let attr_id=IntersectAttributesId::new(used_intersect_attributes.len() as u32);
PhysicsAttributesId::Intersect(attr_id) used_intersect_attributes.push(attr);
}, PhysicsAttributesId::Intersect(attr_id)
}; },
Some(*entry.insert(attr_id)) };
}) physics_attr_id_from_model_attr_id.insert(model.attributes,attr_id);
}){ Some(attr_id)
Some(attr_id)=>attr_id, })
None=>continue, }){
} Some(attr_id)=>attr_id,
None=>continue,
} }
}; };
let mesh_id=if let Some(&mesh_id)=physics_mesh_id_from_model_mesh_id.get(&model.mesh){ let mesh_id=if let Some(&mesh_id)=physics_mesh_id_from_model_mesh_id.get(&model.mesh){
@ -1133,7 +1128,7 @@ impl PhysicsData{
//JUST POLLING!!! NO MUTATION //JUST POLLING!!! NO MUTATION
let mut collector=instruction::InstructionCollector::new(time_limit); let mut collector=instruction::InstructionCollector::new(time_limit);
collector.collect(state.next_move_instruction()); // collector.collect(state.next_move_instruction());
//check for collision ends //check for collision ends
state.touching.predict_collision_end(&mut collector,&data.models,&data.hitbox_mesh,&state.body,state.time); state.touching.predict_collision_end(&mut collector,&data.models,&data.hitbox_mesh,&state.body,state.time);
@ -1145,6 +1140,7 @@ impl PhysicsData{
//let relative_body=state.body.relative_to(&Body::ZERO); //let relative_body=state.body.relative_to(&Body::ZERO);
let relative_body=&state.body; let relative_body=&state.body;
data.bvh.sample_aabb(&aabb,&mut |&convex_mesh_id|{ data.bvh.sample_aabb(&aabb,&mut |&convex_mesh_id|{
println!("sample model={:?}",convex_mesh_id.model_id);
//no checks are needed because of the time limits. //no checks are needed because of the time limits.
let model_mesh=data.models.mesh(convex_mesh_id); let model_mesh=data.models.mesh(convex_mesh_id);
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,data.hitbox_mesh.transformed_mesh()); let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,data.hitbox_mesh.transformed_mesh());
@ -1199,7 +1195,7 @@ fn recalculate_touching(
collision_end_contact(move_state,body,touching,models,hitbox_mesh,style,camera,input_state,models.contact_attr(contact.model_id),contact) collision_end_contact(move_state,body,touching,models,hitbox_mesh,style,camera,input_state,models.contact_attr(contact.model_id),contact)
} }
while let Some(&intersect)=touching.intersects.iter().next(){ while let Some(&intersect)=touching.intersects.iter().next(){
collision_end_intersect(move_state,body,touching,models,hitbox_mesh,style,camera,input_state,mode,run,models.intersect_attr(intersect.model_id),intersect,time); collision_end_intersect(touching,mode,run,models.intersect_attr(intersect.model_id),intersect,time);
} }
//find all models in the teleport region //find all models in the teleport region
let mut aabb=aabb::Aabb::default(); let mut aabb=aabb::Aabb::default();
@ -1611,7 +1607,6 @@ fn collision_start_intersect(
None=>(), None=>(),
} }
} }
move_state.apply_enum_and_body(body,touching,models,hitbox_mesh,style,camera,input_state);
run_teleport_behaviour(intersect.model_id.into(),attr.general.wormhole.as_ref(),mode,move_state,body,touching,run,mode_state,models,hitbox_mesh,bvh,style,camera,input_state,time); run_teleport_behaviour(intersect.model_id.into(),attr.general.wormhole.as_ref(),mode,move_state,body,touching,run,mode_state,models,hitbox_mesh,bvh,style,camera,input_state,time);
} }
@ -1632,26 +1627,15 @@ fn collision_end_contact(
//TODO do better //TODO do better
//this is inner code from move_state.cull_velocity //this is inner code from move_state.cull_velocity
match move_state.get_walk_state(){ match move_state.get_walk_state(){
// did you stop touching the thing you were walking on? //did you stop touching the thing you were walking on?
Some(walk_state)=>if walk_state.contact==contact{ Some(walk_state)=>if walk_state.contact==contact{
move_state.set_move_state(MoveState::Air,body,touching,models,hitbox_mesh,style,camera,input_state); move_state.set_move_state(MoveState::Air,body,touching,models,hitbox_mesh,style,camera,input_state);
}else{
// stopped touching something else while walking
move_state.apply_enum_and_input_and_body(body,touching,models,hitbox_mesh,style,camera,input_state);
}, },
// not walking, but stopped touching something
None=>move_state.apply_enum_and_body(body,touching,models,hitbox_mesh,style,camera,input_state), None=>move_state.apply_enum_and_body(body,touching,models,hitbox_mesh,style,camera,input_state),
} }
} }
fn collision_end_intersect( fn collision_end_intersect(
move_state:&mut MoveState,
body:&mut Body,
touching:&mut TouchingState, touching:&mut TouchingState,
models:&PhysicsModels,
hitbox_mesh:&HitboxMesh,
style:&StyleModifiers,
camera:&PhysicsCamera,
input_state:&InputState,
mode:Option<&gameplay_modes::Mode>, mode:Option<&gameplay_modes::Mode>,
run:&mut run::Run, run:&mut run::Run,
_attr:&gameplay_attributes::IntersectAttributes, _attr:&gameplay_attributes::IntersectAttributes,
@ -1659,7 +1643,6 @@ fn collision_end_intersect(
time:Time, time:Time,
){ ){
touching.remove(&Collision::Intersect(intersect)); touching.remove(&Collision::Intersect(intersect));
move_state.apply_enum_and_body(body,touching,models,hitbox_mesh,style,camera,input_state);
if let Some(mode)=mode{ if let Some(mode)=mode{
let zone=mode.get_zone(intersect.model_id.into()); let zone=mode.get_zone(intersect.model_id.into());
match zone{ match zone{
@ -1689,6 +1672,7 @@ fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:Tim
} }
match ins.instruction{ match ins.instruction{
InternalInstruction::CollisionStart(collision,_)=>{ InternalInstruction::CollisionStart(collision,_)=>{
println!("yeaahhh!");
let mode=data.modes.get_mode(state.mode_state.get_mode_id()); let mode=data.modes.get_mode(state.mode_state.get_mode_id());
match collision{ match collision{
Collision::Contact(contact)=>collision_start_contact( Collision::Contact(contact)=>collision_start_contact(
@ -1716,7 +1700,7 @@ fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:Tim
contact contact
), ),
Collision::Intersect(intersect)=>collision_end_intersect( Collision::Intersect(intersect)=>collision_end_intersect(
&mut state.move_state,&mut state.body,&mut state.touching,&data.models,&data.hitbox_mesh,&state.style,&state.camera,&state.input_state, &mut state.touching,
data.modes.get_mode(state.mode_state.get_mode_id()), data.modes.get_mode(state.mode_state.get_mode_id()),
&mut state.run, &mut state.run,
data.models.intersect_attr(intersect.model_id), data.models.intersect_attr(intersect.model_id),
@ -1926,7 +1910,7 @@ mod test{
assert_eq!(collision.map(|tup|relative_body.time+tup.1.into()),expected_collision_time,"Incorrect time of collision"); assert_eq!(collision.map(|tup|relative_body.time+tup.1.into()),expected_collision_time,"Incorrect time of collision");
} }
fn test_collision(relative_body:Body,expected_collision_time:Option<Time>){ fn test_collision(relative_body:Body,expected_collision_time:Option<Time>){
test_collision_axis_aligned(relative_body,expected_collision_time); test_collision_axis_aligned(relative_body.clone(),expected_collision_time);
test_collision_rotated(relative_body,expected_collision_time); test_collision_rotated(relative_body,expected_collision_time);
} }
#[test] #[test]
@ -1939,15 +1923,6 @@ mod test{
),Some(Time::from_secs(2))); ),Some(Time::from_secs(2)));
} }
#[test] #[test]
fn test_collision_small_mv(){
test_collision(Body::new(
int3(0,5,0),
int3(0,-1,0)+(vec3::X>>32),
vec3::ZERO,
Time::ZERO
),Some(Time::from_secs(2)));
}
#[test]
fn test_collision_degenerate_east(){ fn test_collision_degenerate_east(){
test_collision(Body::new( test_collision(Body::new(
int3(3,5,0), int3(3,5,0),

@ -1 +0,0 @@
/test_files

@ -1,28 +0,0 @@
#[allow(unused)]
#[derive(Debug)]
pub enum ReplayError{
IO(std::io::Error),
SNF(strafesnet_snf::Error),
SNFM(strafesnet_snf::map::Error),
SNFB(strafesnet_snf::bot::Error),
}
impl From<std::io::Error> for ReplayError{
fn from(value:std::io::Error)->Self{
Self::IO(value)
}
}
impl From<strafesnet_snf::Error> for ReplayError{
fn from(value:strafesnet_snf::Error)->Self{
Self::SNF(value)
}
}
impl From<strafesnet_snf::map::Error> for ReplayError{
fn from(value:strafesnet_snf::map::Error)->Self{
Self::SNFM(value)
}
}
impl From<strafesnet_snf::bot::Error> for ReplayError{
fn from(value:strafesnet_snf::bot::Error)->Self{
Self::SNFB(value)
}
}

@ -1,13 +1,7 @@
mod error; use std::io::Cursor;
mod util; use std::path::Path;
#[cfg(test)]
mod tests;
use std::time::Instant; use std::time::Instant;
use error::ReplayError;
use util::read_entire_file;
use strafesnet_physics::physics::{PhysicsData,PhysicsState,PhysicsContext}; use strafesnet_physics::physics::{PhysicsData,PhysicsState,PhysicsContext};
fn main(){ fn main(){
@ -15,10 +9,71 @@ fn main(){
match arg.as_deref(){ match arg.as_deref(){
Some("determinism")|None=>test_determinism().unwrap(), Some("determinism")|None=>test_determinism().unwrap(),
Some("replay")=>run_replay().unwrap(), Some("replay")=>run_replay().unwrap(),
Some("bug2")=>physics_bug_2().unwrap(),
_=>println!("invalid argument"), _=>println!("invalid argument"),
} }
} }
fn physics_bug_2()->Result<(),ReplayError>{
println!("loading map file..");
let data=read_entire_file("bhop_monster_jam.snfm")?;
let map=strafesnet_snf::read_map(data)?.into_complete_map()?;
// create recording
let mut physics_data=PhysicsData::default();
println!("generating models..");
physics_data.generate_models(&map);
println!("simulating...");
//teleport to bug
// body pos = Vector { array: [Fixed { bits: 554895163352 }, Fixed { bits: 1485633089990 }, Fixed { bits: 1279601007173 }] }
// after the fix it's still happening, possibly for a different reason, new position to evince:
// body pos = Vector { array: [Fixed { bits: 555690659654 }, Fixed { bits: 1490485868773 }, Fixed { bits: 1277783839382 }] }
let mut physics=PhysicsState::new_at_position(strafesnet_common::integer::vec3::raw_xyz(555690659654,1490485868773,1277783839382));
// wait one second to activate the bug
// hit=Some(ModelId(2262))
PhysicsContext::run_input_instruction(&mut physics,&physics_data,strafesnet_common::instruction::TimedInstruction{
time:strafesnet_common::integer::Time::from_millis(500),
instruction:strafesnet_common::physics::Instruction::Idle,
});
Ok(())
}
#[allow(unused)]
#[derive(Debug)]
enum ReplayError{
IO(std::io::Error),
SNF(strafesnet_snf::Error),
SNFM(strafesnet_snf::map::Error),
SNFB(strafesnet_snf::bot::Error),
}
impl From<std::io::Error> for ReplayError{
fn from(value:std::io::Error)->Self{
Self::IO(value)
}
}
impl From<strafesnet_snf::Error> for ReplayError{
fn from(value:strafesnet_snf::Error)->Self{
Self::SNF(value)
}
}
impl From<strafesnet_snf::map::Error> for ReplayError{
fn from(value:strafesnet_snf::map::Error)->Self{
Self::SNFM(value)
}
}
impl From<strafesnet_snf::bot::Error> for ReplayError{
fn from(value:strafesnet_snf::bot::Error)->Self{
Self::SNFB(value)
}
}
fn read_entire_file(path:impl AsRef<Path>)->Result<Cursor<Vec<u8>>,std::io::Error>{
let data=std::fs::read(path)?;
Ok(Cursor::new(data))
}
fn run_replay()->Result<(),ReplayError>{ fn run_replay()->Result<(),ReplayError>{
println!("loading map file.."); println!("loading map file..");
let data=read_entire_file("../tools/bhop_maps/5692113331.snfm")?; let data=read_entire_file("../tools/bhop_maps/5692113331.snfm")?;

@ -1,78 +0,0 @@
use crate::error::ReplayError;
use crate::util::read_entire_file;
use strafesnet_physics::physics::{PhysicsData,PhysicsState,PhysicsContext};
#[test]
#[ignore]
fn physics_bug_2()->Result<(),ReplayError>{
println!("loading map file..");
let data=read_entire_file("test_files/bhop_monster_jam.snfm")?;
let map=strafesnet_snf::read_map(data)?.into_complete_map()?;
// create recording
let mut physics_data=PhysicsData::default();
println!("generating models..");
physics_data.generate_models(&map);
println!("simulating...");
//teleport to bug
// body pos = Vector { array: [Fixed { bits: 554895163352 }, Fixed { bits: 1485633089990 }, Fixed { bits: 1279601007173 }] }
// after the fix it's still happening, possibly for a different reason, new position to evince:
// body pos = Vector { array: [Fixed { bits: 555690659654 }, Fixed { bits: 1490485868773 }, Fixed { bits: 1277783839382 }] }
use strafesnet_common::integer::{vec3,Time};
let body=strafesnet_physics::physics::Body::new(
vec3::raw_xyz(555690659654,1490485868773,1277783839382),
vec3::int(0,0,0),
vec3::int(0,-100,0),
Time::ZERO,
);
let mut physics=PhysicsState::new_with_body(body);
// wait one second to activate the bug
// hit=Some(ModelId(2262))
PhysicsContext::run_input_instruction(&mut physics,&physics_data,strafesnet_common::instruction::TimedInstruction{
time:strafesnet_common::integer::Time::from_millis(500),
instruction:strafesnet_common::physics::Instruction::Idle,
});
Ok(())
}
#[test]
#[ignore]
fn physics_bug_3()->Result<(),ReplayError>{
println!("loading map file..");
let data=read_entire_file("../tools/bhop_maps/5692152916.snfm")?;
let map=strafesnet_snf::read_map(data)?.into_complete_map()?;
// create recording
let mut physics_data=PhysicsData::default();
println!("generating models..");
physics_data.generate_models(&map);
println!("simulating...");
//teleport to bug
use strafesnet_common::integer::{vec3,Time};
let body=strafesnet_physics::physics::Body::new(
// bhop_toc corner position after wall hits
// vec3::raw_xyz(-1401734815424,3315081280280,-2466057177493),
// vec3::raw_xyz(0,-96915585363,1265),
// vec3::raw_xyz(0,-429496729600,0),
// alternate room center position
// vec3::raw_xyz(-1129043783837,3324870327882,-2014012350212),
// vec3::raw_xyz(0,-96915585363,1265),
// vec3::raw_xyz(0,-429496729600,0),
// corner setup before wall hits
vec3::raw_xyz(-1392580080675,3325402529458,-2444727738679),
vec3::raw_xyz(-30259028820,-22950929553,-71141663007),
vec3::raw_xyz(0,-429496729600,0),
Time::ZERO,
);
let mut physics=PhysicsState::new_with_body(body);
// wait one second to activate the bug
PhysicsContext::run_input_instruction(&mut physics,&physics_data,strafesnet_common::instruction::TimedInstruction{
time:strafesnet_common::integer::Time::from_millis(500),
instruction:strafesnet_common::physics::Instruction::Idle,
});
Ok(())
}

@ -1,7 +0,0 @@
use std::io::Cursor;
use std::path::Path;
pub fn read_entire_file(path:impl AsRef<Path>)->Result<Cursor<Vec<u8>>,std::io::Error>{
let data=std::fs::read(path)?;
Ok(Cursor::new(data))
}

@ -1,6 +1,6 @@
[package] [package]
name = "strafesnet_bsp_loader" name = "strafesnet_bsp_loader"
version = "0.3.1" version = "0.3.0"
edition = "2024" edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project" repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
@ -11,9 +11,9 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
[dependencies] [dependencies]
glam = "0.30.0" glam = "0.30.0"
strafesnet_common = { version = "0.7.0", path = "../common", registry = "strafesnet" } strafesnet_common = { version = "0.6.0", path = "../common", registry = "strafesnet" }
strafesnet_deferred_loader = { version = "0.5.1", path = "../deferred_loader", registry = "strafesnet" } strafesnet_deferred_loader = { version = "0.5.0", path = "../deferred_loader", registry = "strafesnet" }
vbsp = "0.9.1" vbsp = "0.8.0"
vbsp-entities-css = "0.6.0" vbsp-entities-css = "0.6.0"
vmdl = "0.2.0" vmdl = "0.2.0"
vpk = "0.3.0" vpk = "0.3.0"

@ -1,5 +1,5 @@
use strafesnet_common::integer::Planar64; use strafesnet_common::integer::{self,Planar64,Planar64Vec3};
use strafesnet_common::{model,integer}; use strafesnet_common::model::{self,VertexId};
use strafesnet_common::integer::{vec3::Vector3,Fixed,Ratio}; use strafesnet_common::integer::{vec3::Vector3,Fixed,Ratio};
use crate::{valve_transform_normal,valve_transform_dist}; use crate::{valve_transform_normal,valve_transform_dist};
@ -138,7 +138,15 @@ fn planes_to_faces(face_list:std::collections::HashSet<Face>)->Result<Faces,Plan
loop{ loop{
// push point onto vertices // push point onto vertices
// problem: this may push a vertex that does not fit in the fixed point range and is thus meaningless // problem: this may push a vertex that does not fit in the fixed point range and is thus meaningless
face.push(intersection.divide().narrow_1().unwrap()); //
// physics bug 2 originates from vertices being imprecise?
//
// Mask off the most precise 16 bits so that
// when face normals are calculated from
// the remaining 16 fractional bits
// they never exceed 32 bits of precision.
const MASK:Planar64=Planar64::raw(!((1<<16)-1));
face.push(intersection.divide().narrow_1().unwrap().map(|c|c&MASK));
// we looped back around to face1, we're done! // we looped back around to face1, we're done!
if core::ptr::eq(face1,face2){ if core::ptr::eq(face1,face2){
@ -204,6 +212,33 @@ impl std::fmt::Display for BrushToMeshError{
} }
impl core::error::Error for BrushToMeshError{} impl core::error::Error for BrushToMeshError{}
fn subdivide_max_area(tris:&mut Vec<Vec<VertexId>>,cw_verts:&[(VertexId,Planar64Vec3)],i0:usize,i2:usize,id0:VertexId,id2:VertexId,v0:Planar64Vec3,v2:Planar64Vec3){
if i0+1==i2{
return;
}
let mut best_i1=i0+1;
if i0+2<i2{
let mut best_area={
let (_,v1)=cw_verts[best_i1.rem_euclid(cw_verts.len())];
(v2-v0).cross(v1-v0).length_squared()
};
for i1 in i0+2..=i2-1{
let (_,v1)=cw_verts[i1.rem_euclid(cw_verts.len())];
let area=(v2-v0).cross(v1-v0).length_squared();
if best_area<area{
best_i1=i1;
best_area=area;
}
}
}
let i1=best_i1;
let (id1,v1)=cw_verts[i1.rem_euclid(cw_verts.len())];
// draw max area first
tris.push(vec![id0,id1,id2]);
subdivide_max_area(tris,cw_verts,i0,i1,id0,id1,v0,v1);
subdivide_max_area(tris,cw_verts,i1,i2,id1,id2,v1,v2);
}
pub fn faces_to_mesh(faces:Vec<Vec<integer::Planar64Vec3>>)->model::Mesh{ pub fn faces_to_mesh(faces:Vec<Vec<integer::Planar64Vec3>>)->model::Mesh{
// generate the mesh // generate the mesh
let mut mb=model::MeshBuilder::new(); let mut mb=model::MeshBuilder::new();
@ -212,16 +247,34 @@ pub fn faces_to_mesh(faces:Vec<Vec<integer::Planar64Vec3>>)->model::Mesh{
// normals are ignored by physics // normals are ignored by physics
let normal=mb.acquire_normal_id(integer::vec3::ZERO); let normal=mb.acquire_normal_id(integer::vec3::ZERO);
let polygon_list=faces.into_iter().map(|face|{ let polygon_list=faces.into_iter().flat_map(|face|{
face.into_iter().map(|pos|{ let cw_verts=face.into_iter().map(|position|{
let pos=mb.acquire_pos_id(pos); let pos=mb.acquire_pos_id(position);
mb.acquire_vertex_id(model::IndexedVertex{ (mb.acquire_vertex_id(model::IndexedVertex{
pos, pos,
tex, tex,
normal, normal,
color, color,
}) }),position)
}).collect() }).collect::<Vec<_>>();
// scan and select maximum area triangle O(n^3)
let len=cw_verts.len();
let cw_verts=cw_verts.as_slice();
let ((i0,i1,i2),(v0,v1,v2))=cw_verts[..len-2].iter().enumerate().flat_map(|(i0,&(_,v0))|
cw_verts[i0+1..len-1].iter().enumerate().flat_map(move|(i1,&(_,v1))|
cw_verts[i0+i1+2..].iter().enumerate().map(move|(i2,&(_,v2))|((i0,i0+i1+1,i0+i1+i2+2),(v0,v1,v2)))
)
).max_by_key(|&(_,(v0,v1,v2))|(v2-v0).cross(v1-v0).length_squared()).unwrap();
// scan and select more maximum area triangles n * O(n)
let mut tris=Vec::with_capacity(len-2);
// da big one
let (id0,id1,id2)=(cw_verts[i0].0,cw_verts[i1].0,cw_verts[i2].0);
tris.push(vec![id0,id1,id2]);
subdivide_max_area(&mut tris,cw_verts,i0,i1,id0,id1,v0,v1);
subdivide_max_area(&mut tris,cw_verts,i1,i2,id1,id2,v1,v2);
subdivide_max_area(&mut tris,cw_verts,i2,i0+len,id2,id0,v2,v0);
tris
}).collect(); }).collect();
let polygon_groups=vec![model::PolygonGroup::PolygonList(model::PolygonList::new(polygon_list))]; let polygon_groups=vec![model::PolygonGroup::PolygonList(model::PolygonList::new(polygon_list))];

@ -349,7 +349,7 @@ pub struct PartialMap1{
impl PartialMap1{ impl PartialMap1{
pub fn add_prop_meshes<'a>( pub fn add_prop_meshes<'a>(
self, self,
prop_meshes:Meshes<model::Mesh>, prop_meshes:Meshes,
)->PartialMap2{ )->PartialMap2{
PartialMap2{ PartialMap2{
attributes:self.attributes, attributes:self.attributes,

@ -22,17 +22,17 @@ impl From<std::io::Error> for TextureError{
} }
} }
pub struct TextureLoader; pub struct TextureLoader<'a>(std::marker::PhantomData<&'a ()>);
impl TextureLoader{ impl TextureLoader<'_>{
pub fn new()->Self{ pub fn new()->Self{
Self Self(std::marker::PhantomData)
} }
} }
impl Loader for TextureLoader{ impl<'a> Loader for TextureLoader<'a>{
type Error=TextureError; type Error=TextureError;
type Index<'a>=Cow<'a,str>; type Index=Cow<'a,str>;
type Resource=Texture; type Resource=Texture;
fn load<'a>(&mut self,index:Self::Index<'a>)->Result<Self::Resource,Self::Error>{ fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>{
let file_name=format!("textures/{}.dds",index); let file_name=format!("textures/{}.dds",index);
let mut file=std::fs::File::open(file_name)?; let mut file=std::fs::File::open(file_name)?;
let mut data=Vec::new(); let mut data=Vec::new();
@ -100,24 +100,30 @@ impl<'bsp,'vpk> BspFinder<'bsp,'vpk>{
} }
} }
pub struct ModelLoader<'bsp,'vpk>{ pub struct ModelLoader<'bsp,'vpk,'a>{
finder:BspFinder<'bsp,'vpk>, finder:BspFinder<'bsp,'vpk>,
life:core::marker::PhantomData<&'a ()>,
} }
impl ModelLoader<'_,'_>{ impl ModelLoader<'_,'_,'_>{
#[inline] #[inline]
pub const fn new<'bsp,'vpk>( pub const fn new<'bsp,'vpk,'a>(
finder:BspFinder<'bsp,'vpk>, finder:BspFinder<'bsp,'vpk>,
)->ModelLoader<'bsp,'vpk>{ )->ModelLoader<'bsp,'vpk,'a>{
ModelLoader{ ModelLoader{
finder, finder,
life:core::marker::PhantomData,
} }
} }
} }
impl<'bsp,'vpk> Loader for ModelLoader<'bsp,'vpk>{ impl<'bsp,'vpk,'a> Loader for ModelLoader<'bsp,'vpk,'a>
where
'bsp:'a,
'vpk:'a,
{
type Error=MeshError; type Error=MeshError;
type Index<'a>=&'a str where Self:'a; type Index=&'a str;
type Resource=vmdl::Model; type Resource=vmdl::Model;
fn load<'a>(&'a mut self,index:Self::Index<'a>)->Result<Self::Resource,Self::Error>{ fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>{
let mdl_path_lower=index.to_lowercase(); let mdl_path_lower=index.to_lowercase();
//.mdl, .vvd, .dx90.vtx //.mdl, .vvd, .dx90.vtx
let path=std::path::PathBuf::from(mdl_path_lower.as_str()); let path=std::path::PathBuf::from(mdl_path_lower.as_str());
@ -137,27 +143,31 @@ impl<'bsp,'vpk> Loader for ModelLoader<'bsp,'vpk>{
} }
} }
pub struct MeshLoader<'bsp,'vpk,'load,'str>{ pub struct MeshLoader<'bsp,'vpk,'load,'a>{
finder:BspFinder<'bsp,'vpk>, finder:BspFinder<'bsp,'vpk>,
deferred_loader:&'load mut strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader<Cow<'str,str>>, deferred_loader:&'load mut strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader<Cow<'a,str>>,
} }
impl MeshLoader<'_,'_,'_,'_>{ impl MeshLoader<'_,'_,'_,'_>{
#[inline] #[inline]
pub const fn new<'bsp,'vpk,'load,'str>( pub const fn new<'bsp,'vpk,'load,'a>(
finder:BspFinder<'bsp,'vpk>, finder:BspFinder<'bsp,'vpk>,
deferred_loader:&'load mut strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader<Cow<'str,str>>, deferred_loader:&'load mut strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader<Cow<'a,str>>,
)->MeshLoader<'bsp,'vpk,'load,'str>{ )->MeshLoader<'bsp,'vpk,'load,'a>{
MeshLoader{ MeshLoader{
finder, finder,
deferred_loader deferred_loader
} }
} }
} }
impl<'str,'bsp,'vpk,'load> Loader for MeshLoader<'bsp,'vpk,'load,'str>{ impl<'bsp,'vpk,'load,'a> Loader for MeshLoader<'bsp,'vpk,'load,'a>
where
'bsp:'a,
'vpk:'a,
{
type Error=MeshError; type Error=MeshError;
type Index<'a>=&'a str where Self:'a; type Index=&'a str;
type Resource=Mesh; type Resource=Mesh;
fn load<'a>(&'a mut self,index:Self::Index<'a>)->Result<Self::Resource,Self::Error>{ fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>{
let model=ModelLoader::new(self.finder).load(index)?; let model=ModelLoader::new(self.finder).load(index)?;
let mesh=crate::mesh::convert_mesh(model,&mut self.deferred_loader); let mesh=crate::mesh::convert_mesh(model,&mut self.deferred_loader);
Ok(mesh) Ok(mesh)

@ -1,6 +1,6 @@
[package] [package]
name = "strafesnet_common" name = "strafesnet_common"
version = "0.7.0" version = "0.6.0"
edition = "2024" edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project" repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"

@ -140,15 +140,6 @@ impl ModeId{
pub const MAIN:Self=Self(0); pub const MAIN:Self=Self(0);
pub const BONUS:Self=Self(1); pub const BONUS:Self=Self(1);
} }
impl core::fmt::Display for ModeId{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->core::fmt::Result{
match self{
&Self::MAIN=>write!(f,"Main"),
&Self::BONUS=>write!(f,"Bonus"),
&Self(mode_id)=>write!(f,"Bonus{mode_id}"),
}
}
}
#[derive(Clone)] #[derive(Clone)]
pub struct Mode{ pub struct Mode{
style:gameplay_style::StyleModifiers, style:gameplay_style::StyleModifiers,
@ -449,11 +440,11 @@ impl ModesBuilder{
} }
NormalizedModes::new(modes.into_iter().map(|mode_builder|NormalizedMode(mode_builder.mode)).collect()) NormalizedModes::new(modes.into_iter().map(|mode_builder|NormalizedMode(mode_builder.mode)).collect())
} }
pub fn insert_mode(&mut self,mode_id:ModeId,mode:Mode)->Result<(),ExistingEntryError>{ pub fn insert_mode(&mut self,mode_id:ModeId,mode:Mode){
error_if_exists(self.modes.insert(mode_id,mode)) assert!(self.modes.insert(mode_id,mode).is_none(),"Cannot replace existing mode");
} }
pub fn insert_stage(&mut self,mode_id:ModeId,stage_id:StageId,stage:Stage)->Result<(),ExistingEntryError>{ pub fn insert_stage(&mut self,mode_id:ModeId,stage_id:StageId,stage:Stage){
error_if_exists(self.stages.entry(mode_id).or_insert(HashMap::new()).insert(stage_id,stage)) assert!(self.stages.entry(mode_id).or_insert(HashMap::new()).insert(stage_id,stage).is_none(),"Cannot replace existing stage");
} }
pub fn push_mode_update(&mut self,mode_id:ModeId,mode_update:ModeUpdate){ pub fn push_mode_update(&mut self,mode_id:ModeId,mode_update:ModeUpdate){
self.mode_updates.push((mode_id,mode_update)); self.mode_updates.push((mode_id,mode_update));
@ -462,12 +453,3 @@ impl ModesBuilder{
// self.stage_updates.push((mode_id,stage_id,stage_update)); // self.stage_updates.push((mode_id,stage_id,stage_update));
// } // }
} }
#[derive(Debug)]
pub struct ExistingEntryError;
fn error_if_exists<T>(value:Option<T>)->Result<(),ExistingEntryError>{
match value{
Some(_)=>Err(ExistingEntryError),
None=>Ok(())
}
}

@ -1,5 +1,5 @@
pub use fixed_wide::fixed::*; pub use fixed_wide::fixed::*;
pub use ratio_ops::ratio::{Ratio,Divide,Parity}; pub use ratio_ops::ratio::{Ratio,Divide};
//integer units //integer units
@ -132,21 +132,20 @@ impl<T> std::ops::Mul for Time<T>{
Ratio::new(Fixed::raw(self.0)*Fixed::raw(rhs.0),Fixed::raw_digit(1_000_000_000i64.pow(2))) Ratio::new(Fixed::raw(self.0)*Fixed::raw(rhs.0),Fixed::raw_digit(1_000_000_000i64.pow(2)))
} }
} }
macro_rules! impl_time_i64_rhs_operator { impl<T> std::ops::Div<i64> for Time<T>{
($op:ident,$method:ident)=>{ type Output=Self;
impl<T> core::ops::$op<i64> for Time<T>{ #[inline]
type Output=Self; fn div(self,rhs:i64)->Self::Output{
#[inline] Self::raw(self.0/rhs)
fn $method(self,rhs:i64)->Self::Output{ }
Self::raw(self.0.$method(rhs)) }
} impl<T> std::ops::Mul<i64> for Time<T>{
} type Output=Self;
#[inline]
fn mul(self,rhs:i64)->Self::Output{
Self::raw(self.0*rhs)
} }
} }
impl_time_i64_rhs_operator!(Div,div);
impl_time_i64_rhs_operator!(Mul,mul);
impl_time_i64_rhs_operator!(Shr,shr);
impl_time_i64_rhs_operator!(Shl,shl);
impl<T> core::ops::Mul<Time<T>> for Planar64{ impl<T> core::ops::Mul<Time<T>> for Planar64{
type Output=Ratio<Fixed<2,64>,Planar64>; type Output=Ratio<Fixed<2,64>,Planar64>;
fn mul(self,rhs:Time<T>)->Self::Output{ fn mul(self,rhs:Time<T>)->Self::Output{
@ -402,10 +401,6 @@ impl Angle32{
pub const NEG_FRAC_PI_2:Self=Self(-1<<30); pub const NEG_FRAC_PI_2:Self=Self(-1<<30);
pub const PI:Self=Self(-1<<31); pub const PI:Self=Self(-1<<31);
#[inline] #[inline]
pub const fn raw(num:i32)->Self{
Self(num)
}
#[inline]
pub const fn wrap_from_i64(theta:i64)->Self{ pub const fn wrap_from_i64(theta:i64)->Self{
//take lower bits //take lower bits
//note: this was checked on compiler explorer and compiles to 1 instruction! //note: this was checked on compiler explorer and compiles to 1 instruction!
@ -563,10 +558,6 @@ pub mod vec3{
pub const MAX:Planar64Vec3=Planar64Vec3::new([Planar64::MAX;3]); pub const MAX:Planar64Vec3=Planar64Vec3::new([Planar64::MAX;3]);
pub const ZERO:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO;3]); pub const ZERO:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO;3]);
pub const ZERO_2:linear_ops::types::Vector3<Fixed::<2,64>>=linear_ops::types::Vector3::new([Fixed::<2,64>::ZERO;3]); pub const ZERO_2:linear_ops::types::Vector3<Fixed::<2,64>>=linear_ops::types::Vector3::new([Fixed::<2,64>::ZERO;3]);
pub const ZERO_3:linear_ops::types::Vector3<Fixed::<3,96>>=linear_ops::types::Vector3::new([Fixed::<3,96>::ZERO;3]);
pub const ZERO_4:linear_ops::types::Vector3<Fixed::<4,128>>=linear_ops::types::Vector3::new([Fixed::<4,128>::ZERO;3]);
pub const ZERO_5:linear_ops::types::Vector3<Fixed::<5,160>>=linear_ops::types::Vector3::new([Fixed::<5,160>::ZERO;3]);
pub const ZERO_6:linear_ops::types::Vector3<Fixed::<6,192>>=linear_ops::types::Vector3::new([Fixed::<6,192>::ZERO;3]);
pub const X:Planar64Vec3=Planar64Vec3::new([Planar64::ONE,Planar64::ZERO,Planar64::ZERO]); pub const X:Planar64Vec3=Planar64Vec3::new([Planar64::ONE,Planar64::ZERO,Planar64::ZERO]);
pub const Y:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO,Planar64::ONE,Planar64::ZERO]); pub const Y:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO,Planar64::ONE,Planar64::ZERO]);
pub const Z:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO,Planar64::ZERO,Planar64::ONE]); pub const Z:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO,Planar64::ZERO,Planar64::ONE]);

@ -1,6 +1,6 @@
[package] [package]
name = "strafesnet_deferred_loader" name = "strafesnet_deferred_loader"
version = "0.5.1" version = "0.5.0"
edition = "2024" edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project" repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
@ -10,4 +10,4 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
strafesnet_common = { version = "0.7.0", path = "../common", registry = "strafesnet" } strafesnet_common = { version = "0.6.0", path = "../common", registry = "strafesnet" }

@ -2,7 +2,7 @@ use std::collections::HashMap;
use crate::loader::Loader; use crate::loader::Loader;
use crate::mesh::Meshes; use crate::mesh::Meshes;
use crate::texture::{RenderConfigs,Texture}; use crate::texture::{RenderConfigs,Texture};
use strafesnet_common::model::{MeshId,RenderConfig,RenderConfigId,TextureId}; use strafesnet_common::model::{Mesh,MeshId,RenderConfig,RenderConfigId,TextureId};
#[derive(Clone,Copy,Debug)] #[derive(Clone,Copy,Debug)]
pub enum LoadFailureMode{ pub enum LoadFailureMode{
@ -45,7 +45,7 @@ impl<H:core::hash::Hash+Eq> RenderConfigDeferredLoader<H>{
pub fn into_indices(self)->impl Iterator<Item=H>{ pub fn into_indices(self)->impl Iterator<Item=H>{
self.render_config_id_from_asset_id.into_keys().flatten() self.render_config_id_from_asset_id.into_keys().flatten()
} }
pub fn into_render_configs<'a,L:Loader<Resource=Texture,Index<'a>=H>+'a>(mut self,loader:&mut L,failure_mode:LoadFailureMode)->Result<RenderConfigs,L::Error>{ pub fn into_render_configs<L:Loader<Resource=Texture,Index=H>>(mut self,loader:&mut L,failure_mode:LoadFailureMode)->Result<RenderConfigs,L::Error>{
let mut sorted_textures=vec![None;self.texture_count as usize]; let mut sorted_textures=vec![None;self.texture_count as usize];
for (index_option,render_config_id) in self.render_config_id_from_asset_id{ for (index_option,render_config_id) in self.render_config_id_from_asset_id{
let render_config=&mut self.render_configs[render_config_id.get() as usize]; let render_config=&mut self.render_configs[render_config_id.get() as usize];
@ -93,7 +93,7 @@ impl<H:core::hash::Hash+Eq> MeshDeferredLoader<H>{
pub fn into_indices(self)->impl Iterator<Item=H>{ pub fn into_indices(self)->impl Iterator<Item=H>{
self.mesh_id_from_asset_id.into_keys() self.mesh_id_from_asset_id.into_keys()
} }
pub fn into_meshes<'a,M:Clone,L:Loader<Resource=M,Index<'a>=H>+'a>(self,loader:&mut L,failure_mode:LoadFailureMode)->Result<Meshes<M>,L::Error>{ pub fn into_meshes<L:Loader<Resource=Mesh,Index=H>>(self,loader:&mut L,failure_mode:LoadFailureMode)->Result<Meshes,L::Error>{
let mut mesh_list=vec![None;self.mesh_id_from_asset_id.len()]; let mut mesh_list=vec![None;self.mesh_id_from_asset_id.len()];
for (index,mesh_id) in self.mesh_id_from_asset_id{ for (index,mesh_id) in self.mesh_id_from_asset_id{
let resource_result=loader.load(index); let resource_result=loader.load(index);

@ -2,7 +2,7 @@ use std::error::Error;
pub trait Loader{ pub trait Loader{
type Error:Error; type Error:Error;
type Index<'a> where Self:'a; type Index;
type Resource; type Resource;
fn load<'a>(&mut self,index:Self::Index<'a>)->Result<Self::Resource,Self::Error>; fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>;
} }

@ -1,15 +1,15 @@
use strafesnet_common::model::MeshId; use strafesnet_common::model::{Mesh,MeshId};
pub struct Meshes<M>{ pub struct Meshes{
meshes:Vec<Option<M>>, meshes:Vec<Option<Mesh>>,
} }
impl<M> Meshes<M>{ impl Meshes{
pub(crate) const fn new(meshes:Vec<Option<M>>)->Self{ pub(crate) const fn new(meshes:Vec<Option<Mesh>>)->Self{
Self{ Self{
meshes, meshes,
} }
} }
pub fn consume(self)->impl Iterator<Item=(MeshId,M)>{ pub fn consume(self)->impl Iterator<Item=(MeshId,Mesh)>{
self.meshes.into_iter().enumerate().filter_map(|(mesh_id,maybe_mesh)| self.meshes.into_iter().enumerate().filter_map(|(mesh_id,maybe_mesh)|
maybe_mesh.map(|mesh|(MeshId::new(mesh_id as u32),mesh)) maybe_mesh.map(|mesh|(MeshId::new(mesh_id as u32),mesh))
) )

@ -1,6 +1,6 @@
[package] [package]
name = "fixed_wide" name = "fixed_wide"
version = "0.2.1" version = "0.2.0"
edition = "2024" edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project" repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"

@ -233,11 +233,6 @@ impl FixedFromFloatError{
} }
} }
} }
impl core::fmt::Display for FixedFromFloatError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
macro_rules! impl_from_float { macro_rules! impl_from_float {
( $decode:ident, $input: ty, $mantissa_bits:expr ) => { ( $decode:ident, $input: ty, $mantissa_bits:expr ) => {
impl<const N:usize,const F:usize> TryFrom<$input> for Fixed<N,F>{ impl<const N:usize,const F:usize> TryFrom<$input> for Fixed<N,F>{

@ -204,19 +204,13 @@ macro_rules! impl_matrix_named_fields_shape {
type Target=$struct_outer<Vector<$size_inner,T>>; type Target=$struct_outer<Vector<$size_inner,T>>;
#[inline] #[inline]
fn deref(&self)->&Self::Target{ fn deref(&self)->&Self::Target{
// This cast is valid because Matrix has #[repr(transparent)] unsafe{core::mem::transmute(&self.array)}
let ptr=&self.array as *const [[T;$size_inner];$size_outer] as *const Self::Target;
// SAFETY: this pointer is non-null because it comes from a reference
unsafe{&*ptr}
} }
} }
impl<T> core::ops::DerefMut for Matrix<$size_outer,$size_inner,T>{ impl<T> core::ops::DerefMut for Matrix<$size_outer,$size_inner,T>{
#[inline] #[inline]
fn deref_mut(&mut self)->&mut Self::Target{ fn deref_mut(&mut self)->&mut Self::Target{
// This cast is valid because Matrix has #[repr(transparent)] unsafe{core::mem::transmute(&mut self.array)}
let ptr=&mut self.array as *mut [[T;$size_inner];$size_outer] as *mut Self::Target;
// SAFETY: this pointer is non-null because it comes from a reference
unsafe{&mut*ptr}
} }
} }
} }

@ -330,19 +330,13 @@ macro_rules! impl_vector_named_fields {
type Target=$struct<T>; type Target=$struct<T>;
#[inline] #[inline]
fn deref(&self)->&Self::Target{ fn deref(&self)->&Self::Target{
// This cast is valid because Vector has #[repr(transparent)] unsafe{core::mem::transmute(&self.array)}
let ptr=&self.array as *const [T;$size] as *const Self::Target;
// SAFETY: this pointer is non-null because it comes from a reference
unsafe{&*ptr}
} }
} }
impl<T> core::ops::DerefMut for Vector<$size,T>{ impl<T> core::ops::DerefMut for Vector<$size,T>{
#[inline] #[inline]
fn deref_mut(&mut self)->&mut Self::Target{ fn deref_mut(&mut self)->&mut Self::Target{
// This cast is valid because Vector has #[repr(transparent)] unsafe{core::mem::transmute(&mut self.array)}
let ptr=&mut self.array as *mut [T;$size] as *mut Self::Target;
// SAFETY: this pointer is non-null because it comes from a reference
unsafe{&mut*ptr}
} }
} }
} }

@ -1,6 +1,6 @@
[package] [package]
name = "strafesnet_rbx_loader" name = "strafesnet_rbx_loader"
version = "0.7.0" version = "0.6.0"
edition = "2024" edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project" repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
@ -13,13 +13,12 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
bytemuck = "1.14.3" bytemuck = "1.14.3"
glam = "0.30.0" glam = "0.30.0"
lazy-regex = "3.1.0" lazy-regex = "3.1.0"
rbx_binary = { version = "1.1.0-sn4", registry = "strafesnet" } rbx_binary = { version = "0.7.4", registry = "strafesnet" }
rbx_dom_weak = { version = "3.1.0-sn4", registry = "strafesnet", features = ["instance-userdata"] } rbx_dom_weak = { version = "2.7.0", registry = "strafesnet" }
rbx_mesh = "0.4.0" rbx_mesh = "0.3.1"
rbx_reflection = "5.0.0" rbx_reflection_database = { version = "0.2.10", registry = "strafesnet" }
rbx_reflection_database = "1.0.0" rbx_xml = { version = "0.13.3", registry = "strafesnet" }
rbx_xml = { version = "1.1.0-sn4", registry = "strafesnet" }
rbxassetid = { version = "0.1.0", path = "../rbxassetid", registry = "strafesnet" } rbxassetid = { version = "0.1.0", path = "../rbxassetid", registry = "strafesnet" }
roblox_emulator = { version = "0.5.1", path = "../roblox_emulator", default-features = false, registry = "strafesnet" } roblox_emulator = { version = "0.4.7", path = "../roblox_emulator", registry = "strafesnet" }
strafesnet_common = { version = "0.7.0", path = "../common", registry = "strafesnet" } strafesnet_common = { version = "0.6.0", path = "../common", registry = "strafesnet" }
strafesnet_deferred_loader = { version = "0.5.1", path = "../deferred_loader", registry = "strafesnet" } strafesnet_deferred_loader = { version = "0.5.0", path = "../deferred_loader", registry = "strafesnet" }

@ -1,245 +0,0 @@
use std::collections::HashSet;
use std::num::ParseIntError;
use strafesnet_common::gameplay_modes::{StageId,ModeId};
use strafesnet_common::integer::{FixedFromFloatError,Planar64TryFromFloatError};
/// A collection of errors which can be ignored at your peril
#[derive(Debug,Default)]
pub struct RecoverableErrors{
/// A basepart has an invalid / missing property.
pub basepart_property:Vec<InstancePath>,
/// A part has an unconvertable CFrame.
pub basepart_cframe:Vec<CFrameError>,
/// A part has an unconvertable Velocity.
pub basepart_velocity:Vec<Planar64ConvertError>,
/// A part has an invalid / missing property.
pub part_property:Vec<InstancePath>,
/// A part has an invalid shape.
pub part_shape:Vec<ShapeError>,
/// A meshpart has an invalid / missing property.
pub meshpart_property:Vec<InstancePath>,
/// A meshpart has no mesh.
pub meshpart_content:Vec<InstancePath>,
/// A basepart has an unsupported subclass.
pub unsupported_class:HashSet<String>,
/// A decal has an invalid / missing property.
pub decal_property:Vec<InstancePath>,
/// A decal has an invalid normal_id.
pub normal_id:Vec<NormalIdError>,
/// A texture has an invalid / missing property.
pub texture_property:Vec<InstancePath>,
/// A mode_id failed to parse.
pub mode_id_parse_int:Vec<ParseIntContext>,
/// There is a duplicate mode.
pub duplicate_mode:HashSet<ModeId>,
/// A mode_id failed to parse.
pub stage_id_parse_int:Vec<ParseIntContext>,
/// A Stage was duplicated leading to undefined behaviour.
pub duplicate_stage:HashSet<DuplicateStageError>,
/// A WormholeOut id failed to parse.
pub wormhole_out_id_parse_int:Vec<ParseIntContext>,
/// A WormholeOut was duplicated leading to undefined behaviour.
pub duplicate_wormhole_out:HashSet<u32>,
/// A WormholeIn id failed to parse.
pub wormhole_in_id_parse_int:Vec<ParseIntContext>,
/// A jump limit failed to parse.
pub jump_limit_parse_int:Vec<ParseIntContext>,
}
impl RecoverableErrors{
pub fn count(&self)->usize{
self.basepart_property.len()+
self.basepart_cframe.len()+
self.basepart_velocity.len()+
self.part_property.len()+
self.part_shape.len()+
self.meshpart_property.len()+
self.meshpart_content.len()+
self.unsupported_class.len()+
self.decal_property.len()+
self.normal_id.len()+
self.texture_property.len()+
self.mode_id_parse_int.len()+
self.duplicate_mode.len()+
self.stage_id_parse_int.len()+
self.duplicate_stage.len()+
self.wormhole_out_id_parse_int.len()+
self.duplicate_wormhole_out.len()+
self.wormhole_in_id_parse_int.len()+
self.jump_limit_parse_int.len()
}
}
fn write_comma_separated<T>(
f:&mut std::fmt::Formatter<'_>,
mut it:impl Iterator<Item=T>,
custom_write:impl Fn(&mut std::fmt::Formatter<'_>,T)->std::fmt::Result
)->std::fmt::Result{
if let Some(t)=it.next(){
custom_write(f,t)?;
for t in it{
write!(f,", ")?;
custom_write(f,t)?;
}
}
Ok(())
}
macro_rules! write_instance_path_error{
($f:ident,$self:ident,$field:ident,$class:literal,$class_plural:literal,$problem:literal)=>{
let len=$self.$field.len();
if len!=0{
let plural=if len==1{$class}else{$class_plural};
write!($f,"The following {plural} {}: ",$problem)?;
write_comma_separated($f,$self.$field.iter(),|f,InstancePath(path)|
write!(f,"{path}")
)?;
writeln!($f)?;
}
};
}
macro_rules! write_duplicate_error{
($f:ident,$self:ident,$field:ident,$class:literal,$class_plural:literal)=>{
let len=$self.$field.len();
if len!=0{
let plural=if len==1{$class}else{$class_plural};
write!($f,"The following {plural} duplicates: ")?;
write_comma_separated($f,$self.$field.iter(),|f,id|
write!(f,"{id}")
)?;
writeln!($f)?;
}
};
}
macro_rules! write_bespoke_error{
($f:ident,$self:ident,$field:ident,$class:literal,$class_plural:literal,$problem:literal,$path_field:ident,$error_field:ident)=>{
let len=$self.$field.len();
if len!=0{
let plural=if len==1{$class}else{$class_plural};
write!($f,"The following {plural} {}: ",$problem)?;
write_comma_separated($f,$self.$field.iter(),|f,context|
write!(f,"{} ({})",context.$path_field,context.$error_field)
)?;
writeln!($f)?;
}
};
}
impl core::fmt::Display for RecoverableErrors{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write_instance_path_error!(f,self,basepart_property,"BasePart is","BaseParts are","missing a property");
write_bespoke_error!(f,self,basepart_cframe,"BasePart","BaseParts","CFrame float convert failed",path,error);
write_bespoke_error!(f,self,basepart_velocity,"BasePart","BaseParts","Velocity float convert failed",path,error);
write_instance_path_error!(f,self,part_property,"Part is","Parts are","missing a property");
write_bespoke_error!(f,self,part_shape,"Part","Parts","Shape is invalid",path,shape);
write_instance_path_error!(f,self,meshpart_property,"MeshPart is","MeshParts are","missing a property");
write_instance_path_error!(f,self,meshpart_content,"MeshPart has","MeshParts have","no mesh");
{
let len=self.unsupported_class.len();
if len!=0{
let plural=if len==1{"Class is"}else{"Classes are"};
write!(f,"The following {plural} not supported: ")?;
write_comma_separated(f,self.unsupported_class.iter(),|f,classname|write!(f,"{classname}"))?;
writeln!(f)?;
}
}
write_instance_path_error!(f,self,decal_property,"Decal is","Decals are","missing a property");
write_bespoke_error!(f,self,normal_id,"Decal","Decals","NormalId is invalid",path,normal_id);
write_instance_path_error!(f,self,texture_property,"Texture is","Textures are","missing a property");
write_bespoke_error!(f,self,mode_id_parse_int,"ModeId","ModeIds","failed to parse",context,error);
write_duplicate_error!(f,self,duplicate_mode,"ModeId has","ModeIds have");
write_bespoke_error!(f,self,stage_id_parse_int,"StageId","StageIds","failed to parse",context,error);
write_duplicate_error!(f,self,duplicate_stage,"StageId has","StageIds have");
write_bespoke_error!(f,self,wormhole_out_id_parse_int,"WormholeOutId","WormholeOutIds","failed to parse",context,error);
write_duplicate_error!(f,self,duplicate_wormhole_out,"WormholeOutId has","WormholeOutIds have");
write_bespoke_error!(f,self,wormhole_in_id_parse_int,"WormholeInId","WormholeInIds","failed to parse",context,error);
write_bespoke_error!(f,self,jump_limit_parse_int,"jump limit","jump limits","failed to parse",context,error);
Ok(())
}
}
/// A Decal was missing required properties
#[derive(Debug)]
pub struct InstancePath(pub String);
impl core::fmt::Display for InstancePath{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
self.0.fmt(f)
}
}
impl InstancePath{
pub fn new(dom:&rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance)->InstancePath{
let mut names:Vec<_>=core::iter::successors(
Some(instance),
|i|dom.get_by_ref(i.parent())
).map(
|i|i.name.as_str()
).collect();
// discard the name of the root object
names.pop();
names.reverse();
InstancePath(names.join("."))
}
}
#[derive(Debug)]
pub struct ParseIntContext{
pub context:String,
pub error:ParseIntError,
}
impl ParseIntContext{
pub fn parse<T:core::str::FromStr<Err=ParseIntError>>(input:&str)->Result<T,Self>{
input.parse().map_err(|error|ParseIntContext{
context:input.to_owned(),
error,
})
}
}
#[derive(Debug)]
pub struct NormalIdError{
pub path:InstancePath,
pub normal_id:u32,
}
#[derive(Debug)]
pub struct ShapeError{
pub path:InstancePath,
pub shape:u32,
}
#[derive(Debug)]
pub enum CFrameErrorType{
ZeroDeterminant,
Convert(FixedFromFloatError),
}
impl core::fmt::Display for CFrameErrorType{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
#[derive(Debug)]
pub struct CFrameError{
pub path:InstancePath,
pub error:CFrameErrorType,
}
#[derive(Debug)]
pub struct Planar64ConvertError{
pub path:InstancePath,
pub error:Planar64TryFromFloatError,
}
#[derive(Debug,Hash,Eq,PartialEq)]
pub struct DuplicateStageError{
pub mode_id:ModeId,
pub stage_id:StageId,
}
impl core::fmt::Display for DuplicateStageError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{}-Spawn{}",self.mode_id,self.stage_id.get())
}
}

@ -1,15 +1,9 @@
use std::io::Read; use std::io::Read;
use rbx_dom_weak::WeakDom; use rbx_dom_weak::WeakDom;
use roblox_emulator::context::Context;
use strafesnet_common::map::CompleteMap;
use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLoader,RenderConfigDeferredLoader}; use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLoader,RenderConfigDeferredLoader};
pub use error::RecoverableErrors;
pub use roblox_emulator::runner::Error as RunnerError;
mod rbx; mod rbx;
mod mesh; mod mesh;
mod error;
mod union; mod union;
pub mod loader; pub mod loader;
mod primitives; mod primitives;
@ -33,7 +27,13 @@ impl Model{
fn new(dom:WeakDom)->Self{ fn new(dom:WeakDom)->Self{
Self{dom} Self{dom}
} }
pub fn to_snf(&self,failure_mode:LoadFailureMode)->Result<(CompleteMap,RecoverableErrors),LoadError>{ pub fn into_place(self)->Place{
let Self{mut dom}=self;
let context=roblox_emulator::context::Context::from_mut(&mut dom);
let services=context.convert_into_place();
Place{dom,services}
}
pub fn to_snf(&self,failure_mode:LoadFailureMode)->Result<strafesnet_common::map::CompleteMap,LoadError>{
to_snf(self,failure_mode) to_snf(self,failure_mode)
} }
} }
@ -44,43 +44,36 @@ impl AsRef<WeakDom> for Model{
} }
pub struct Place{ pub struct Place{
context:Context, dom:WeakDom,
services:roblox_emulator::context::Services,
} }
impl Place{ impl Place{
pub fn new(dom:WeakDom)->Result<Self,roblox_emulator::context::ServicesError>{ pub fn new(dom:WeakDom)->Option<Self>{
let context=Context::from_place(dom)?; let context=roblox_emulator::context::Context::from_ref(&dom);
Ok(Self{ Some(Self{
context, services:context.find_services()?,
dom,
}) })
} }
pub fn run_scripts(&mut self)->Result<Vec<RunnerError>,RunnerError>{ pub fn run_scripts(&mut self){
let Place{context}=self; let Place{dom,services}=self;
let runner=roblox_emulator::runner::Runner::new()?; let runner=roblox_emulator::runner::Runner::new().unwrap();
let context=roblox_emulator::context::Context::from_mut(dom);
let scripts=context.scripts(); let scripts=context.scripts();
let runnable=runner.runnable_context(context)?; let runnable=runner.runnable_context_with_services(context,services).unwrap();
let mut errors=Vec::new();
for script in scripts{ for script in scripts{
if let Err(e)=runnable.run_script(script){ if let Err(e)=runnable.run_script(script){
errors.push(e); println!("runner error: {e}");
} }
} }
Ok(errors)
} }
pub fn to_snf(&self,failure_mode:LoadFailureMode)->Result<(CompleteMap,RecoverableErrors),LoadError>{ pub fn to_snf(&self,failure_mode:LoadFailureMode)->Result<strafesnet_common::map::CompleteMap,LoadError>{
to_snf(self,failure_mode) to_snf(self,failure_mode)
} }
} }
impl AsRef<WeakDom> for Place{ impl AsRef<WeakDom> for Place{
fn as_ref(&self)->&WeakDom{ fn as_ref(&self)->&WeakDom{
self.context.as_ref() &self.dom
}
}
impl From<Model> for Place{
fn from(model:Model)->Self{
let context=Context::from_model(model.dom);
Self{
context,
}
} }
} }
@ -101,9 +94,9 @@ impl std::error::Error for ReadError{}
pub fn read<R:Read>(input:R)->Result<Model,ReadError>{ pub fn read<R:Read>(input:R)->Result<Model,ReadError>{
let mut buf=std::io::BufReader::new(input); let mut buf=std::io::BufReader::new(input);
let peek=std::io::BufRead::fill_buf(&mut buf).map_err(ReadError::Io)?; let peek=std::io::BufRead::fill_buf(&mut buf).map_err(ReadError::Io)?;
match peek.get(0..8){ match &peek[0..8]{
Some(b"<roblox!")=>rbx_binary::from_reader(buf).map(Model::new).map_err(ReadError::RbxBinary), b"<roblox!"=>rbx_binary::from_reader(buf).map(Model::new).map_err(ReadError::RbxBinary),
Some(b"<roblox ")=>rbx_xml::from_reader_default(buf).map(Model::new).map_err(ReadError::RbxXml), b"<roblox "=>rbx_xml::from_reader_default(buf).map(Model::new).map_err(ReadError::RbxXml),
_=>Err(ReadError::UnknownFileFormat), _=>Err(ReadError::UnknownFileFormat),
} }
} }
@ -130,7 +123,7 @@ impl From<loader::MeshError> for LoadError{
} }
} }
fn to_snf(dom:impl AsRef<WeakDom>,failure_mode:LoadFailureMode)->Result<(CompleteMap,RecoverableErrors),LoadError>{ fn to_snf(dom:impl AsRef<WeakDom>,failure_mode:LoadFailureMode)->Result<strafesnet_common::map::CompleteMap,LoadError>{
let dom=dom.as_ref(); let dom=dom.as_ref();
let mut texture_deferred_loader=RenderConfigDeferredLoader::new(); let mut texture_deferred_loader=RenderConfigDeferredLoader::new();
@ -150,5 +143,7 @@ fn to_snf(dom:impl AsRef<WeakDom>,failure_mode:LoadFailureMode)->Result<(Complet
let mut texture_loader=loader::TextureLoader::new(); let mut texture_loader=loader::TextureLoader::new();
let render_configs=texture_deferred_loader.into_render_configs(&mut texture_loader,failure_mode).map_err(LoadError::Texture)?; let render_configs=texture_deferred_loader.into_render_configs(&mut texture_loader,failure_mode).map_err(LoadError::Texture)?;
Ok(map_step2.add_render_configs_and_textures(render_configs)) let map=map_step2.add_render_configs_and_textures(render_configs);
Ok(map)
} }

@ -4,12 +4,7 @@ use strafesnet_common::model::Mesh;
use strafesnet_deferred_loader::{loader::Loader,texture::Texture}; use strafesnet_deferred_loader::{loader::Loader,texture::Texture};
use crate::data::RobloxMeshBytes; use crate::data::RobloxMeshBytes;
use crate::rbx::RobloxPartDescription; use crate::rbx::RobloxFaceTextureDescription;
// disallow non-static lifetimes
fn static_ustr(s:&'static str)->rbx_dom_weak::Ustr{
rbx_dom_weak::ustr(s)
}
fn read_entire_file(path:impl AsRef<std::path::Path>)->Result<Vec<u8>,std::io::Error>{ fn read_entire_file(path:impl AsRef<std::path::Path>)->Result<Vec<u8>,std::io::Error>{
let mut file=std::fs::File::open(path)?; let mut file=std::fs::File::open(path)?;
@ -41,17 +36,17 @@ impl From<RobloxAssetIdParseErr> for TextureError{
} }
} }
pub struct TextureLoader; pub struct TextureLoader<'a>(std::marker::PhantomData<&'a ()>);
impl TextureLoader{ impl TextureLoader<'_>{
pub fn new()->Self{ pub fn new()->Self{
Self Self(std::marker::PhantomData)
} }
} }
impl Loader for TextureLoader{ impl<'a> Loader for TextureLoader<'a>{
type Error=TextureError; type Error=TextureError;
type Index<'a>=&'a str; type Index=&'a str;
type Resource=Texture; type Resource=Texture;
fn load<'a>(&mut self,index:Self::Index<'a>)->Result<Self::Resource,Self::Error>{ fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>{
let RobloxAssetId(asset_id)=index.parse()?; let RobloxAssetId(asset_id)=index.parse()?;
let file_name=format!("textures/{}.dds",asset_id); let file_name=format!("textures/{}.dds",asset_id);
let data=read_entire_file(file_name)?; let data=read_entire_file(file_name)?;
@ -109,7 +104,7 @@ pub enum MeshType<'a>{
mesh_data:&'a [u8], mesh_data:&'a [u8],
physics_data:&'a [u8], physics_data:&'a [u8],
size_float_bits:[u32;3], size_float_bits:[u32;3],
part_texture_description:RobloxPartDescription, part_texture_description:[Option<RobloxFaceTextureDescription>;6],
}, },
} }
#[derive(Hash,Eq,PartialEq)] #[derive(Hash,Eq,PartialEq)]
@ -129,7 +124,7 @@ impl MeshIndex<'_>{
mesh_data:&'a [u8], mesh_data:&'a [u8],
physics_data:&'a [u8], physics_data:&'a [u8],
size:&rbx_dom_weak::types::Vector3, size:&rbx_dom_weak::types::Vector3,
part_texture_description:RobloxPartDescription, part_texture_description:crate::rbx::RobloxPartDescription,
)->MeshIndex<'a>{ )->MeshIndex<'a>{
MeshIndex{ MeshIndex{
mesh_type:MeshType::Union{ mesh_type:MeshType::Union{
@ -143,23 +138,17 @@ impl MeshIndex<'_>{
} }
} }
#[derive(Clone)] pub struct MeshLoader<'a>(std::marker::PhantomData<&'a ()>);
pub struct MeshWithSize{ impl MeshLoader<'_>{
pub(crate) mesh:Mesh,
pub(crate) size:strafesnet_common::integer::Planar64Vec3,
}
pub struct MeshLoader;
impl MeshLoader{
pub fn new()->Self{ pub fn new()->Self{
Self Self(std::marker::PhantomData)
} }
} }
impl Loader for MeshLoader{ impl<'a> Loader for MeshLoader<'a>{
type Error=MeshError; type Error=MeshError;
type Index<'a>=MeshIndex<'a>; type Index=MeshIndex<'a>;
type Resource=MeshWithSize; type Resource=Mesh;
fn load<'a>(&mut self,index:Self::Index<'a>)->Result<Self::Resource,Self::Error>{ fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>{
let mesh=match index.mesh_type{ let mesh=match index.mesh_type{
MeshType::FileMesh=>{ MeshType::FileMesh=>{
let RobloxAssetId(asset_id)=index.content.parse()?; let RobloxAssetId(asset_id)=index.content.parse()?;
@ -182,12 +171,12 @@ impl Loader for MeshLoader{
return Err(MeshError::MissingInstance); return Err(MeshError::MissingInstance);
}; };
if physics_data.is_empty(){ if physics_data.is_empty(){
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get(&static_ustr("PhysicsData")){ if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get("PhysicsData"){
physics_data=data.as_ref(); physics_data=data.as_ref();
} }
} }
if mesh_data.is_empty(){ if mesh_data.is_empty(){
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get(&static_ustr("MeshData")){ if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get("MeshData"){
mesh_data=data.as_ref(); mesh_data=data.as_ref();
} }
} }

@ -1,11 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use rbx_mesh::mesh::{Vertex2,Vertex2Truncated}; use rbx_mesh::mesh::{Vertex2,Vertex2Truncated};
use strafesnet_common::aabb::Aabb; use strafesnet_common::{integer::vec3,model::{self,ColorId,IndexedVertex,NormalId,PolygonGroup,PolygonList,PositionId,RenderConfigId,TextureCoordinateId,VertexId}};
use strafesnet_common::integer::vec3;
use strafesnet_common::model::{self,ColorId,IndexedVertex,NormalId,PolygonGroup,PolygonList,PositionId,RenderConfigId,TextureCoordinateId,VertexId};
use crate::loader::MeshWithSize;
#[allow(dead_code)] #[allow(dead_code)]
#[derive(Debug)] #[derive(Debug)]
@ -99,7 +95,7 @@ fn ingest_faces2_lods3(
)) ))
} }
pub fn convert(roblox_mesh_bytes:crate::data::RobloxMeshBytes)->Result<MeshWithSize,Error>{ pub fn convert(roblox_mesh_bytes:crate::data::RobloxMeshBytes)->Result<model::Mesh,Error>{
//generate that mesh boi //generate that mesh boi
let mut unique_pos=Vec::new(); let mut unique_pos=Vec::new();
let mut pos_id_from=HashMap::new(); let mut pos_id_from=HashMap::new();
@ -112,10 +108,8 @@ pub fn convert(roblox_mesh_bytes:crate::data::RobloxMeshBytes)->Result<MeshWithS
let mut unique_vertices=Vec::new(); let mut unique_vertices=Vec::new();
let mut vertex_id_from=HashMap::new(); let mut vertex_id_from=HashMap::new();
let mut polygon_groups=Vec::new(); let mut polygon_groups=Vec::new();
let mut aabb=Aabb::default();
let mut acquire_pos_id=|pos|{ let mut acquire_pos_id=|pos|{
let p=vec3::try_from_f32_array(pos).map_err(Error::Planar64Vec3)?; let p=vec3::try_from_f32_array(pos).map_err(Error::Planar64Vec3)?;
aabb.grow(p);
Ok(PositionId::new(*pos_id_from.entry(p).or_insert_with(||{ Ok(PositionId::new(*pos_id_from.entry(p).or_insert_with(||{
let pos_id=unique_pos.len(); let pos_id=unique_pos.len();
unique_pos.push(p); unique_pos.push(p);
@ -154,7 +148,7 @@ pub fn convert(roblox_mesh_bytes:crate::data::RobloxMeshBytes)->Result<MeshWithS
}) as u32) }) as u32)
}; };
match rbx_mesh::read_versioned(roblox_mesh_bytes.cursor()).map_err(Error::RbxMesh)?{ match rbx_mesh::read_versioned(roblox_mesh_bytes.cursor()).map_err(Error::RbxMesh)?{
rbx_mesh::mesh::Mesh::V1(mesh)=>{ rbx_mesh::mesh::VersionedMesh::Version1(mesh)=>{
let color_id=acquire_color_id([1.0f32;4]); let color_id=acquire_color_id([1.0f32;4]);
polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(mesh.vertices.chunks_exact(3).map(|trip|{ polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(mesh.vertices.chunks_exact(3).map(|trip|{
let mut ingest_vertex1=|vertex:&rbx_mesh::mesh::Vertex1|Ok(acquire_vertex_id(IndexedVertex{ let mut ingest_vertex1=|vertex:&rbx_mesh::mesh::Vertex1|Ok(acquire_vertex_id(IndexedVertex{
@ -166,7 +160,7 @@ pub fn convert(roblox_mesh_bytes:crate::data::RobloxMeshBytes)->Result<MeshWithS
Ok(vec![ingest_vertex1(&trip[0])?,ingest_vertex1(&trip[1])?,ingest_vertex1(&trip[2])?]) Ok(vec![ingest_vertex1(&trip[0])?,ingest_vertex1(&trip[1])?,ingest_vertex1(&trip[2])?])
}).collect::<Result<_,_>>()?))); }).collect::<Result<_,_>>()?)));
}, },
rbx_mesh::mesh::Mesh::V2(mesh)=>{ rbx_mesh::mesh::VersionedMesh::Version2(mesh)=>{
let vertex_id_map=match mesh.header.sizeof_vertex{ let vertex_id_map=match mesh.header.sizeof_vertex{
rbx_mesh::mesh::SizeOfVertex2::Truncated=>{ rbx_mesh::mesh::SizeOfVertex2::Truncated=>{
//pick white and make all the vertices white //pick white and make all the vertices white
@ -180,7 +174,7 @@ pub fn convert(roblox_mesh_bytes:crate::data::RobloxMeshBytes)->Result<MeshWithS
vec![vertex_id_map[&face.0],vertex_id_map[&face.1],vertex_id_map[&face.2]] vec![vertex_id_map[&face.0],vertex_id_map[&face.1],vertex_id_map[&face.2]]
).collect()))); ).collect())));
}, },
rbx_mesh::mesh::Mesh::V3(mesh)=>{ rbx_mesh::mesh::VersionedMesh::Version3(mesh)=>{
let vertex_id_map=match mesh.header.sizeof_vertex{ let vertex_id_map=match mesh.header.sizeof_vertex{
rbx_mesh::mesh::SizeOfVertex2::Truncated=>{ rbx_mesh::mesh::SizeOfVertex2::Truncated=>{
let color_id=acquire_color_id([1.0f32;4]); let color_id=acquire_color_id([1.0f32;4]);
@ -190,20 +184,20 @@ pub fn convert(roblox_mesh_bytes:crate::data::RobloxMeshBytes)->Result<MeshWithS
}?; }?;
ingest_faces2_lods3(&mut polygon_groups,&vertex_id_map,&mesh.faces,&mesh.lods); ingest_faces2_lods3(&mut polygon_groups,&vertex_id_map,&mesh.faces,&mesh.lods);
}, },
rbx_mesh::mesh::Mesh::V4(mesh)=>{ rbx_mesh::mesh::VersionedMesh::Version4(mesh)=>{
let vertex_id_map=ingest_vertices2( let vertex_id_map=ingest_vertices2(
mesh.vertices,&mut acquire_pos_id,&mut acquire_tex_id,&mut acquire_normal_id,&mut acquire_color_id,&mut acquire_vertex_id mesh.vertices,&mut acquire_pos_id,&mut acquire_tex_id,&mut acquire_normal_id,&mut acquire_color_id,&mut acquire_vertex_id
)?; )?;
ingest_faces2_lods3(&mut polygon_groups,&vertex_id_map,&mesh.faces,&mesh.lods); ingest_faces2_lods3(&mut polygon_groups,&vertex_id_map,&mesh.faces,&mesh.lods);
}, },
rbx_mesh::mesh::Mesh::V5(mesh)=>{ rbx_mesh::mesh::VersionedMesh::Version5(mesh)=>{
let vertex_id_map=ingest_vertices2( let vertex_id_map=ingest_vertices2(
mesh.vertices,&mut acquire_pos_id,&mut acquire_tex_id,&mut acquire_normal_id,&mut acquire_color_id,&mut acquire_vertex_id mesh.vertices,&mut acquire_pos_id,&mut acquire_tex_id,&mut acquire_normal_id,&mut acquire_color_id,&mut acquire_vertex_id
)?; )?;
ingest_faces2_lods3(&mut polygon_groups,&vertex_id_map,&mesh.faces,&mesh.lods); ingest_faces2_lods3(&mut polygon_groups,&vertex_id_map,&mesh.faces,&mesh.lods);
}, },
} }
let mesh=model::Mesh{ Ok(model::Mesh{
unique_pos, unique_pos,
unique_normal, unique_normal,
unique_tex, unique_tex,
@ -219,6 +213,5 @@ pub fn convert(roblox_mesh_bytes:crate::data::RobloxMeshBytes)->Result<MeshWithS
}], }],
//disable physics //disable physics
physics_groups:Vec::new(), physics_groups:Vec::new(),
}; })
Ok(MeshWithSize{mesh,size:aabb.size()})
} }

@ -1,6 +1,5 @@
use crate::rbx::{RobloxPartDescription,RobloxWedgeDescription,RobloxCornerWedgeDescription}; use strafesnet_common::model::{Color4,TextureCoordinate,Mesh,IndexedGraphicsGroup,IndexedPhysicsGroup,IndexedVertex,PolygonGroupId,PolygonGroup,PolygonList,PositionId,TextureCoordinateId,NormalId,ColorId,VertexId,RenderConfigId};
use strafesnet_common::model::{Color4,TextureCoordinate,Mesh,MeshBuilder,IndexedGraphicsGroup,IndexedPhysicsGroup,IndexedVertex,PolygonGroupId,PolygonGroup,PolygonList,PositionId,TextureCoordinateId,NormalId,ColorId,VertexId,RenderConfigId}; use strafesnet_common::integer::{vec3,Planar64Vec3};
use strafesnet_common::integer::{vec3,Planar64,Planar64Vec3};
#[derive(Debug)] #[derive(Debug)]
pub enum Primitives{ pub enum Primitives{
@ -10,22 +9,7 @@ pub enum Primitives{
Wedge, Wedge,
CornerWedge, CornerWedge,
} }
#[derive(Debug)] #[derive(Hash,PartialEq,Eq)]
pub struct PrimitivesError;
impl TryFrom<u32> for Primitives{
type Error=PrimitivesError;
fn try_from(value:u32)->Result<Self,Self::Error>{
match value{
0=>Ok(Primitives::Sphere),
1=>Ok(Primitives::Cube),
2=>Ok(Primitives::Cylinder),
3=>Ok(Primitives::Wedge),
4=>Ok(Primitives::CornerWedge),
_=>Err(PrimitivesError),
}
}
}
#[derive(Clone,Copy,Hash,PartialEq,Eq)]
pub enum CubeFace{ pub enum CubeFace{
Right, Right,
Top, Top,
@ -34,29 +18,13 @@ pub enum CubeFace{
Bottom, Bottom,
Front, Front,
} }
#[derive(Debug)]
pub struct CubeFaceError;
impl TryFrom<u32> for CubeFace{
type Error=CubeFaceError;
fn try_from(value:u32)->Result<Self,Self::Error>{
match value{
0=>Ok(CubeFace::Right),
1=>Ok(CubeFace::Top),
2=>Ok(CubeFace::Back),
3=>Ok(CubeFace::Left),
4=>Ok(CubeFace::Bottom),
5=>Ok(CubeFace::Front),
_=>Err(CubeFaceError),
}
}
}
const CUBE_DEFAULT_TEXTURE_COORDS:[TextureCoordinate;4]=[ const CUBE_DEFAULT_TEXTURE_COORDS:[TextureCoordinate;4]=[
TextureCoordinate::new(0.0,0.0), TextureCoordinate::new(0.0,0.0),
TextureCoordinate::new(1.0,0.0), TextureCoordinate::new(1.0,0.0),
TextureCoordinate::new(1.0,1.0), TextureCoordinate::new(1.0,1.0),
TextureCoordinate::new(0.0,1.0), TextureCoordinate::new(0.0,1.0),
]; ];
pub const CUBE_DEFAULT_VERTICES:[Planar64Vec3;8]=[ const CUBE_DEFAULT_VERTICES:[Planar64Vec3;8]=[
vec3::int(-1,-1, 1),//0 left bottom back vec3::int(-1,-1, 1),//0 left bottom back
vec3::int( 1,-1, 1),//1 right bottom back vec3::int( 1,-1, 1),//1 right bottom back
vec3::int( 1, 1, 1),//2 right top back vec3::int( 1, 1, 1),//2 right top back
@ -66,7 +34,7 @@ pub const CUBE_DEFAULT_VERTICES:[Planar64Vec3;8]=[
vec3::int( 1,-1,-1),//6 right bottom front vec3::int( 1,-1,-1),//6 right bottom front
vec3::int(-1,-1,-1),//7 left bottom front vec3::int(-1,-1,-1),//7 left bottom front
]; ];
pub const CUBE_DEFAULT_NORMALS:[Planar64Vec3;6]=[ const CUBE_DEFAULT_NORMALS:[Planar64Vec3;6]=[
vec3::int( 1, 0, 0),//CubeFace::Right vec3::int( 1, 0, 0),//CubeFace::Right
vec3::int( 0, 1, 0),//CubeFace::Top vec3::int( 0, 1, 0),//CubeFace::Top
vec3::int( 0, 0, 1),//CubeFace::Back vec3::int( 0, 0, 1),//CubeFace::Back
@ -75,36 +43,103 @@ pub const CUBE_DEFAULT_NORMALS:[Planar64Vec3;6]=[
vec3::int( 0, 0,-1),//CubeFace::Front vec3::int( 0, 0,-1),//CubeFace::Front
]; ];
pub struct CubeFaceDescription([FaceDescription;Self::FACES]); #[derive(Hash,PartialEq,Eq)]
pub enum WedgeFace{
Right,
TopFront,
Back,
Left,
Bottom,
}
const WEDGE_DEFAULT_NORMALS:[Planar64Vec3;5]=[
vec3::int( 1, 0, 0),//Wedge::Right
vec3::int( 0, 1,-1),//Wedge::TopFront
vec3::int( 0, 0, 1),//Wedge::Back
vec3::int(-1, 0, 0),//Wedge::Left
vec3::int( 0,-1, 0),//Wedge::Bottom
];
/*
local cornerWedgeVerticies = {
Vector3.new(-1/2,-1/2,-1/2),7
Vector3.new(-1/2,-1/2, 1/2),0
Vector3.new( 1/2,-1/2,-1/2),6
Vector3.new( 1/2,-1/2, 1/2),1
Vector3.new( 1/2, 1/2,-1/2),5
}
*/
#[derive(Hash,PartialEq,Eq)]
pub enum CornerWedgeFace{
Right,
TopBack,
TopLeft,
Bottom,
Front,
}
const CORNERWEDGE_DEFAULT_NORMALS:[Planar64Vec3;5]=[
vec3::int( 1, 0, 0),//CornerWedge::Right
vec3::int( 0, 1, 1),//CornerWedge::BackTop
vec3::int(-1, 1, 0),//CornerWedge::LeftTop
vec3::int( 0,-1, 0),//CornerWedge::Bottom
vec3::int( 0, 0,-1),//CornerWedge::Front
];
#[derive(Default)]
pub struct CubeFaceDescription([Option<FaceDescription>;6]);
impl CubeFaceDescription{ impl CubeFaceDescription{
pub const FACES:usize=6; pub fn insert(&mut self,index:CubeFace,value:FaceDescription){
pub fn new(RobloxPartDescription(part_description):RobloxPartDescription,textureless_render_id:RenderConfigId)->Self{ self.0[index as usize]=Some(value);
Self(part_description.map(|face_description|match face_description{ }
Some(roblox_texture_transform)=>roblox_texture_transform.to_face_description(), pub fn pairs(self)->impl Iterator<Item=(usize,FaceDescription)>{
None=>FaceDescription::new_with_render_id(textureless_render_id), self.0.into_iter().enumerate().filter_map(|(i,v)|v.map(|u|(i,u)))
}))
} }
} }
pub struct WedgeFaceDescription([FaceDescription;Self::FACES]); pub fn unit_cube(render:RenderConfigId)->Mesh{
let mut t=CubeFaceDescription::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)
}
#[derive(Default)]
pub struct WedgeFaceDescription([Option<FaceDescription>;5]);
impl WedgeFaceDescription{ impl WedgeFaceDescription{
pub const FACES:usize=5; pub fn insert(&mut self,index:WedgeFace,value:FaceDescription){
pub fn new(RobloxWedgeDescription(part_description):RobloxWedgeDescription,textureless_render_id:RenderConfigId)->Self{ self.0[index as usize]=Some(value);
Self(part_description.map(|face_description|match face_description{ }
Some(roblox_texture_transform)=>roblox_texture_transform.to_face_description(), pub fn pairs(self)->std::iter::FilterMap<std::iter::Enumerate<std::array::IntoIter<Option<FaceDescription>,5>>,impl FnMut((usize,Option<FaceDescription>))->Option<(usize,FaceDescription)>>{
None=>FaceDescription::new_with_render_id(textureless_render_id), self.0.into_iter().enumerate().filter_map(|v|v.1.map(|u|(v.0,u)))
}))
} }
} }
pub struct CornerWedgeFaceDescription([FaceDescription;Self::FACES]); // pub fn unit_wedge(render:RenderConfigId)->Mesh{
// let mut t=WedgeFaceDescription::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)]
pub struct CornerWedgeFaceDescription([Option<FaceDescription>;5]);
impl CornerWedgeFaceDescription{ impl CornerWedgeFaceDescription{
pub const FACES:usize=5; pub fn insert(&mut self,index:CornerWedgeFace,value:FaceDescription){
pub fn new(RobloxCornerWedgeDescription(part_description):RobloxCornerWedgeDescription,textureless_render_id:RenderConfigId)->Self{ self.0[index as usize]=Some(value);
Self(part_description.map(|face_description|match face_description{ }
Some(roblox_texture_transform)=>roblox_texture_transform.to_face_description(), pub fn pairs(self)->std::iter::FilterMap<std::iter::Enumerate<std::array::IntoIter<Option<FaceDescription>,5>>,impl FnMut((usize,Option<FaceDescription>))->Option<(usize,FaceDescription)>>{
None=>FaceDescription::new_with_render_id(textureless_render_id), self.0.into_iter().enumerate().filter_map(|v|v.1.map(|u|(v.0,u)))
}))
} }
} }
// pub fn unit_cornerwedge(render:RenderConfigId)->Mesh{
// let mut t=CornerWedgeFaceDescription::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)] #[derive(Clone)]
pub struct FaceDescription{ pub struct FaceDescription{
@ -113,7 +148,7 @@ pub struct FaceDescription{
pub color:Color4, pub color:Color4,
} }
impl FaceDescription{ impl FaceDescription{
pub fn new_with_render_id(render:RenderConfigId)->Self{ pub fn new_with_render_id(render:RenderConfigId)->Self {
Self{ Self{
render, render,
transform:glam::Affine2::IDENTITY, transform:glam::Affine2::IDENTITY,
@ -121,51 +156,51 @@ impl FaceDescription{
} }
} }
} }
pub const CUBE_DEFAULT_POLYS:[[[u32;2];4];6]=[ pub fn generate_partial_unit_cube(face_descriptions:CubeFaceDescription)->Mesh{
const CUBE_DEFAULT_POLYS:[[[u32;3];4];6]=[
// right (1, 0, 0) // right (1, 0, 0)
[ [
[6,2],//[vertex,tex] [6,2,0],//[vertex,tex,norm]
[5,1], [5,1,0],
[2,0], [2,0,0],
[1,3], [1,3,0],
], ],
// top (0, 1, 0) // top (0, 1, 0)
[ [
[5,3], [5,3,1],
[4,2], [4,2,1],
[3,1], [3,1,1],
[2,0], [2,0,1],
], ],
// back (0, 0, 1) // back (0, 0, 1)
[ [
[0,3], [0,3,2],
[1,2], [1,2,2],
[2,1], [2,1,2],
[3,0], [3,0,2],
], ],
// left (-1, 0, 0) // left (-1, 0, 0)
[ [
[0,2], [0,2,3],
[3,1], [3,1,3],
[4,0], [4,0,3],
[7,3], [7,3,3],
], ],
// bottom (0,-1, 0) // bottom (0,-1, 0)
[ [
[1,1], [1,1,4],
[0,0], [0,0,4],
[7,3], [7,3,4],
[6,2], [6,2,4],
], ],
// front (0, 0,-1) // front (0, 0,-1)
[ [
[4,1], [4,1,5],
[5,0], [5,0,5],
[6,3], [6,3,5],
[7,2], [7,2,5],
], ],
]; ];
pub fn unit_cube(CubeFaceDescription(face_descriptions):CubeFaceDescription)->Mesh{
let mut generated_pos=Vec::new(); let mut generated_pos=Vec::new();
let mut generated_tex=Vec::new(); let mut generated_tex=Vec::new();
let mut generated_normal=Vec::new(); let mut generated_normal=Vec::new();
@ -176,7 +211,7 @@ pub fn unit_cube(CubeFaceDescription(face_descriptions):CubeFaceDescription)->Me
let mut physics_group=IndexedPhysicsGroup::default(); let mut physics_group=IndexedPhysicsGroup::default();
let mut transforms=Vec::new(); 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. //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.into_iter().enumerate(){ for (face_id,face_description) in face_descriptions.pairs(){
//assume that scanning short lists is faster than hashing. //assume that scanning short lists is faster than hashing.
let transform_index=if let Some(transform_index)=transforms.iter().position(|&transform|transform==face_description.transform){ let transform_index=if let Some(transform_index)=transforms.iter().position(|&transform|transform==face_description.transform){
transform_index transform_index
@ -203,8 +238,8 @@ pub fn unit_cube(CubeFaceDescription(face_descriptions):CubeFaceDescription)->Me
//push vertices as they are needed //push vertices as they are needed
let group_id=PolygonGroupId::new(polygon_groups.len() as u32); let group_id=PolygonGroupId::new(polygon_groups.len() as u32);
polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(vec![ polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(vec![
CUBE_DEFAULT_POLYS[face_id].map(|[pos_id,tex_id]|{ CUBE_DEFAULT_POLYS[face_id].map(|tup|{
let pos=CUBE_DEFAULT_VERTICES[pos_id as usize]; let pos=CUBE_DEFAULT_VERTICES[tup[0] as usize];
let pos_index=if let Some(pos_index)=generated_pos.iter().position(|&p|p==pos){ let pos_index=if let Some(pos_index)=generated_pos.iter().position(|&p|p==pos){
pos_index pos_index
}else{ }else{
@ -216,7 +251,7 @@ pub fn unit_cube(CubeFaceDescription(face_descriptions):CubeFaceDescription)->Me
//always push vertex //always push vertex
let vertex=IndexedVertex{ let vertex=IndexedVertex{
pos:PositionId::new(pos_index), pos:PositionId::new(pos_index),
tex:TextureCoordinateId::new(tex_id+4*transform_index), tex:TextureCoordinateId::new(tup[1]+4*transform_index),
normal:NormalId::new(normal_index), normal:NormalId::new(normal_index),
color:ColorId::new(color_index), color:ColorId::new(color_index),
}; };
@ -243,49 +278,42 @@ pub fn unit_cube(CubeFaceDescription(face_descriptions):CubeFaceDescription)->Me
} }
} }
//don't think too hard about the copy paste because this is all going into the map tool eventually... //don't think too hard about the copy paste because this is all going into the map tool eventually...
pub fn unit_wedge(WedgeFaceDescription(face_descriptions):WedgeFaceDescription)->Mesh{ pub fn generate_partial_unit_wedge(face_descriptions:WedgeFaceDescription)->Mesh{
const WEDGE_DEFAULT_POLYS:[&[[u32;2]];5]=[ const WEDGE_DEFAULT_POLYS:[&[[u32;3]];5]=[
// right (1, 0, 0) // right (1, 0, 0)
&[ &[
[6,2],//[vertex,tex] [6,2,0],//[vertex,tex,norm]
[2,0], [2,0,0],
[1,3], [1,3,0],
], ],
// FrontTop (0, 1, -1) // FrontTop (0, 1, -1)
&[ &[
[3,1], [3,1,1],
[2,0], [2,0,1],
[6,3], [6,3,1],
[7,2], [7,2,1],
], ],
// back (0, 0, 1) // back (0, 0, 1)
&[ &[
[0,3], [0,3,2],
[1,2], [1,2,2],
[2,1], [2,1,2],
[3,0], [3,0,2],
], ],
// left (-1, 0, 0) // left (-1, 0, 0)
&[ &[
[0,2], [0,2,3],
[3,1], [3,1,3],
[7,3], [7,3,3],
], ],
// bottom (0,-1, 0) // bottom (0,-1, 0)
&[ &[
[1,1], [1,1,4],
[0,0], [0,0,4],
[7,3], [7,3,4],
[6,2], [6,2,4],
], ],
]; ];
const WEDGE_DEFAULT_NORMALS:[Planar64Vec3;5]=[
vec3::int( 1, 0, 0),//Wedge::Right
vec3::int( 0, 1,-1),//Wedge::TopFront
vec3::int( 0, 0, 1),//Wedge::Back
vec3::int(-1, 0, 0),//Wedge::Left
vec3::int( 0,-1, 0),//Wedge::Bottom
];
let mut generated_pos=Vec::new(); let mut generated_pos=Vec::new();
let mut generated_tex=Vec::new(); let mut generated_tex=Vec::new();
let mut generated_normal=Vec::new(); let mut generated_normal=Vec::new();
@ -296,7 +324,7 @@ pub fn unit_wedge(WedgeFaceDescription(face_descriptions):WedgeFaceDescription)-
let mut physics_group=IndexedPhysicsGroup::default(); let mut physics_group=IndexedPhysicsGroup::default();
let mut transforms=Vec::new(); 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. //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.into_iter().enumerate(){ for (face_id,face_description) in face_descriptions.pairs(){
//assume that scanning short lists is faster than hashing. //assume that scanning short lists is faster than hashing.
let transform_index=if let Some(transform_index)=transforms.iter().position(|&transform|transform==face_description.transform){ let transform_index=if let Some(transform_index)=transforms.iter().position(|&transform|transform==face_description.transform){
transform_index transform_index
@ -323,8 +351,8 @@ pub fn unit_wedge(WedgeFaceDescription(face_descriptions):WedgeFaceDescription)-
//push vertices as they are needed //push vertices as they are needed
let group_id=PolygonGroupId::new(polygon_groups.len() as u32); let group_id=PolygonGroupId::new(polygon_groups.len() as u32);
polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(vec![ polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(vec![
WEDGE_DEFAULT_POLYS[face_id].iter().map(|&[pos_id,tex_id]|{ WEDGE_DEFAULT_POLYS[face_id].iter().map(|tup|{
let pos=CUBE_DEFAULT_VERTICES[pos_id as usize]; let pos=CUBE_DEFAULT_VERTICES[tup[0] as usize];
let pos_index=if let Some(pos_index)=generated_pos.iter().position(|&p|p==pos){ let pos_index=if let Some(pos_index)=generated_pos.iter().position(|&p|p==pos){
pos_index pos_index
}else{ }else{
@ -336,7 +364,7 @@ pub fn unit_wedge(WedgeFaceDescription(face_descriptions):WedgeFaceDescription)-
//always push vertex //always push vertex
let vertex=IndexedVertex{ let vertex=IndexedVertex{
pos:PositionId::new(pos_index), pos:PositionId::new(pos_index),
tex:TextureCoordinateId::new(tex_id+4*transform_index), tex:TextureCoordinateId::new(tup[1]+4*transform_index),
normal:NormalId::new(normal_index), normal:NormalId::new(normal_index),
color:ColorId::new(color_index), color:ColorId::new(color_index),
}; };
@ -363,47 +391,40 @@ pub fn unit_wedge(WedgeFaceDescription(face_descriptions):WedgeFaceDescription)-
} }
} }
pub fn unit_cornerwedge(CornerWedgeFaceDescription(face_descriptions):CornerWedgeFaceDescription)->Mesh{ pub fn generate_partial_unit_cornerwedge(face_descriptions:CornerWedgeFaceDescription)->Mesh{
const CORNERWEDGE_DEFAULT_POLYS:[&[[u32;2]];5]=[ const CORNERWEDGE_DEFAULT_POLYS:[&[[u32;3]];5]=[
// right (1, 0, 0) // right (1, 0, 0)
&[ &[
[6,2],//[vertex,tex] [6,2,0],//[vertex,tex,norm]
[5,1], [5,1,0],
[1,3], [1,3,0],
], ],
// BackTop (0, 1, 1) // BackTop (0, 1, 1)
&[ &[
[5,3], [5,3,1],
[0,1], [0,1,1],
[1,0], [1,0,1],
], ],
// LeftTop (-1, 1, 0) // LeftTop (-1, 1, 0)
&[ &[
[5,3], [5,3,2],
[7,2], [7,2,2],
[0,1], [0,1,2],
], ],
// bottom (0,-1, 0) // bottom (0,-1, 0)
&[ &[
[1,1], [1,1,3],
[0,0], [0,0,3],
[7,3], [7,3,3],
[6,2], [6,2,3],
], ],
// front (0, 0,-1) // front (0, 0,-1)
&[ &[
[5,0], [5,0,4],
[6,3], [6,3,4],
[7,2], [7,2,4],
], ],
]; ];
const CORNERWEDGE_DEFAULT_NORMALS:[Planar64Vec3;5]=[
vec3::int( 1, 0, 0),//CornerWedge::Right
vec3::int( 0, 1, 1),//CornerWedge::BackTop
vec3::int(-1, 1, 0),//CornerWedge::LeftTop
vec3::int( 0,-1, 0),//CornerWedge::Bottom
vec3::int( 0, 0,-1),//CornerWedge::Front
];
let mut generated_pos=Vec::new(); let mut generated_pos=Vec::new();
let mut generated_tex=Vec::new(); let mut generated_tex=Vec::new();
let mut generated_normal=Vec::new(); let mut generated_normal=Vec::new();
@ -414,7 +435,7 @@ pub fn unit_cornerwedge(CornerWedgeFaceDescription(face_descriptions):CornerWedg
let mut physics_group=IndexedPhysicsGroup::default(); let mut physics_group=IndexedPhysicsGroup::default();
let mut transforms=Vec::new(); 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. //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.into_iter().enumerate(){ for (face_id,face_description) in face_descriptions.pairs(){
//assume that scanning short lists is faster than hashing. //assume that scanning short lists is faster than hashing.
let transform_index=if let Some(transform_index)=transforms.iter().position(|&transform|transform==face_description.transform){ let transform_index=if let Some(transform_index)=transforms.iter().position(|&transform|transform==face_description.transform){
transform_index transform_index
@ -441,8 +462,8 @@ pub fn unit_cornerwedge(CornerWedgeFaceDescription(face_descriptions):CornerWedg
//push vertices as they are needed //push vertices as they are needed
let group_id=PolygonGroupId::new(polygon_groups.len() as u32); let group_id=PolygonGroupId::new(polygon_groups.len() as u32);
polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(vec![ polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(vec![
CORNERWEDGE_DEFAULT_POLYS[face_id].iter().map(|&[pos_id,tex_id]|{ CORNERWEDGE_DEFAULT_POLYS[face_id].iter().map(|tup|{
let pos=CUBE_DEFAULT_VERTICES[pos_id as usize]; let pos=CUBE_DEFAULT_VERTICES[tup[0] as usize];
let pos_index=if let Some(pos_index)=generated_pos.iter().position(|&p|p==pos){ let pos_index=if let Some(pos_index)=generated_pos.iter().position(|&p|p==pos){
pos_index pos_index
}else{ }else{
@ -454,7 +475,7 @@ pub fn unit_cornerwedge(CornerWedgeFaceDescription(face_descriptions):CornerWedg
//always push vertex //always push vertex
let vertex=IndexedVertex{ let vertex=IndexedVertex{
pos:PositionId::new(pos_index), pos:PositionId::new(pos_index),
tex:TextureCoordinateId::new(tex_id+4*transform_index), tex:TextureCoordinateId::new(tup[1]+4*transform_index),
normal:NormalId::new(normal_index), normal:NormalId::new(normal_index),
color:ColorId::new(color_index), color:ColorId::new(color_index),
}; };
@ -480,133 +501,3 @@ pub fn unit_cornerwedge(CornerWedgeFaceDescription(face_descriptions):CornerWedg
physics_groups:vec![physics_group], physics_groups:vec![physics_group],
} }
} }
// TODO: fix face texture orientation
pub fn unit_cylinder(face_descriptions:CubeFaceDescription)->Mesh{
// cylinder is oriented about the x axis
// roblox cylinders use projected grid coordinates
/// how many grid coordinates to use (positive and negative)
const GON:i32=3;
/// grid perimeter
const POINTS:[[i32;2];4*2*GON as usize]=const{
let mut points=[[0;2];{4*2*GON as usize}];
let mut i=-GON;
while i<GON{
points[(i+GON) as usize]=[i,GON];
points[(i+GON+1*2*GON) as usize]=[GON,-i];
points[(i+GON+2*2*GON) as usize]=[-i,-GON];
points[(i+GON+3*2*GON) as usize]=[-GON,i];
i+=1;
}
points
};
let mut mb=MeshBuilder::new();
let mut polygon_groups=Vec::with_capacity(CubeFaceDescription::FACES);
let mut graphics_groups=Vec::with_capacity(CubeFaceDescription::FACES);
let mut physics_group=IndexedPhysicsGroup{groups:Vec::with_capacity(CubeFaceDescription::FACES)};
let CubeFaceDescription([right,top,back,left,bottom,front])=face_descriptions;
macro_rules! end_face{
($face_description:expr,$end:expr,$iter:expr)=>{
let normal=mb.acquire_normal_id($end);
let color=mb.acquire_color_id($face_description.color);
// single polygon for physics
let polygon:Vec<_>=$iter.map(|[x,y]|{
let tex=mb.acquire_tex_id(
$face_description.transform.transform_point2(
(glam::vec2(-x as f32,y as f32).normalize()+1.0)/2.0
)
);
let pos=mb.acquire_pos_id($end+vec3::int(0,-x,y).with_length(Planar64::ONE).divide().wrap_1());
mb.acquire_vertex_id(IndexedVertex{pos,tex,normal,color})
}).collect();
// fanned polygons for graphics
let pos=mb.acquire_pos_id($end);
let tex=mb.acquire_tex_id($face_description.transform.transform_point2(glam::Vec2::ONE/2.0));
let center=mb.acquire_vertex_id(IndexedVertex{pos,tex,normal,color});
let polygon_list=(0..POINTS.len()).map(|i|
vec![center,polygon[i],polygon[(i+1)%POINTS.len()]]
).collect();
// end face graphics
let group_id=PolygonGroupId::new(polygon_groups.len() as u32);
polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(polygon_list)));
graphics_groups.push(IndexedGraphicsGroup{
render:$face_description.render,
groups:vec![group_id],
});
// end face physics
let polygon_list=vec![polygon];
let group_id=PolygonGroupId::new(polygon_groups.len() as u32);
polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(polygon_list)));
physics_group.groups.push(group_id);
}
}
macro_rules! tex{
($face_description:expr,$tex:expr)=>{{
let [x,y]=$tex;
$face_description.transform.transform_point2(
glam::vec2((x+GON) as f32,(y+GON) as f32)/(2*GON) as f32
)
}};
}
macro_rules! barrel_face{
($face_description:expr,$loop:ident,$lo_dir:expr,$hi_dir:expr,$tex_0:expr,$tex_1:expr,$tex_2:expr,$tex_3:expr)=>{
let mut polygon_list=Vec::with_capacity(CubeFaceDescription::FACES);
for $loop in -GON..GON{
// lo Z
let lz_dir=$lo_dir.with_length(Planar64::ONE).divide().wrap_1();
// hi Z
let hz_dir=$hi_dir.with_length(Planar64::ONE).divide().wrap_1();
// pos
let lx_lz_pos=mb.acquire_pos_id(vec3::NEG_X+lz_dir);
let lx_hz_pos=mb.acquire_pos_id(vec3::NEG_X+hz_dir);
let hx_hz_pos=mb.acquire_pos_id(vec3::X+hz_dir);
let hx_lz_pos=mb.acquire_pos_id(vec3::X+lz_dir);
// tex
let lx_lz_tex=mb.acquire_tex_id(tex!($face_description,$tex_0));
let lx_hz_tex=mb.acquire_tex_id(tex!($face_description,$tex_1));
let hx_hz_tex=mb.acquire_tex_id(tex!($face_description,$tex_2));
let hx_lz_tex=mb.acquire_tex_id(tex!($face_description,$tex_3));
// norm
let lz_norm=mb.acquire_normal_id(lz_dir);
let hz_norm=mb.acquire_normal_id(hz_dir);
// color
let color=mb.acquire_color_id($face_description.color);
polygon_list.push(vec![
mb.acquire_vertex_id(IndexedVertex{pos:lx_lz_pos,tex:lx_lz_tex,normal:lz_norm,color}),
mb.acquire_vertex_id(IndexedVertex{pos:lx_hz_pos,tex:lx_hz_tex,normal:hz_norm,color}),
mb.acquire_vertex_id(IndexedVertex{pos:hx_hz_pos,tex:hx_hz_tex,normal:hz_norm,color}),
mb.acquire_vertex_id(IndexedVertex{pos:hx_lz_pos,tex:hx_lz_tex,normal:lz_norm,color}),
]);
}
// push face
let group_id=PolygonGroupId::new(polygon_groups.len() as u32);
polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(polygon_list)));
graphics_groups.push(IndexedGraphicsGroup{
render:$face_description.render,
groups:vec![group_id],
});
physics_group.groups.push(group_id);
};
}
end_face!(right, vec3::X,POINTS.into_iter());
barrel_face!(top, z,vec3::int(0,GON,z),vec3::int(0,GON,z+1), [GON,z],[GON,z+1],[-GON,z+1],[-GON,z]);
barrel_face!(back, y,vec3::int(0,y+1,GON),vec3::int(0,y,GON), [GON,y+1],[GON,y],[-GON,y],[-GON,y+1]);
end_face!(left, vec3::NEG_X,POINTS.into_iter().rev());
barrel_face!(bottom, z,vec3::int(0,-GON,z+1),vec3::int(0,-GON,z), [-GON,z+1],[-GON,z],[GON,z],[GON,z+1]);
barrel_face!(front, y,vec3::int(0,y,-GON),vec3::int(0,y+1,-GON), [-GON,y],[-GON,y+1],[GON,y+1],[GON,y]);
let physics_groups=vec![physics_group];
mb.build(polygon_groups,graphics_groups,physics_groups)
}

File diff suppressed because it is too large Load Diff

@ -1,9 +1,4 @@
use crate::loader::MeshWithSize; use rbx_mesh::mesh_data::NormalId2 as MeshDataNormalId2;
use crate::rbx::RobloxPartDescription;
use crate::primitives::{CUBE_DEFAULT_VERTICES,CUBE_DEFAULT_POLYS};
use rbx_mesh::mesh_data::{VertexId as MeshDataVertexId,NormalId2 as MeshDataNormalId2};
use rbx_mesh::physics_data::VertexId as PhysicsDataVertexId;
use strafesnet_common::model::{self,IndexedVertex,PolygonGroup,PolygonGroupId,PolygonList,RenderConfigId}; use strafesnet_common::model::{self,IndexedVertex,PolygonGroup,PolygonGroupId,PolygonList,RenderConfigId};
use strafesnet_common::integer::vec3; use strafesnet_common::integer::vec3;
@ -61,10 +56,10 @@ pub fn convert(
roblox_physics_data:&[u8], roblox_physics_data:&[u8],
roblox_mesh_data:&[u8], roblox_mesh_data:&[u8],
size:glam::Vec3, size:glam::Vec3,
RobloxPartDescription(part_texture_description):RobloxPartDescription, part_texture_description:crate::rbx::RobloxPartDescription,
)->Result<MeshWithSize,Error>{ )->Result<model::Mesh,Error>{
const NORMAL_FACES:usize=6; const NORMAL_FACES:usize=6;
let mut polygon_groups_normal_id:[_;NORMAL_FACES]=[vec![],vec![],vec![],vec![],vec![],vec![]]; let mut polygon_groups_normal_id=vec![Vec::new();NORMAL_FACES];
// build graphics and physics meshes // build graphics and physics meshes
let mut mb=strafesnet_common::model::MeshBuilder::new(); let mut mb=strafesnet_common::model::MeshBuilder::new();
@ -81,29 +76,19 @@ pub fn convert(
).map_err(Error::RobloxMeshData)?; ).map_err(Error::RobloxMeshData)?;
let graphics_mesh=match mesh_data{ let graphics_mesh=match mesh_data{
rbx_mesh::mesh_data::MeshData::CSGK(_)=>return Err(Error::Block), rbx_mesh::mesh_data::MeshData::CSGK(_)=>return Err(Error::Block),
rbx_mesh::mesh_data::MeshData::CSGMDL(rbx_mesh::mesh_data::CSGMDL::V2(mesh_data2))=>mesh_data2.mesh, rbx_mesh::mesh_data::MeshData::CSGMDL(rbx_mesh::mesh_data::CSGMDL::CSGMDL2(mesh_data2))=>mesh_data2.mesh,
rbx_mesh::mesh_data::MeshData::CSGMDL(rbx_mesh::mesh_data::CSGMDL::V4(mesh_data4))=>mesh_data4.mesh, rbx_mesh::mesh_data::MeshData::CSGMDL(rbx_mesh::mesh_data::CSGMDL::CSGMDL4(mesh_data4))=>mesh_data4.mesh,
}; };
//autoscale to size, idk what roblox is doing with the graphics mesh size for [vertex_id0,vertex_id1,vertex_id2] in graphics_mesh.faces{
let mut pos_min=glam::Vec3::MAX;
let mut pos_max=glam::Vec3::MIN;
for vertex in &graphics_mesh.vertices{
let p=vertex.pos.into();
pos_min=pos_min.min(p);
pos_max=pos_max.max(p);
}
let graphics_size=pos_max-pos_min;
for [MeshDataVertexId(vertex_id0),MeshDataVertexId(vertex_id1),MeshDataVertexId(vertex_id2)] in graphics_mesh.faces{
let face=[ let face=[
graphics_mesh.vertices.get(vertex_id0 as usize).ok_or(Error::MissingVertexId(vertex_id0))?, graphics_mesh.vertices.get(vertex_id0.0 as usize).ok_or(Error::MissingVertexId(vertex_id0.0))?,
graphics_mesh.vertices.get(vertex_id1 as usize).ok_or(Error::MissingVertexId(vertex_id1))?, graphics_mesh.vertices.get(vertex_id1.0 as usize).ok_or(Error::MissingVertexId(vertex_id1.0))?,
graphics_mesh.vertices.get(vertex_id2 as usize).ok_or(Error::MissingVertexId(vertex_id2))?, graphics_mesh.vertices.get(vertex_id2.0 as usize).ok_or(Error::MissingVertexId(vertex_id2.0))?,
]; ];
let mut normal_agreement_checker=MeshDataNormalChecker::new(); let mut normal_agreement_checker=MeshDataNormalChecker::new();
let face=face.into_iter().map(|vertex|{ let face=face.into_iter().map(|vertex|{
normal_agreement_checker.check(vertex.normal_id); normal_agreement_checker.check(vertex.normal_id);
let pos=glam::Vec3::from_array(vertex.pos)/graphics_size; let pos=mb.acquire_pos_id(vec3::try_from_f32_array(vertex.pos)?);
let pos=mb.acquire_pos_id(vec3::try_from_f32_array(pos.to_array())?);
let normal=mb.acquire_normal_id(vec3::try_from_f32_array(vertex.norm)?); let normal=mb.acquire_normal_id(vec3::try_from_f32_array(vertex.norm)?);
let tex_coord=glam::Vec2::from_array(vertex.tex); let tex_coord=glam::Vec2::from_array(vertex.tex);
let maybe_face_description=&cube_face_description[vertex.normal_id as usize-1]; let maybe_face_description=&cube_face_description[vertex.normal_id as usize-1];
@ -140,11 +125,7 @@ pub fn convert(
}; };
//physics //physics
let polygon_groups_normal_it=polygon_groups_normal_id.into_iter().map(|faces| let physics_convex_meshes=if !roblox_physics_data.is_empty(){
// graphics polygon groups (to be rendered)
Ok(PolygonGroup::PolygonList(PolygonList::new(faces)))
);
let polygon_groups:Vec<PolygonGroup>=if !roblox_physics_data.is_empty(){
let physics_data=rbx_mesh::read_physics_data_versioned( let physics_data=rbx_mesh::read_physics_data_versioned(
std::io::Cursor::new(roblox_physics_data) std::io::Cursor::new(roblox_physics_data)
).map_err(Error::RobloxPhysicsData)?; ).map_err(Error::RobloxPhysicsData)?;
@ -153,56 +134,44 @@ pub fn convert(
// have not seen this format in practice // have not seen this format in practice
|rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::Block) |rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::Block)
=>return Err(Error::Block), =>return Err(Error::Block),
rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::V3(meshes)) rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::Meshes(meshes))
|rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::V5(meshes))
=>meshes.meshes,
rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::V6(meshes))
=>vec![meshes.mesh],
rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::V7(meshes))
=>meshes.meshes, =>meshes.meshes,
rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::PhysicsInfoMesh(pim))
=>vec![pim.mesh],
}; };
let physics_convex_meshes_it=physics_convex_meshes.into_iter().map(|mesh|{ physics_convex_meshes
// this can be factored out of the loop but I am lazy
let color=mb.acquire_color_id(glam::Vec4::ONE);
let tex=mb.acquire_tex_id(glam::Vec2::ZERO);
// physics polygon groups (to do physics)
Ok(PolygonGroup::PolygonList(PolygonList::new(mesh.faces.into_iter().map(|[PhysicsDataVertexId(vertex_id0),PhysicsDataVertexId(vertex_id1),PhysicsDataVertexId(vertex_id2)]|{
let face=[
mesh.vertices.get(vertex_id0 as usize).ok_or(Error::MissingVertexId(vertex_id0))?,
mesh.vertices.get(vertex_id1 as usize).ok_or(Error::MissingVertexId(vertex_id1))?,
mesh.vertices.get(vertex_id2 as usize).ok_or(Error::MissingVertexId(vertex_id2))?,
].map(|v|glam::Vec3::from_slice(v)/size);
let vertex_norm=(face[1]-face[0])
.cross(face[2]-face[0]);
let normal=mb.acquire_normal_id(vec3::try_from_f32_array(vertex_norm.to_array()).map_err(Error::Planar64Vec3)?);
face.into_iter().map(|vertex_pos|{
let pos=mb.acquire_pos_id(vec3::try_from_f32_array(vertex_pos.to_array()).map_err(Error::Planar64Vec3)?);
Ok(mb.acquire_vertex_id(IndexedVertex{pos,tex,normal,color}))
}).collect()
}).collect::<Result<_,_>>()?)))
});
polygon_groups_normal_it.chain(physics_convex_meshes_it).collect::<Result<_,_>>()?
}else{ }else{
// generate a unit cube as default physics Vec::new()
let pos_list=CUBE_DEFAULT_VERTICES.map(|pos|mb.acquire_pos_id(pos>>1));
let tex=mb.acquire_tex_id(glam::Vec2::ZERO);
let normal=mb.acquire_normal_id(vec3::ZERO);
let color=mb.acquire_color_id(glam::Vec4::ONE);
let polygon_group=PolygonGroup::PolygonList(PolygonList::new(CUBE_DEFAULT_POLYS.map(|poly|poly.map(|[pos_id,_]|
mb.acquire_vertex_id(IndexedVertex{pos:pos_list[pos_id as usize],tex,normal,color})
).to_vec()).to_vec()));
polygon_groups_normal_it.chain([Ok(polygon_group)]).collect::<Result<_,_>>()?
}; };
let polygon_groups:Vec<PolygonGroup>=polygon_groups_normal_id.into_iter().map(|faces|
// graphics polygon groups (to be rendered)
Ok(PolygonGroup::PolygonList(PolygonList::new(faces)))
).chain(physics_convex_meshes.into_iter().map(|mesh|{
// this can be factored out of the loop but I am lazy
let color=mb.acquire_color_id(glam::Vec4::ONE);
let tex=mb.acquire_tex_id(glam::Vec2::ZERO);
// physics polygon groups (to do physics)
Ok(PolygonGroup::PolygonList(PolygonList::new(mesh.faces.into_iter().map(|[vertex_id0,vertex_id1,vertex_id2]|{
let face=[
mesh.vertices.get(vertex_id0.0 as usize).ok_or(Error::MissingVertexId(vertex_id0.0))?,
mesh.vertices.get(vertex_id1.0 as usize).ok_or(Error::MissingVertexId(vertex_id1.0))?,
mesh.vertices.get(vertex_id2.0 as usize).ok_or(Error::MissingVertexId(vertex_id2.0))?,
].map(|v|glam::Vec3::from_slice(v)/size);
let vertex_norm=(face[1]-face[0])
.cross(face[2]-face[0]);
let normal=mb.acquire_normal_id(vec3::try_from_f32_array(vertex_norm.to_array()).map_err(Error::Planar64Vec3)?);
face.into_iter().map(|vertex_pos|{
let pos=mb.acquire_pos_id(vec3::try_from_f32_array(vertex_pos.to_array()).map_err(Error::Planar64Vec3)?);
Ok(mb.acquire_vertex_id(IndexedVertex{pos,tex,normal,color}))
}).collect()
}).collect::<Result<_,_>>()?)))
})).collect::<Result<_,_>>()?;
let physics_groups=(NORMAL_FACES..polygon_groups.len()).map(|id|model::IndexedPhysicsGroup{ let physics_groups=(NORMAL_FACES..polygon_groups.len()).map(|id|model::IndexedPhysicsGroup{
groups:vec![PolygonGroupId::new(id as u32)] groups:vec![PolygonGroupId::new(id as u32)]
}).collect(); }).collect();
let mesh=mb.build( Ok(mb.build(
polygon_groups, polygon_groups,
graphics_groups, graphics_groups,
physics_groups, physics_groups,
); ))
Ok(MeshWithSize{
mesh,
size:vec3::ONE,
})
} }

@ -1,6 +1,6 @@
[package] [package]
name = "roblox_emulator" name = "roblox_emulator"
version = "0.5.1" version = "0.4.7"
edition = "2024" edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project" repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
@ -15,7 +15,7 @@ run-service=[]
glam = "0.30.0" glam = "0.30.0"
mlua = { version = "0.10.1", features = ["luau"] } mlua = { version = "0.10.1", features = ["luau"] }
phf = { version = "0.11.2", features = ["macros"] } phf = { version = "0.11.2", features = ["macros"] }
rbx_dom_weak = { version = "3.1.0-sn4", registry = "strafesnet", features = ["instance-userdata"] } rbx_dom_weak = { version = "2.7.0", registry = "strafesnet" }
rbx_reflection = "5.0.0" rbx_reflection = { version = "4.7.0", registry = "strafesnet" }
rbx_reflection_database = "1.0.0" rbx_reflection_database = { version = "0.2.10", registry = "strafesnet" }
rbx_types = "2.0.0" rbx_types = { version = "1.10.0", registry = "strafesnet" }

@ -1,121 +1,93 @@
use crate::util::static_ustr;
use rbx_dom_weak::{types::Ref,InstanceBuilder,WeakDom}; use rbx_dom_weak::{types::Ref,InstanceBuilder,WeakDom};
#[derive(Debug)] pub fn class_is_a(class:&str,superclass:&str)->bool{
pub enum ServicesError{ class==superclass
WorkspaceNotFound, ||rbx_reflection_database::get().classes.get(class)
} .is_some_and(|descriptor|
impl std::fmt::Display for ServicesError{ descriptor.superclass.as_ref().is_some_and(|class_super|
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ class_is_a(class_super,superclass)
write!(f,"{self:?}") )
} )
}
impl std::error::Error for ServicesError{}
pub struct Services{
pub(crate) game:Ref,
pub(crate) workspace:Ref,
} }
impl Services{ #[repr(transparent)]
fn find_services(dom:&WeakDom)->Result<Services,ServicesError>{
Ok(Services{
workspace:*dom.root().children().iter().find(|&&r|
dom.get_by_ref(r).is_some_and(|instance|instance.class=="Workspace")
).ok_or(ServicesError::WorkspaceNotFound)?,
game:dom.root_ref(),
})
}
}
pub type LuaAppData=&'static mut WeakDom;
pub struct Context{ pub struct Context{
pub(crate)dom:WeakDom, pub(crate)dom:WeakDom,
pub(crate)services:Services,
} }
impl Context{ impl Context{
pub fn from_place(dom:WeakDom)->Result<Context,ServicesError>{ pub const fn new(dom:WeakDom)->Self{
let services=Services::find_services(&dom)?; Self{dom}
Ok(Self{dom,services})
} }
pub fn script_singleton(source:String)->(Context,crate::runner::instance::Instance){ pub fn script_singleton(source:String)->(Context,crate::runner::instance::Instance,Services){
let script=InstanceBuilder::new("Script") let script=InstanceBuilder::new("Script")
.with_property("Source",rbx_types::Variant::String(source)); .with_property("Source",rbx_types::Variant::String(source));
let script_ref=script.referent(); let script_ref=script.referent();
let dom=WeakDom::new( let mut context=Self::new(WeakDom::new(
InstanceBuilder::new("DataModel") InstanceBuilder::new("DataModel")
.with_child(script) .with_child(script)
); ));
let context=Self::from_model(dom); let services=context.convert_into_place();
(context,crate::runner::instance::Instance::new_unchecked(script_ref)) (context,crate::runner::instance::Instance::new(script_ref),services)
}
pub fn from_ref(dom:&WeakDom)->&Context{
unsafe{&*(dom as *const WeakDom as *const Context)}
}
pub fn from_mut(dom:&mut WeakDom)->&mut Context{
unsafe{&mut *(dom as *mut WeakDom as *mut Context)}
} }
/// Creates an iterator over all items of a particular class. /// Creates an iterator over all items of a particular class.
pub fn superclass_iter<'a>(&'a self,superclass:&'a str)->impl Iterator<Item=Ref>+'a{ pub fn superclass_iter<'a>(&'a self,superclass:&'a str)->impl Iterator<Item=Ref>+'a{
let db=rbx_reflection_database::get(); self.dom.descendants().filter(|&instance|
let Some(superclass)=db.classes.get(superclass)else{ class_is_a(instance.class.as_ref(),superclass)
panic!("Invalid class"); ).map(|instance|instance.referent())
};
self.dom.descendants().filter_map(|instance|{
let class=db.classes.get(instance.class.as_str())?;
db.has_superclass(class,superclass).then(||instance.referent())
})
} }
pub fn scripts(&self)->Vec<crate::runner::instance::Instance>{ pub fn scripts(&self)->Vec<crate::runner::instance::Instance>{
self.superclass_iter("Script") self.superclass_iter("LuaSourceContainer").map(crate::runner::instance::Instance::new).collect()
.filter_map(|script_ref|{
let script=self.dom.get_by_ref(script_ref)?;
if let None|Some(rbx_dom_weak::types::Variant::Bool(false))=script.properties.get(&static_ustr("Disabled")){
return Some(crate::runner::instance::Instance::new_unchecked(script_ref));
}
None
})
.collect()
} }
pub fn from_model(mut dom:WeakDom)->Context{ pub fn find_services(&self)->Option<Services>{
Some(Services{
workspace:*self.dom.root().children().iter().find(|&&r|
self.dom.get_by_ref(r).is_some_and(|instance|instance.class=="Workspace")
)?,
game:self.dom.root_ref(),
})
}
pub fn convert_into_place(&mut self)->Services{
//snapshot root instances //snapshot root instances
let children=dom.root().children().to_owned(); let children=self.dom.root().children().to_owned();
//insert services //insert services
let game=dom.root_ref(); let game=self.dom.root_ref();
let terrain_bldr=InstanceBuilder::new("Terrain") let terrain_bldr=InstanceBuilder::new("Terrain");
.with_properties([ let workspace=self.dom.insert(game,
("CFrame",rbx_dom_weak::types::Variant::CFrame(rbx_dom_weak::types::CFrame::new(rbx_dom_weak::types::Vector3::new(0.0,0.0,0.0),rbx_dom_weak::types::Matrix3::identity()))),
("Size",rbx_dom_weak::types::Variant::Vector3(rbx_dom_weak::types::Vector3::new(1.0,1.0,1.0))),
("Velocity",rbx_dom_weak::types::Variant::Vector3(rbx_dom_weak::types::Vector3::new(0.0,0.0,0.0))),
("Transparency",rbx_dom_weak::types::Variant::Float32(0.0)),
("Color",rbx_dom_weak::types::Variant::Color3uint8(rbx_dom_weak::types::Color3uint8::new(255,255,255))),
("CanCollide",rbx_dom_weak::types::Variant::Bool(true)),
]);
let workspace=dom.insert(game,
InstanceBuilder::new("Workspace") InstanceBuilder::new("Workspace")
//Set Workspace.Terrain property equal to Terrain //Set Workspace.Terrain property equal to Terrain
.with_property("Terrain",terrain_bldr.referent()) .with_property("Terrain",terrain_bldr.referent())
.with_child(terrain_bldr) .with_child(terrain_bldr)
); );
{
//Lowercase and upper case workspace property!
let game=self.dom.root_mut();
game.properties.insert("workspace".to_owned(),rbx_types::Variant::Ref(workspace));
game.properties.insert("Workspace".to_owned(),rbx_types::Variant::Ref(workspace));
}
self.dom.insert(game,InstanceBuilder::new("Lighting"));
//transfer original root instances into workspace //transfer original root instances into workspace
for instance in children{ for instance in children{
dom.transfer_within(instance,workspace); self.dom.transfer_within(instance,workspace);
} }
{ Services{
//Lowercase and upper case workspace property! game,
let game=dom.root_mut(); workspace,
// TODO: DELETE THIS!
game.properties.insert(static_ustr("workspace"),rbx_types::Variant::Ref(workspace));
game.properties.insert(static_ustr("Workspace"),rbx_types::Variant::Ref(workspace));
} }
dom.insert(game,InstanceBuilder::new("Lighting"));
let services=Services{game,workspace};
Self{dom,services}
} }
} }
impl AsRef<WeakDom> for Context{ pub struct Services{
fn as_ref(&self)->&WeakDom{ pub game:Ref,
&self.dom pub workspace:Ref,
}
} }

@ -1,4 +1,3 @@
mod util;
pub mod runner; pub mod runner;
pub mod context; pub mod context;
#[cfg(feature="run-service")] #[cfg(feature="run-service")]
@ -6,7 +5,3 @@ pub(crate) mod scheduler;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
pub mod mlua{
pub use mlua::{Result,Error};
}

@ -1,59 +0,0 @@
use super::color3::Color3;
#[derive(Clone,Copy)]
pub struct BrickColor(rbx_types::BrickColor);
impl BrickColor{
pub fn from_name(name:&str)->Option<Self>{
Some(BrickColor(rbx_types::BrickColor::from_name(name)?))
}
pub fn from_number(number:u16)->Option<Self>{
Some(BrickColor(rbx_types::BrickColor::from_number(number)?))
}
}
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
let table=lua.create_table()?;
table.raw_set("new",
lua.create_function(|_,r:mlua::Value|
match r{
mlua::Value::String(name)=>Ok(BrickColor::from_name(&*name.to_str()?)),
mlua::Value::Integer(number)=>Ok(BrickColor::from_number(number as u16)),
_=>Err(mlua::Error::runtime("Unsupported arguments"))
}
)?
)?;
macro_rules! brickcolor_constructor{
($fname:expr,$internal:ident)=>{
table.raw_set($fname,
lua.create_function(|_,_:()|
Ok(BrickColor(rbx_types::BrickColor::$internal))
)?
)?;
};
}
brickcolor_constructor!("White",White);
brickcolor_constructor!("Gray",MediumStoneGrey);
brickcolor_constructor!("DarkGray",DarkStoneGrey);
brickcolor_constructor!("Black",Black);
brickcolor_constructor!("Red",BrightRed);
brickcolor_constructor!("Yellow",BrightYellow);
brickcolor_constructor!("Green",DarkGreen);
brickcolor_constructor!("Blue",BrightBlue);
globals.set("BrickColor",table)?;
Ok(())
}
impl mlua::UserData for BrickColor{
fn add_fields<F:mlua::UserDataFields<Self>>(fields:&mut F){
fields.add_field_method_get("Color",|_,BrickColor(this)|{
let rbx_types::Color3uint8{r,g,b}=this.to_color3uint8();
Ok(Color3::from_rgb(r,g,b))
});
}
}
type_from_lua_userdata!(BrickColor);

@ -1,10 +1,7 @@
use mlua::FromLua;
use super::number::Number;
use super::vector3::Vector3; use super::vector3::Vector3;
#[derive(Clone,Copy)] #[derive(Clone,Copy)]
pub struct CFrame(glam::Affine3A); pub struct CFrame(pub(crate)glam::Affine3A);
impl CFrame{ impl CFrame{
pub fn new( pub fn new(
@ -37,14 +34,14 @@ fn vec3_from_glam(v:rbx_types::Vector3)->glam::Vec3A{
glam::vec3a(v.x,v.y,v.z) glam::vec3a(v.x,v.y,v.z)
} }
impl From<CFrame> for rbx_types::CFrame{ impl Into<rbx_types::CFrame> for CFrame{
fn from(CFrame(cf):CFrame)->rbx_types::CFrame{ fn into(self)->rbx_types::CFrame{
rbx_types::CFrame::new( rbx_types::CFrame::new(
vec3_to_glam(cf.translation), vec3_to_glam(self.0.translation),
rbx_types::Matrix3::new( rbx_types::Matrix3::new(
vec3_to_glam(cf.matrix3.x_axis), vec3_to_glam(self.0.matrix3.x_axis),
vec3_to_glam(cf.matrix3.y_axis), vec3_to_glam(self.0.matrix3.y_axis),
vec3_to_glam(cf.matrix3.z_axis), vec3_to_glam(self.0.matrix3.z_axis),
) )
) )
} }
@ -63,25 +60,16 @@ impl From<rbx_types::CFrame> for CFrame{
} }
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{ pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
let table=lua.create_table()?; let cframe_table=lua.create_table()?;
//CFrame.new //CFrame.new
table.raw_set("new", cframe_table.raw_set("new",
lua.create_function(|lua,tuple:( lua.create_function(|_,tuple:(
mlua::Value,mlua::Value,Option<Number>, mlua::Value,mlua::Value,Option<f32>,
Option<Number>,Option<Number>,Option<Number>, Option<f32>,Option<f32>,Option<f32>,
Option<Number>,Option<Number>,Option<Number>, Option<f32>,Option<f32>,Option<f32>,
Option<Number>,Option<Number>,Option<Number>, Option<f32>,Option<f32>,Option<f32>,
)|match tuple{ )|match tuple{
//CFrame.new()
(
mlua::Value::Nil,mlua::Value::Nil,None,
None,None,None,
None,None,None,
None,None,None,
)=>{
Ok(CFrame(glam::Affine3A::IDENTITY))
},
//CFrame.new(pos) //CFrame.new(pos)
( (
mlua::Value::UserData(pos),mlua::Value::Nil,None, mlua::Value::UserData(pos),mlua::Value::Nil,None,
@ -89,8 +77,8 @@ pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
None,None,None, None,None,None,
None,None,None, None,None,None,
)=>{ )=>{
let Vector3(pos):&Vector3=&*pos.borrow()?; let pos:Vector3=pos.take()?;
Ok(CFrame::point(pos.x,pos.y,pos.z)) Ok(CFrame::point(pos.0.x,pos.0.y,pos.0.z))
}, },
//TODO: CFrame.new(pos,look) //TODO: CFrame.new(pos,look)
( (
@ -99,99 +87,85 @@ pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
None,None,None, None,None,None,
None,None,None, None,None,None,
)=>{ )=>{
let _pos:&Vector3=&*pos.borrow()?; let _pos:Vector3=pos.take()?;
let _look:&Vector3=&*look.borrow()?; let _look:Vector3=look.take()?;
Err(mlua::Error::runtime("Not yet implemented")) Err(mlua::Error::runtime("Not yet implemented"))
}, },
//CFrame.new(x,y,z) //CFrame.new(x,y,z)
( (
x,y,Some(z), mlua::Value::Number(x),mlua::Value::Number(y),Some(z),
None,None,None, None,None,None,
None,None,None, None,None,None,
None,None,None, None,None,None,
)=>Ok(CFrame::point(Number::from_lua(x,lua)?.into(),Number::from_lua(y,lua)?.into(),z.into())), )=>Ok(CFrame::point(x as f32,y as f32,z)),
//CFrame.new(x,y,z,xx,yx,zx,xy,yy,zy,xz,yz,zz) //CFrame.new(x,y,z,xx,yx,zx,xy,yy,zy,xz,yz,zz)
( (
x,y,Some(z), mlua::Value::Number(x),mlua::Value::Number(y),Some(z),
Some(xx),Some(yx),Some(zx), Some(xx),Some(yx),Some(zx),
Some(xy),Some(yy),Some(zy), Some(xy),Some(yy),Some(zy),
Some(xz),Some(yz),Some(zz), Some(xz),Some(yz),Some(zz),
)=>Ok(CFrame::new(Number::from_lua(x,lua)?.into(),Number::from_lua(y,lua)?.into(),z.into(), )=>Ok(CFrame::new(x as f32,y as f32,z,
xx.into(),yx.into(),zx.into(), xx,yx,zx,
xy.into(),yy.into(),zy.into(), xy,yy,zy,
xz.into(),yz.into(),zz.into(), xz,yz,zz,
)), )),
_=>Err(mlua::Error::runtime("Invalid arguments")) _=>Err(mlua::Error::runtime("Invalid arguments"))
})? })?
)?; )?;
//CFrame.Angles //CFrame.Angles
let from_euler_angles=lua.create_function(|_,(x,y,z):(Number,Number,Number)| cframe_table.raw_set("Angles",
Ok(CFrame::angles(x.into(),y.into(),z.into())) lua.create_function(|_,(x,y,z):(f32,f32,f32)|
Ok(CFrame::angles(x,y,z))
)?
)?; )?;
table.raw_set("Angles",from_euler_angles.clone())?;
table.raw_set("fromEulerAnglesXYZ",from_euler_angles.clone())?;
table.raw_set("FromEulerAnglesXYZ",from_euler_angles)?;
globals.set("CFrame",table)?; globals.set("CFrame",cframe_table)?;
Ok(()) Ok(())
} }
impl mlua::UserData for CFrame{ impl mlua::UserData for CFrame{
fn add_fields<F:mlua::UserDataFields<Self>>(fields:&mut F){ fn add_fields<F:mlua::UserDataFields<Self>>(fields:&mut F){
fields.add_field_method_get("p",|_,CFrame(this)|Ok(Vector3(this.translation))); //CFrame.p
fields.add_field_method_get("x",|_,CFrame(this)|Ok(this.translation.x)); fields.add_field_method_get("p",|_,this|Ok(Vector3(this.0.translation)));
fields.add_field_method_get("X",|_,CFrame(this)|Ok(this.translation.x));
fields.add_field_method_get("y",|_,CFrame(this)|Ok(this.translation.y));
fields.add_field_method_get("Y",|_,CFrame(this)|Ok(this.translation.y));
fields.add_field_method_get("z",|_,CFrame(this)|Ok(this.translation.z));
fields.add_field_method_get("Z",|_,CFrame(this)|Ok(this.translation.z));
fields.add_field_method_get("rightVector",|_,CFrame(this)|Ok(Vector3(this.matrix3.x_axis)));
fields.add_field_method_get("RightVector",|_,CFrame(this)|Ok(Vector3(this.matrix3.x_axis)));
fields.add_field_method_get("upVector",|_,CFrame(this)|Ok(Vector3(this.matrix3.y_axis)));
fields.add_field_method_get("UpVector",|_,CFrame(this)|Ok(Vector3(this.matrix3.y_axis)));
fields.add_field_method_get("lookVector",|_,CFrame(this)|Ok(Vector3(-this.matrix3.z_axis)));
fields.add_field_method_get("LookVector",|_,CFrame(this)|Ok(Vector3(-this.matrix3.z_axis)));
fields.add_field_method_get("XVector",|_,CFrame(this)|Ok(Vector3(this.matrix3.row(0))));
fields.add_field_method_get("YVector",|_,CFrame(this)|Ok(Vector3(this.matrix3.row(1))));
fields.add_field_method_get("ZVector",|_,CFrame(this)|Ok(Vector3(this.matrix3.row(2))));
} }
fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){ fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){
methods.add_method("components",|_,CFrame(this),()|Ok(( methods.add_method("components",|_,this,()|Ok((
this.translation.x, this.0.translation.x,
this.translation.y, this.0.translation.y,
this.translation.z, this.0.translation.z,
this.matrix3.x_axis.x, this.0.matrix3.x_axis.x,
this.matrix3.y_axis.x, this.0.matrix3.y_axis.x,
this.matrix3.z_axis.x, this.0.matrix3.z_axis.x,
this.matrix3.x_axis.y, this.0.matrix3.x_axis.y,
this.matrix3.y_axis.y, this.0.matrix3.y_axis.y,
this.matrix3.z_axis.y, this.0.matrix3.z_axis.y,
this.matrix3.x_axis.z, this.0.matrix3.x_axis.z,
this.matrix3.y_axis.z, this.0.matrix3.y_axis.z,
this.matrix3.z_axis.z, this.0.matrix3.z_axis.z,
))); )));
methods.add_method("VectorToWorldSpace",|_,CFrame(this),Vector3(v):Vector3| methods.add_method("VectorToWorldSpace",|_,this,v:Vector3|
Ok(Vector3(this.transform_vector3a(v))) Ok(Vector3(this.0.transform_vector3a(v.0)))
); );
methods.add_meta_function(mlua::MetaMethod::Mul,|_,(CFrame(this),CFrame(val)):(Self,Self)|Ok(Self(this*val))); //methods.add_meta_method(mlua::MetaMethod::Mul,|_,this,val:&Vector3|Ok(Vector3(this.0.matrix3*val.0+this.0.translation)));
methods.add_meta_function(mlua::MetaMethod::ToString,|_,CFrame(this):Self| methods.add_meta_function(mlua::MetaMethod::Mul,|_,(this,val):(Self,Self)|Ok(Self(this.0*val.0)));
methods.add_meta_function(mlua::MetaMethod::ToString,|_,this:Self|
Ok(format!("CFrame.new({},{},{},{},{},{},{},{},{},{},{},{})", Ok(format!("CFrame.new({},{},{},{},{},{},{},{},{},{},{},{})",
this.translation.x, this.0.translation.x,
this.translation.y, this.0.translation.y,
this.translation.z, this.0.translation.z,
this.matrix3.x_axis.x, this.0.matrix3.x_axis.x,
this.matrix3.y_axis.x, this.0.matrix3.y_axis.x,
this.matrix3.z_axis.x, this.0.matrix3.z_axis.x,
this.matrix3.x_axis.y, this.0.matrix3.x_axis.y,
this.matrix3.y_axis.y, this.0.matrix3.y_axis.y,
this.matrix3.z_axis.y, this.0.matrix3.z_axis.y,
this.matrix3.x_axis.z, this.0.matrix3.x_axis.z,
this.matrix3.y_axis.z, this.0.matrix3.y_axis.z,
this.matrix3.z_axis.z, this.0.matrix3.z_axis.z,
)) ))
); );
} }

@ -1,5 +1,3 @@
use super::number::Number;
#[derive(Clone,Copy)] #[derive(Clone,Copy)]
pub struct Color3{ pub struct Color3{
r:f32, r:f32,
@ -10,37 +8,28 @@ impl Color3{
pub const fn new(r:f32,g:f32,b:f32)->Self{ pub const fn new(r:f32,g:f32,b:f32)->Self{
Self{r,g,b} Self{r,g,b}
} }
pub const fn from_rgb(r:u8,g:u8,b:u8)->Self{
Color3::new(r as f32/255.0,g as f32/255.0,b as f32/255.0)
}
} }
impl From<rbx_types::Color3> for Color3{ impl Into<rbx_types::Color3> for Color3{
fn from(value:rbx_types::Color3)->Color3{ fn into(self)->rbx_types::Color3{
Color3::new(value.r,value.g,value.b) rbx_types::Color3::new(self.r,self.g,self.b)
}
}
impl From<Color3> for rbx_types::Color3{
fn from(value:Color3)->rbx_types::Color3{
rbx_types::Color3::new(value.r,value.g,value.b)
} }
} }
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{ pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
let table=lua.create_table()?; let color3_table=lua.create_table()?;
table.raw_set("new", color3_table.raw_set("new",
lua.create_function(|_,(r,g,b):(Number,Number,Number)| lua.create_function(|_,(r,g,b):(f32,f32,f32)|
Ok(Color3::new(r.into(),g.into(),b.into())) Ok(Color3::new(r,g,b))
)?
)?;
color3_table.raw_set("fromRGB",
lua.create_function(|_,(r,g,b):(u8,u8,u8)|
Ok(Color3::new(r as f32/255.0,g as f32/255.0,b as f32/255.0))
)? )?
)?; )?;
let from_rgb=lua.create_function(|_,(r,g,b):(u8,u8,u8)| globals.set("Color3",color3_table)?;
Ok(Color3::from_rgb(r,g,b))
)?;
table.raw_set("fromRGB",from_rgb.clone())?;
table.raw_set("FromRGB",from_rgb)?;
globals.set("Color3",table)?;
Ok(()) Ok(())
} }

@ -1,29 +1,31 @@
#[derive(Clone)] #[derive(Clone,Copy)]
pub struct ColorSequence(rbx_types::ColorSequence); pub struct ColorSequence{}
impl ColorSequence{ impl ColorSequence{
pub const fn new(keypoints:Vec<rbx_types::ColorSequenceKeypoint>)->Self{ pub const fn new()->Self{
Self(rbx_types::ColorSequence{keypoints}) Self{}
} }
} }
impl From<ColorSequence> for rbx_types::ColorSequence{ impl Into<rbx_types::ColorSequence> for ColorSequence{
fn from(ColorSequence(value):ColorSequence)->rbx_types::ColorSequence{ fn into(self)->rbx_types::ColorSequence{
value rbx_types::ColorSequence{
keypoints:Vec::new()
}
} }
} }
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{ pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
let table=lua.create_table()?; let number_sequence_table=lua.create_table()?;
table.raw_set("new", number_sequence_table.raw_set("new",
lua.create_function(|_,_:mlua::MultiValue| lua.create_function(|_,_:mlua::MultiValue|
Ok(ColorSequence::new(Vec::new())) Ok(ColorSequence::new())
)? )?
)?; )?;
globals.set("ColorSequence",table)?; globals.set("ColorSequence",number_sequence_table)?;
Ok(()) Ok(())
} }
impl mlua::UserData for ColorSequence{} impl mlua::UserData for ColorSequence{}
type_from_lua_userdata_clone!(ColorSequence); type_from_lua_userdata!(ColorSequence);

@ -1,136 +1,63 @@
#[derive(Clone,Copy)] use mlua::IntoLua;
pub struct EnumItem<'a>{
name:Option<&'a str>,
value:u32,
}
impl<'a> EnumItem<'a>{
fn known_name((name,&value):(&'a std::borrow::Cow<'a,str>,&u32))->Self{
Self{name:Some(name.as_ref()),value}
}
}
impl<'a> From<rbx_types::Enum> for EnumItem<'a>{
fn from(e:rbx_types::Enum)->Self{
EnumItem{
name:None,
value:e.to_u32(),
}
}
}
impl From<EnumItem<'_>> for rbx_types::Enum{
fn from(e:EnumItem)->rbx_types::Enum{
rbx_types::Enum::from_u32(e.value)
}
}
impl PartialEq for EnumItem<'_>{
fn eq(&self,other:&EnumItem<'_>)->bool{
self.value==other.value&&{
// if both names are known, they must match, otherwise whatever
match (self.name,other.name){
(Some(lhs),Some(rhs))=>lhs==rhs,
_=>true,
}
}
}
}
#[derive(Clone,Copy)] #[derive(Clone,Copy)]
pub struct Enums; pub struct Enum(u32);
impl Enums{
pub fn get(&self,index:&str)->Option<EnumItems<'static>>{
let db=rbx_reflection_database::get();
db.enums.get(index).map(|ed|EnumItems{ed})
}
}
#[derive(Clone,Copy)] #[derive(Clone,Copy)]
pub struct EnumItems<'a>{ pub struct EnumItems;
#[derive(Clone,Copy)]
pub struct EnumItem<'a>{
ed:&'a rbx_reflection::EnumDescriptor<'a>, ed:&'a rbx_reflection::EnumDescriptor<'a>,
} }
impl<'a> EnumItems<'a>{ impl Into<rbx_types::Enum> for Enum{
pub fn from_value(&self,value:u32)->Option<EnumItem<'a>>{ fn into(self)->rbx_types::Enum{
self.ed.items.iter().find(|&(_,&v)|v==value).map(EnumItem::known_name) rbx_types::Enum::from_u32(self.0)
}
pub fn from_name(&self,name:&str)->Option<EnumItem<'a>>{
self.ed.items.get_key_value(name).map(EnumItem::known_name)
}
pub fn from_enum(&self,enum_item:EnumItem)->Option<EnumItem<'a>>{
match enum_item.name{
Some(s)=>{
let got=self.from_name(s)?;
(got.value==enum_item.value).then_some(got)
},
None=>self.from_value(enum_item.value)
}
} }
} }
pub enum CoerceEnum<'a>{ impl<'a> EnumItem<'a>{
Integer(i32), const fn new(ed:&'a rbx_reflection::EnumDescriptor)->Self{
String(mlua::String), Self{ed}
Enum(EnumItem<'a>),
}
impl CoerceEnum<'_>{
pub fn coerce_to<'a>(self,enum_items:EnumItems<'a>)->mlua::Result<EnumItem<'a>>{
match self{
CoerceEnum::Integer(int)=>enum_items.from_value(int as u32),
CoerceEnum::String(s)=>enum_items.from_name(&*s.to_str()?),
CoerceEnum::Enum(enum_item)=>enum_items.from_enum(enum_item),
}.ok_or_else(||mlua::Error::runtime(format!("Bad {} EnumItem",enum_items.ed.name)))
}
}
impl mlua::FromLua for CoerceEnum<'_>{
fn from_lua(value:mlua::Value,_lua:&mlua::Lua)->Result<Self,mlua::Error>{
match value{
mlua::Value::Integer(int)=>Ok(CoerceEnum::Integer(int)),
mlua::Value::String(s)=>Ok(CoerceEnum::String(s)),
mlua::Value::UserData(ud)=>Ok(CoerceEnum::Enum(*ud.borrow()?)),
other=>Err(mlua::Error::runtime(format!("Expected {} got {:?}",stringify!(Enum),other))),
}
} }
} }
pub fn set_globals(_lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{ pub fn set_globals(_lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
globals.set("Enum",Enums) globals.set("Enum",EnumItems)
} }
impl mlua::UserData for EnumItems<'static>{
fn add_fields<F:mlua::UserDataFields<Self>>(_fields:&mut F){
}
fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){
methods.add_method("FromName",|_,this:&EnumItems,name:mlua::String|Ok(this.from_name(&*name.to_str()?)));
methods.add_method("FromValue",|_,this:&EnumItems,value:u32|Ok(this.from_value(value)));
methods.add_method("GetEnumItems",|_,this:&EnumItems,()|->mlua::Result<Vec<EnumItem>>{
Ok(this.ed.items.iter().map(EnumItem::known_name).collect())
});
methods.add_meta_function(mlua::MetaMethod::Index,|_,(this,val):(EnumItems,mlua::String)|{
let index=&*val.to_str()?;
Ok(this.ed.items.get_key_value(index).map(EnumItem::known_name))
});
}
}
type_from_lua_userdata_lua_lifetime!(EnumItems);
impl mlua::UserData for Enums{
fn add_fields<F:mlua::UserDataFields<Self>>(_fields:&mut F){
}
fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){
methods.add_meta_function(mlua::MetaMethod::Index,|_,(enums,val):(Self,mlua::String)|{
Ok(enums.get(&*val.to_str()?))
});
}
}
type_from_lua_userdata!(Enums);
impl mlua::UserData for EnumItem<'_>{ impl mlua::UserData for EnumItem<'_>{
fn add_fields<F:mlua::UserDataFields<Self>>(fields:&mut F){ fn add_fields<F:mlua::UserDataFields<Self>>(_fields:&mut F){
fields.add_field_method_get("Name",|_,this|Ok(this.name));
fields.add_field_method_get("Value",|_,this|Ok(this.value));
} }
fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){ fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){
methods.add_meta_function(mlua::MetaMethod::Eq,|_,(lhs,rhs):(EnumItem<'_>,EnumItem<'_>)|{ methods.add_meta_function(mlua::MetaMethod::Index,|lua,(this,val):(EnumItem<'_>,mlua::String)|{
Ok(lhs==rhs) match this.ed.items.get(&*val.to_str()?){
Some(&id)=>Enum(id).into_lua(lua),
None=>mlua::Value::Nil.into_lua(lua),
}
}); });
} }
} }
type_from_lua_userdata_lua_lifetime!(EnumItem); type_from_lua_userdata_lua_lifetime!(EnumItem);
impl mlua::UserData for EnumItems{
fn add_fields<F:mlua::UserDataFields<Self>>(_fields:&mut F){
}
fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){
methods.add_meta_function(mlua::MetaMethod::Index,|lua,(_,val):(Self,mlua::String)|{
let db=rbx_reflection_database::get();
match db.enums.get(&*val.to_str()?){
Some(ed)=>EnumItem::new(ed).into_lua(lua),
None=>mlua::Value::Nil.into_lua(lua),
}
});
}
}
type_from_lua_userdata!(EnumItems);
impl mlua::UserData for Enum{
fn add_fields<F:mlua::UserDataFields<Self>>(_fields:&mut F){
}
fn add_methods<M:mlua::UserDataMethods<Self>>(_methods:&mut M){
}
}
type_from_lua_userdata!(Enum);

@ -2,47 +2,48 @@ use std::collections::{hash_map::Entry,HashMap};
use mlua::{FromLua,FromLuaMulti,IntoLua,IntoLuaMulti}; use mlua::{FromLua,FromLuaMulti,IntoLua,IntoLuaMulti};
use rbx_types::Ref; use rbx_types::Ref;
use rbx_dom_weak::{Ustr,InstanceBuilder,WeakDom}; use rbx_dom_weak::{InstanceBuilder,WeakDom};
use crate::util::static_ustr;
use crate::runner::vector3::Vector3; use crate::runner::vector3::Vector3;
use crate::runner::number::Number;
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{ pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
//class functions store //class functions store
lua.set_app_data(ClassMethodsStore::default()); lua.set_app_data(ClassMethodsStore::default());
lua.set_app_data(InstanceValueStore::default());
let table=lua.create_table()?; let instance_table=lua.create_table()?;
//Instance.new //Instance.new
table.raw_set("new", instance_table.raw_set("new",
lua.create_function(|lua,(class_name,parent):(mlua::String,Option<Instance>)|{ lua.create_function(|lua,(class_name,parent):(mlua::String,Option<Instance>)|{
let class_name_str=&*class_name.to_str()?; let class_name_str=&*class_name.to_str()?;
let parent_ref=parent.map_or(Ref::none(),|instance|instance.referent); let parent=parent.ok_or_else(||mlua::Error::runtime("Nil Parent not yet supported"))?;
dom_mut(lua,|dom|{ dom_mut(lua,|dom|{
Ok(Instance::new_unchecked(dom.insert(parent_ref,InstanceBuilder::new(class_name_str)))) //TODO: Nil instances
Ok(Instance::new(dom.insert(parent.referent,InstanceBuilder::new(class_name_str))))
}) })
})? })?
)?; )?;
globals.set("Instance",table)?; globals.set("Instance",instance_table)?;
Ok(()) Ok(())
} }
// LMAO look at this function! // LMAO look at this function!
pub fn dom_mut<T>(lua:&mlua::Lua,mut f:impl FnMut(&mut WeakDom)->mlua::Result<T>)->mlua::Result<T>{ pub fn dom_mut<T>(lua:&mlua::Lua,mut f:impl FnMut(&mut WeakDom)->mlua::Result<T>)->mlua::Result<T>{
let mut dom=lua.app_data_mut::<crate::context::LuaAppData>().ok_or_else(||mlua::Error::runtime("DataModel missing"))?; let mut dom=lua.app_data_mut::<&'static mut WeakDom>().ok_or_else(||mlua::Error::runtime("DataModel missing"))?;
f(*dom) f(*dom)
} }
pub fn class_is_a(class:&str,superclass:&str)->bool{ fn coerce_float32(value:&mlua::Value)->Option<f32>{
let db=rbx_reflection_database::get(); match value{
let (Some(class),Some(superclass))=(db.classes.get(class),db.classes.get(superclass))else{ &mlua::Value::Integer(i)=>Some(i as f32),
return false; &mlua::Value::Number(f)=>Some(f as f32),
}; _=>None,
db.has_superclass(class,superclass) }
} }
fn get_full_name(dom:&rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance)->String{ fn get_full_name(dom:&rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance)->String{
let mut full_name=instance.name.clone(); let mut full_name=instance.name.clone();
let mut pref=instance.parent(); let mut pref=instance.parent();
@ -57,7 +58,7 @@ fn get_full_name(dom:&rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance)->S
pub fn get_name_source(lua:&mlua::Lua,script:Instance)->Result<(String,String),mlua::Error>{ pub fn get_name_source(lua:&mlua::Lua,script:Instance)->Result<(String,String),mlua::Error>{
dom_mut(lua,|dom|{ dom_mut(lua,|dom|{
let instance=script.get(dom)?; let instance=script.get(dom)?;
let source=match instance.properties.get(&static_ustr("Source")){ let source=match instance.properties.get("Source"){
Some(rbx_dom_weak::types::Variant::String(s))=>s.clone(), Some(rbx_dom_weak::types::Variant::String(s))=>s.clone(),
_=>Err(mlua::Error::external("Missing script.Source"))?, _=>Err(mlua::Error::external("Missing script.Source"))?,
}; };
@ -79,32 +80,14 @@ pub fn find_first_descendant_of_class<'a>(dom:&'a rbx_dom_weak::WeakDom,instance
dom.descendants_of(instance.referent()).find(|&inst|inst.class==class) dom.descendants_of(instance.referent()).find(|&inst|inst.class==class)
} }
pub fn find_first_child_which_is_a<'a>(dom:&'a rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance,superclass:&str)->Option<&'a rbx_dom_weak::Instance>{
let db=rbx_reflection_database::get();
let superclass_descriptor=db.classes.get(superclass)?;
instance.children().iter().filter_map(|&r|dom.get_by_ref(r)).find(|inst|{
db.classes.get(inst.class.as_str()).is_some_and(|descriptor|db.has_superclass(descriptor,superclass_descriptor))
})
}
pub fn find_first_descendant_which_is_a<'a>(dom:&'a rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance,superclass:&str)->Option<&'a rbx_dom_weak::Instance>{
let db=rbx_reflection_database::get();
let superclass_descriptor=db.classes.get(superclass)?;
dom.descendants_of(instance.referent()).find(|inst|{
db.classes.get(inst.class.as_str()).is_some_and(|descriptor|db.has_superclass(descriptor,superclass_descriptor))
})
}
#[derive(Clone,Copy)] #[derive(Clone,Copy)]
pub struct Instance{ pub struct Instance{
referent:Ref, referent:Ref,
} }
impl Instance{ impl Instance{
pub const fn new_unchecked(referent:Ref)->Self{ pub const fn new(referent:Ref)->Self{
Self{referent} Self{referent}
} }
pub fn new(referent:Ref)->Option<Self>{
referent.is_some().then_some(Self{referent})
}
pub fn get<'a>(&self,dom:&'a WeakDom)->mlua::Result<&'a rbx_dom_weak::Instance>{ pub fn get<'a>(&self,dom:&'a WeakDom)->mlua::Result<&'a rbx_dom_weak::Instance>{
dom.get_by_ref(self.referent).ok_or_else(||mlua::Error::runtime("Instance missing")) dom.get_by_ref(self.referent).ok_or_else(||mlua::Error::runtime("Instance missing"))
} }
@ -114,25 +97,40 @@ impl Instance{
} }
type_from_lua_userdata!(Instance); type_from_lua_userdata!(Instance);
//TODO: update rbx_reflection and use dom.superclasses_iter
pub struct SuperClassIter<'a> {
database: &'a rbx_reflection::ReflectionDatabase<'a>,
descriptor: Option<&'a rbx_reflection::ClassDescriptor<'a>>,
}
impl<'a> SuperClassIter<'a> {
fn next_descriptor(&self) -> Option<&'a rbx_reflection::ClassDescriptor<'a>> {
let superclass = self.descriptor?.superclass.as_ref()?;
self.database.classes.get(superclass)
}
}
impl<'a> Iterator for SuperClassIter<'a> {
type Item = &'a rbx_reflection::ClassDescriptor<'a>;
fn next(&mut self) -> Option<Self::Item> {
let next_descriptor = self.next_descriptor();
std::mem::replace(&mut self.descriptor, next_descriptor)
}
}
impl mlua::UserData for Instance{ impl mlua::UserData for Instance{
fn add_fields<F:mlua::UserDataFields<Self>>(fields:&mut F){ fn add_fields<F:mlua::UserDataFields<Self>>(fields:&mut F){
fn get_parent(lua:&mlua::Lua,this:&Instance)->mlua::Result<Option<Instance>>{ fields.add_field_method_get("Parent",|lua,this|{
dom_mut(lua,|dom|{ dom_mut(lua,|dom|{
let instance=this.get(dom)?; let instance=this.get(dom)?;
Ok(Instance::new(instance.parent())) Ok(Instance::new(instance.parent()))
}) })
} });
fields.add_field_method_get("parent",get_parent); fields.add_field_method_set("Parent",|lua,this,val:Option<Instance>|{
fields.add_field_method_get("Parent",get_parent); let parent=val.ok_or_else(||mlua::Error::runtime("Nil Parent not yet supported"))?;
fn set_parent(lua:&mlua::Lua,this:&mut Instance,new_parent:Option<Instance>)->mlua::Result<()>{
let parent_ref=new_parent.map_or(Ref::none(),|instance|instance.referent);
dom_mut(lua,|dom|{ dom_mut(lua,|dom|{
dom.transfer_within(this.referent,parent_ref); dom.transfer_within(this.referent,parent.referent);
Ok(()) Ok(())
}) })
} });
fields.add_field_method_set("parent",set_parent);
fields.add_field_method_set("Parent",set_parent);
fields.add_field_method_get("Name",|lua,this|{ fields.add_field_method_get("Name",|lua,this|{
dom_mut(lua,|dom|{ dom_mut(lua,|dom|{
let instance=this.get(dom)?; let instance=this.get(dom)?;
@ -150,38 +148,22 @@ impl mlua::UserData for Instance{
fields.add_field_method_get("ClassName",|lua,this|{ fields.add_field_method_get("ClassName",|lua,this|{
dom_mut(lua,|dom|{ dom_mut(lua,|dom|{
let instance=this.get(dom)?; let instance=this.get(dom)?;
Ok(instance.class.to_owned()) Ok(instance.class.clone())
}) })
}); });
} }
fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){ fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){
fn clone(lua:&mlua::Lua,this:&Instance,_:())->mlua::Result<Instance>{ methods.add_method("GetChildren",|lua,this,_:()|
dom_mut(lua,|dom|{
let instance_ref=dom.clone_within(this.referent);
Ok(Instance::new_unchecked(instance_ref))
})
}
methods.add_method("clone",clone);
methods.add_method("Clone",clone);
fn get_children(lua:&mlua::Lua,this:&Instance,_:())->mlua::Result<Vec<Instance>>{
dom_mut(lua,|dom|{ dom_mut(lua,|dom|{
let instance=this.get(dom)?; let instance=this.get(dom)?;
let children:Vec<_>=instance let children:Vec<_>=instance
.children() .children()
.iter() .iter()
.copied() .copied()
.map(Instance::new_unchecked) .map(Instance::new)
.collect(); .collect();
Ok(children) Ok(children)
}) })
}
methods.add_method("children",get_children);
methods.add_method("GetChildren",get_children);
methods.add_method("GetFullName",|lua,this,()|
dom_mut(lua,|dom|{
let instance=this.get(dom)?;
Ok(get_full_name(dom,instance))
})
); );
fn ffc(lua:&mlua::Lua,this:&Instance,(name,search_descendants):(mlua::String,Option<bool>))->mlua::Result<Option<Instance>>{ fn ffc(lua:&mlua::Lua,this:&Instance,(name,search_descendants):(mlua::String,Option<bool>))->mlua::Result<Option<Instance>>{
let name_str=&*name.to_str()?; let name_str=&*name.to_str()?;
@ -193,7 +175,7 @@ impl mlua::UserData for Instance{
false=>find_first_child(dom,instance,name_str), false=>find_first_child(dom,instance,name_str),
} }
.map(|instance| .map(|instance|
Instance::new_unchecked(instance.referent()) Instance::new(instance.referent())
) )
) )
}) })
@ -203,29 +185,13 @@ impl mlua::UserData for Instance{
methods.add_method("FindFirstChildOfClass",|lua,this,(class,search_descendants):(mlua::String,Option<bool>)|{ methods.add_method("FindFirstChildOfClass",|lua,this,(class,search_descendants):(mlua::String,Option<bool>)|{
let class_str=&*class.to_str()?; let class_str=&*class.to_str()?;
dom_mut(lua,|dom|{ dom_mut(lua,|dom|{
let inst=this.get(dom)?;
Ok( Ok(
match search_descendants.unwrap_or(false){ match search_descendants.unwrap_or(false){
true=>find_first_descendant_of_class(dom,inst,class_str), true=>find_first_descendant_of_class(dom,this.get(dom)?,class_str),
false=>find_first_child_of_class(dom,inst,class_str), false=>find_first_child_of_class(dom,this.get(dom)?,class_str),
} }
.map(|instance| .map(|instance|
Instance::new_unchecked(instance.referent()) Instance::new(instance.referent())
)
)
})
});
methods.add_method("FindFirstChildWhichIsA",|lua,this,(class,search_descendants):(mlua::String,Option<bool>)|{
let class_str=&*class.to_str()?;
dom_mut(lua,|dom|{
let inst=this.get(dom)?;
Ok(
match search_descendants.unwrap_or(false){
true=>find_first_descendant_which_is_a(dom,inst,class_str),
false=>find_first_child_which_is_a(dom,inst,class_str),
}
.map(|instance|
Instance::new_unchecked(instance.referent())
) )
) )
}) })
@ -235,42 +201,24 @@ impl mlua::UserData for Instance{
let children:Vec<_>=dom let children:Vec<_>=dom
.descendants_of(this.referent) .descendants_of(this.referent)
.map(|instance| .map(|instance|
Instance::new_unchecked(instance.referent()) Instance::new(instance.referent())
) )
.collect(); .collect();
Ok(children) Ok(children)
}) })
); );
methods.add_method("IsAncestorOf",|lua,this,descendant:Instance| methods.add_method("IsA",|lua,this,classname:mlua::String|
dom_mut(lua,|dom|{
let instance=descendant.get(dom)?;
Ok(std::iter::successors(Some(instance),|inst|dom.get_by_ref(inst.parent())).any(|inst|inst.referent()==this.referent))
})
);
methods.add_method("IsDescendantOf",|lua,this,ancestor:Instance|
dom_mut(lua,|dom|{ dom_mut(lua,|dom|{
let instance=this.get(dom)?; let instance=this.get(dom)?;
Ok(std::iter::successors(Some(instance),|inst|dom.get_by_ref(inst.parent())).any(|inst|inst.referent()==ancestor.referent)) Ok(crate::context::class_is_a(instance.class.as_str(),&*classname.to_str()?))
}) })
); );
fn is_a(lua:&mlua::Lua,this:&Instance,classname:mlua::String)->mlua::Result<bool>{ methods.add_method("Destroy",|lua,this,()|
dom_mut(lua,|dom|{ dom_mut(lua,|dom|{
let instance=this.get(dom)?; dom.destroy(this.referent);
Ok(class_is_a(instance.class.as_str(),&*classname.to_str()?))
})
}
methods.add_method("isA",is_a);
methods.add_method("IsA",is_a);
fn destroy(lua:&mlua::Lua,this:&Instance,_:())->mlua::Result<()>{
dom_mut(lua,|dom|{
dom.transfer_within(this.referent,Ref::none());
Ok(()) Ok(())
}) })
} );
methods.add_method("remove",destroy);
methods.add_method("Remove",destroy);
methods.add_method("destroy",destroy);
methods.add_method("Destroy",destroy);
methods.add_meta_function(mlua::MetaMethod::ToString,|lua,this:Instance|{ methods.add_meta_function(mlua::MetaMethod::ToString,|lua,this:Instance|{
dom_mut(lua,|dom|{ dom_mut(lua,|dom|{
let instance=this.get(dom)?; let instance=this.get(dom)?;
@ -284,38 +232,39 @@ impl mlua::UserData for Instance{
//println!("__index t={} i={index:?}",instance.name); //println!("__index t={} i={index:?}",instance.name);
let db=rbx_reflection_database::get(); let db=rbx_reflection_database::get();
let class=db.classes.get(instance.class.as_str()).ok_or_else(||mlua::Error::runtime("Class missing"))?; let class=db.classes.get(instance.class.as_str()).ok_or_else(||mlua::Error::runtime("Class missing"))?;
// Find existing property //Find existing property
// Interestingly, ustr can know ahead of time if match instance.properties.get(index_str)
// a property does not exist in any runtime instance .cloned()
match Ustr::from_existing(index_str)
.and_then(|index_ustr|
instance.properties.get(&index_ustr).cloned()
)
//Find default value //Find default value
.or_else(||db.find_default_property(class,index_str).cloned()) .or_else(||db.find_default_property(class,index_str).cloned())
//Find virtual property //Find virtual property
.or_else(||db.superclasses_iter(class).find_map(|class| .or_else(||{
find_virtual_property(&instance.properties,class,index_str) SuperClassIter{
)) database:db,
descriptor:Some(class),
}
.find_map(|class|
find_virtual_property(&instance.properties,class,index_str)
)
})
{ {
Some(rbx_types::Variant::Bool(val))=>return val.into_lua(lua),
Some(rbx_types::Variant::Int32(val))=>return val.into_lua(lua), Some(rbx_types::Variant::Int32(val))=>return val.into_lua(lua),
Some(rbx_types::Variant::Int64(val))=>return val.into_lua(lua), Some(rbx_types::Variant::Int64(val))=>return val.into_lua(lua),
Some(rbx_types::Variant::Float32(val))=>return val.into_lua(lua), Some(rbx_types::Variant::Float32(val))=>return val.into_lua(lua),
Some(rbx_types::Variant::Float64(val))=>return val.into_lua(lua), Some(rbx_types::Variant::Float64(val))=>return val.into_lua(lua),
Some(rbx_types::Variant::String(val))=>return val.into_lua(lua), Some(rbx_types::Variant::Ref(val))=>return Instance::new(val).into_lua(lua),
Some(rbx_types::Variant::Ref(val))=>return Instance::new_unchecked(val).into_lua(lua), Some(rbx_types::Variant::CFrame(cf))=>return Into::<crate::runner::cframe::CFrame>::into(cf).into_lua(lua),
Some(rbx_types::Variant::Enum(e))=>return crate::runner::r#enum::EnumItem::from(e).into_lua(lua), Some(rbx_types::Variant::Vector3(v))=>return Into::<crate::runner::vector3::Vector3>::into(v).into_lua(lua),
Some(rbx_types::Variant::Color3(c))=>return crate::runner::color3::Color3::from(c).into_lua(lua),
Some(rbx_types::Variant::CFrame(cf))=>return crate::runner::cframe::CFrame::from(cf).into_lua(lua),
Some(rbx_types::Variant::Vector2(v))=>return crate::runner::vector2::Vector2::from(v).into_lua(lua),
Some(rbx_types::Variant::Vector3(v))=>return crate::runner::vector3::Vector3::from(v).into_lua(lua),
None=>(), None=>(),
other=>return Err(mlua::Error::runtime(format!("Instance.__index Unsupported property type instance={} index={index_str} value={other:?}",instance.name))), other=>return Err(mlua::Error::runtime(format!("Instance.__index Unsupported property type instance={} index={index_str} value={other:?}",instance.name))),
} }
//find a function with a matching name //find a function with a matching name
if let Some(function)=class_methods_store_mut(lua,|cf|{ if let Some(function)=class_methods_store_mut(lua,|cf|{
db.superclasses_iter(class).find_map(|class|{ let mut iter=SuperClassIter{
database:db,
descriptor:Some(class),
};
iter.find_map(|class|{
let mut class_methods=cf.get_or_create_class_methods(&class.name)?; let mut class_methods=cf.get_or_create_class_methods(&class.name)?;
class_methods.get_or_create_function(lua,index_str) class_methods.get_or_create_function(lua,index_str)
.transpose() .transpose()
@ -325,114 +274,80 @@ impl mlua::UserData for Instance{
} }
//find or create an associated userdata object //find or create an associated userdata object
let instance=this.get_mut(dom)?; if let Some(value)=instance_value_store_mut(lua,|ivs|{
if let Some(value)=get_or_create_userdata(instance,lua,index_str)?{ //TODO: walk class tree somehow
match ivs.get_or_create_instance_values(&instance){
Some(mut instance_values)=>instance_values.get_or_create_value(lua,index_str),
None=>Ok(None)
}
})?{
return value.into_lua(lua); return value.into_lua(lua);
} }
// drop mutable borrow
//find a child with a matching name //find a child with a matching name
let instance=this.get(dom)?;
find_first_child(dom,instance,index_str) find_first_child(dom,instance,index_str)
.map(|instance|Instance::new_unchecked(instance.referent())) .map(|instance|Instance::new(instance.referent()))
.into_lua(lua) .into_lua(lua)
}) })
}); });
methods.add_meta_function(mlua::MetaMethod::NewIndex,|lua,(this,index,value):(Instance,mlua::String,mlua::Value)|{ methods.add_meta_function(mlua::MetaMethod::NewIndex,|lua,(this,index,value):(Instance,mlua::String,mlua::Value)|{
let index_str=&*index.to_str()?;
dom_mut(lua,|dom|{ dom_mut(lua,|dom|{
let instance=this.get_mut(dom)?; let instance=this.get_mut(dom)?;
//println!("__newindex t={} i={index:?} v={value:?}",instance.name);
let index_str=&*index.to_str()?;
let db=rbx_reflection_database::get(); let db=rbx_reflection_database::get();
let class=db.classes.get(instance.class.as_str()).ok_or_else(||mlua::Error::runtime("Class missing"))?; let class=db.classes.get(instance.class.as_str()).ok_or_else(||mlua::Error::runtime("Class missing"))?;
let property=db.superclasses_iter(class).find_map(|cls| let mut iter=SuperClassIter{
cls.properties.get(index_str) database:db,
).ok_or_else(|| descriptor:Some(class),
mlua::Error::runtime(format!("Property '{index_str}' missing on class '{}'",class.name)) };
)?; let property=iter.find_map(|cls|cls.properties.get(index_str)).ok_or_else(||mlua::Error::runtime(format!("Property '{index_str}' missing on class '{}'",class.name)))?;
let value=match &property.data_type{ match &property.data_type{
rbx_reflection::DataType::Value(rbx_types::VariantType::Vector3)=>{
let typed_value:Vector3=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected Userdata"))?.borrow()?;
instance.properties.insert(index_str.to_owned(),rbx_types::Variant::Vector3(typed_value.into()));
},
rbx_reflection::DataType::Value(rbx_types::VariantType::Float32)=>{ rbx_reflection::DataType::Value(rbx_types::VariantType::Float32)=>{
let typed_value=Number::from_lua(value.clone(),lua)?.to_f32(); let typed_value:f32=coerce_float32(&value).ok_or_else(||mlua::Error::runtime("Expected f32"))?;
rbx_types::Variant::Float32(typed_value) instance.properties.insert(index_str.to_owned(),rbx_types::Variant::Float32(typed_value));
}, },
rbx_reflection::DataType::Enum(enum_name)=>{ rbx_reflection::DataType::Enum(enum_name)=>{
let typed_value=match &value{ let typed_value=match &value{
&mlua::Value::Integer(int)=>Ok(rbx_types::Enum::from_u32(int as u32)), &mlua::Value::Integer(int)=>Ok(rbx_types::Enum::from_u32(int as u32)),
&mlua::Value::Number(num)=>Ok(rbx_types::Enum::from_u32(num as u32)), &mlua::Value::Number(num)=>Ok(rbx_types::Enum::from_u32(num as u32)),
mlua::Value::String(s)=>{ mlua::Value::String(s)=>{
let e=db.enums.get(enum_name).ok_or_else(||mlua::Error::runtime("Database DataType Enum name does not exist"))?; let e=db.enums.get(enum_name).ok_or_else(||mlua::Error::runtime("Database DataType Enum name does not exist"))?;
Ok(rbx_types::Enum::from_u32(*e.items.get(&*s.to_str()?).ok_or_else(||mlua::Error::runtime("Invalid enum item"))?)) Ok(rbx_types::Enum::from_u32(*e.items.get(&*s.to_str()?).ok_or_else(||mlua::Error::runtime("Invalid enum item"))?))
}, },
mlua::Value::UserData(any_user_data)=>{ mlua::Value::UserData(any_user_data)=>{
let e:crate::runner::r#enum::EnumItem=*any_user_data.borrow()?; let e:crate::runner::r#enum::Enum=*any_user_data.borrow()?;
Ok(e.into()) Ok(e.into())
}, },
_=>Err(mlua::Error::runtime("Expected Enum")), _=>Err(mlua::Error::runtime("Expected Enum")),
}?; }?;
rbx_types::Variant::Enum(typed_value) instance.properties.insert(index_str.to_owned(),rbx_types::Variant::Enum(typed_value));
}, },
rbx_reflection::DataType::Value(rbx_types::VariantType::Color3)=>{ rbx_reflection::DataType::Value(rbx_types::VariantType::Color3)=>{
let typed_value:crate::runner::color3::Color3=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected Color3"))?.borrow()?; let typed_value:crate::runner::color3::Color3=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected Color3"))?.borrow()?;
rbx_types::Variant::Color3(typed_value.into()) instance.properties.insert(index_str.to_owned(),rbx_types::Variant::Color3(typed_value.into()));
}, },
rbx_reflection::DataType::Value(rbx_types::VariantType::Bool)=>{ rbx_reflection::DataType::Value(rbx_types::VariantType::Bool)=>{
let typed_value=value.as_boolean().ok_or_else(||mlua::Error::runtime("Expected boolean"))?; let typed_value=value.as_boolean().ok_or_else(||mlua::Error::runtime("Expected boolean"))?;
rbx_types::Variant::Bool(typed_value) instance.properties.insert(index_str.to_owned(),rbx_types::Variant::Bool(typed_value));
},
rbx_reflection::DataType::Value(rbx_types::VariantType::Int32)=>{
let typed_value=value.as_i32().ok_or_else(||mlua::Error::runtime("Expected Int32"))?;
rbx_types::Variant::Int32(typed_value)
}, },
rbx_reflection::DataType::Value(rbx_types::VariantType::String)=>{ rbx_reflection::DataType::Value(rbx_types::VariantType::String)=>{
let typed_value=match &value{ let typed_value=value.as_str().ok_or_else(||mlua::Error::runtime("Expected boolean"))?;
mlua::Value::Integer(i)=>i.to_string(), instance.properties.insert(index_str.to_owned(),rbx_types::Variant::String(typed_value.to_owned()));
mlua::Value::Number(n)=>n.to_string(),
mlua::Value::String(s)=>s.to_str()?.to_owned(),
_=>return Err(mlua::Error::runtime("Expected string")),
};
rbx_types::Variant::String(typed_value)
},
rbx_reflection::DataType::Value(rbx_types::VariantType::UDim2)=>{
let typed_value:&crate::runner::udim2::UDim2=&*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected UDim2"))?.borrow()?;
rbx_types::Variant::UDim2(typed_value.clone().into())
},
rbx_reflection::DataType::Value(rbx_types::VariantType::NumberRange)=>{
let typed_value:&crate::runner::number_range::NumberRange=&*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected NumberRange"))?.borrow()?;
rbx_types::Variant::NumberRange(typed_value.clone().into())
}, },
rbx_reflection::DataType::Value(rbx_types::VariantType::NumberSequence)=>{ rbx_reflection::DataType::Value(rbx_types::VariantType::NumberSequence)=>{
let typed_value:&crate::runner::number_sequence::NumberSequence=&*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected NumberSequence"))?.borrow()?; let typed_value:crate::runner::number_sequence::NumberSequence=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected NumberSequence"))?.borrow()?;
rbx_types::Variant::NumberSequence(typed_value.clone().into()) instance.properties.insert(index_str.to_owned(),rbx_types::Variant::NumberSequence(typed_value.into()));
}, },
rbx_reflection::DataType::Value(rbx_types::VariantType::ColorSequence)=>{ rbx_reflection::DataType::Value(rbx_types::VariantType::ColorSequence)=>{
let typed_value:&crate::runner::color_sequence::ColorSequence=&*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected ColorSequence"))?.borrow()?; let typed_value:crate::runner::color_sequence::ColorSequence=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected ColorSequence"))?.borrow()?;
rbx_types::Variant::ColorSequence(typed_value.clone().into()) instance.properties.insert(index_str.to_owned(),rbx_types::Variant::ColorSequence(typed_value.into()));
},
rbx_reflection::DataType::Value(rbx_types::VariantType::Vector2)=>{
let typed_value:crate::runner::vector2::Vector2=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected Vector2"))?.borrow()?;
rbx_types::Variant::Vector2(typed_value.clone().into())
},
rbx_reflection::DataType::Value(rbx_types::VariantType::Vector3)=>{
let typed_value:crate::runner::vector3::Vector3=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected Vector3"))?.borrow()?;
rbx_types::Variant::Vector3(typed_value.clone().into())
},
rbx_reflection::DataType::Value(rbx_types::VariantType::CFrame)=>{
let typed_value:crate::runner::cframe::CFrame=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected CFrame"))?.borrow()?;
rbx_types::Variant::CFrame(typed_value.clone().into())
},
rbx_reflection::DataType::Value(rbx_types::VariantType::ContentId)=>{
let typed_value=value.as_str().ok_or_else(||mlua::Error::runtime("Expected string"))?.to_owned();
rbx_types::Variant::ContentId(typed_value.into())
},
rbx_reflection::DataType::Value(rbx_types::VariantType::Ref)=>{
// why clone?
let typed_value=Option::<Instance>::from_lua(value.clone(),lua)?;
rbx_types::Variant::Ref(typed_value.map_or(Ref::none(),|instance|instance.referent))
}, },
other=>return Err(mlua::Error::runtime(format!("Unimplemented property type: {other:?}"))), other=>return Err(mlua::Error::runtime(format!("Unimplemented property type: {other:?}"))),
}; }
// the index is known to be a real property at this point
// allow creating a permanent ustr (memory leak)
let index_ustr=rbx_dom_weak::ustr(index_str);
instance.properties.insert(index_ustr,value);
Ok(()) Ok(())
}) })
}); });
@ -457,53 +372,28 @@ type CFD=phf::Map<&'static str,// Class name
ClassFunctionPointer ClassFunctionPointer
> >
>; >;
const GET_SERVICE:ClassFunctionPointer=cf!(|lua,_this,service:mlua::String|{
dom_mut(lua,|dom|{
//dom.root_ref()==this.referent ?
let service=&*service.to_str()?;
match service{
"Lighting"|"RunService"|"Players"|"Workspace"|"MaterialService"|"TweenService"=>{
let referent=find_first_child_of_class(dom,dom.root(),service)
.map(|instance|instance.referent())
.unwrap_or_else(||
dom.insert(dom.root_ref(),InstanceBuilder::new(service))
);
Ok(Instance::new_unchecked(referent))
},
other=>Err(mlua::Error::runtime(format!("Service '{other}' not supported"))),
}
})
});
const GET_PLAYERS:ClassFunctionPointer=cf!(|_lua,_this,()|->mlua::Result<_>{
Ok(Vec::<Instance>::new())
});
const NO_OP:ClassFunctionPointer=cf!(|_lua,_this,_:mlua::MultiValue|->mlua::Result<_>{Ok(())});
static CLASS_FUNCTION_DATABASE:CFD=phf::phf_map!{ static CLASS_FUNCTION_DATABASE:CFD=phf::phf_map!{
"DataModel"=>phf::phf_map!{ "DataModel"=>phf::phf_map!{
"service"=>GET_SERVICE, "GetService"=>cf!(|lua,_this,service:mlua::String|{
"GetService"=>GET_SERVICE, dom_mut(lua,|dom|{
//dom.root_ref()==this.referent ?
let service=&*service.to_str()?;
match service{
"Lighting"|"RunService"=>{
let referent=find_first_child_of_class(dom,dom.root(),service)
.map(|instance|instance.referent())
.unwrap_or_else(||
dom.insert(dom.root_ref(),InstanceBuilder::new(service))
);
Ok(Instance::new(referent))
},
other=>Err::<Instance,_>(mlua::Error::runtime(format!("Service '{other}' not supported"))),
}
})
}),
}, },
"Terrain"=>phf::phf_map!{ "Terrain"=>phf::phf_map!{
"FillBall"=>cf!(|_lua,_,_:(Vector3,Number,crate::runner::r#enum::CoerceEnum)|mlua::Result::Ok(())), "FillBlock"=>cf!(|_lua,_,_:(crate::runner::cframe::CFrame,Vector3,crate::runner::r#enum::Enum)|mlua::Result::Ok(()))
"FillBlock"=>cf!(|_lua,_,_:(crate::runner::cframe::CFrame,Vector3,crate::runner::r#enum::CoerceEnum)|mlua::Result::Ok(())),
"FillCylinder"=>cf!(|_lua,_,_:(crate::runner::cframe::CFrame,Number,Number,crate::runner::r#enum::CoerceEnum)|mlua::Result::Ok(())),
"SetMaterialColor"=>cf!(|_lua,_,_:(crate::runner::r#enum::CoerceEnum,crate::runner::color3::Color3)|mlua::Result::Ok(())),
},
"Players"=>phf::phf_map!{
"players"=>GET_PLAYERS,
"GetPlayers"=>GET_PLAYERS,
},
"Sound"=>phf::phf_map!{
"Play"=>NO_OP,
},
"TweenService"=>phf::phf_map!{
"Create"=>cf!(|_lua,_,(instance,tween_info,goal):(Instance,crate::runner::tween_info::TweenInfo,mlua::Table)|->mlua::Result<_>{
Ok(crate::runner::tween::Tween::create(
instance,
tween_info,
goal,
))
}),
}, },
}; };
@ -590,70 +480,78 @@ static VIRTUAL_PROPERTY_DATABASE:VPD=phf::phf_map!{
}; };
fn find_virtual_property( fn find_virtual_property(
properties:&rbx_dom_weak::UstrMap<rbx_types::Variant>, properties:&HashMap<String,rbx_types::Variant>,
class:&rbx_reflection::ClassDescriptor, class:&rbx_reflection::ClassDescriptor,
index:&str, index:&str
)->Option<rbx_types::Variant>{ )->Option<rbx_types::Variant>{
//Find virtual property //Find virtual property
let class_virtual_properties=VIRTUAL_PROPERTY_DATABASE.get(&class.name)?; let class_virtual_properties=VIRTUAL_PROPERTY_DATABASE.get(&class.name)?;
let virtual_property=class_virtual_properties.get(index)?; let virtual_property=class_virtual_properties.get(index)?;
//Get source property //Get source property
let variant=properties.get(&static_ustr(virtual_property.property))?; let variant=properties.get(virtual_property.property)?;
//Transform Source property with provided function //Transform Source property with provided function
(virtual_property.pointer)(variant) (virtual_property.pointer)(variant)
} }
// lazy-loaded per-instance userdata values // lazy-loaded per-instance userdata values
// This whole thing is a bad idea and a garbage collection nightmare.
// TODO: recreate rbx_dom_weak with my own instance type that owns this data.
type CreateUserData=fn(&mlua::Lua)->mlua::Result<mlua::AnyUserData>; type CreateUserData=fn(&mlua::Lua)->mlua::Result<mlua::AnyUserData>;
type LUD=phf::Map<&'static str,// Class name type LUD=phf::Map<&'static str,// Class name
phf::Map<&'static str,// Value name phf::Map<&'static str,// Value name
CreateUserData CreateUserData
> >
>; >;
fn create_script_signal(lua:&mlua::Lua)->mlua::Result<mlua::AnyUserData>{
lua.create_any_userdata(crate::runner::script_signal::ScriptSignal::new())
}
static LAZY_USER_DATA:LUD=phf::phf_map!{ static LAZY_USER_DATA:LUD=phf::phf_map!{
"RunService"=>phf::phf_map!{ "RunService"=>phf::phf_map!{
"Stepped"=>create_script_signal, "RenderStepped"=>|lua|{
"Heartbeat"=>create_script_signal, lua.create_any_userdata(crate::runner::script_signal::ScriptSignal::new())
"RenderStepped"=>create_script_signal, },
},
"Players"=>phf::phf_map!{
"PlayerAdded"=>create_script_signal,
},
"BasePart"=>phf::phf_map!{
"Touched"=>create_script_signal,
"TouchEnded"=>create_script_signal,
},
"Instance"=>phf::phf_map!{
"ChildAdded"=>create_script_signal,
"ChildRemoved"=>create_script_signal,
"DescendantAdded"=>create_script_signal,
"DescendantRemoved"=>create_script_signal,
},
"ClickDetector"=>phf::phf_map!{
"MouseClick"=>create_script_signal,
}, },
}; };
fn get_or_create_userdata(instance:&mut rbx_dom_weak::Instance,lua:&mlua::Lua,index:&str)->mlua::Result<Option<mlua::AnyUserData>>{ #[derive(Default)]
use std::collections::hash_map::Entry; pub struct InstanceValueStore{
let db=rbx_reflection_database::get(); values:HashMap<Ref,
let Some(class)=db.classes.get(instance.class.as_str())else{ HashMap<&'static str,
return Ok(None) mlua::AnyUserData
}; >
if let Some((&static_str,create_userdata))=db.superclasses_iter(class).find_map(|superclass| >,
// find pair (class,index) }
LAZY_USER_DATA.get(&superclass.name) pub struct InstanceValues<'a>{
.and_then(|map|map.get_entry(index)) named_values:&'static phf::Map<&'static str,CreateUserData>,
){ values:&'a mut HashMap<&'static str,mlua::AnyUserData>,
let index_ustr=static_ustr(static_str); }
return Ok(Some(match instance.userdata.entry(index_ustr){ impl InstanceValueStore{
Entry::Occupied(entry)=>entry.get().clone(), pub fn get_or_create_instance_values(&mut self,instance:&rbx_dom_weak::Instance)->Option<InstanceValues>{
Entry::Vacant(entry)=>entry.insert(create_userdata(lua)?).clone(), LAZY_USER_DATA.get(instance.class.as_str())
})); .map(|named_values|
} InstanceValues{
Ok(None) named_values,
values:self.values.entry(instance.referent())
.or_insert_with(||HashMap::new()),
}
)
}
}
impl InstanceValues<'_>{
pub fn get_or_create_value(&mut self,lua:&mlua::Lua,index:&str)->mlua::Result<Option<mlua::AnyUserData>>{
Ok(match self.named_values.get_entry(index){
Some((&static_index_str,&function_pointer))=>Some(
match self.values.entry(static_index_str){
Entry::Occupied(entry)=>entry.get().clone(),
Entry::Vacant(entry)=>entry.insert(
function_pointer(lua)?
).clone(),
}
),
None=>None,
})
}
}
pub fn instance_value_store_mut<T>(lua:&mlua::Lua,mut f:impl FnMut(&mut InstanceValueStore)->mlua::Result<T>)->mlua::Result<T>{
let mut cf=lua.app_data_mut::<InstanceValueStore>().ok_or_else(||mlua::Error::runtime("InstanceValueStore missing"))?;
f(&mut *cf)
} }

@ -10,18 +10,6 @@ macro_rules! type_from_lua_userdata{
} }
}; };
} }
macro_rules! type_from_lua_userdata_clone{
($ty:ident)=>{
impl mlua::FromLua for $ty{
fn from_lua(value:mlua::Value,_lua:&mlua::Lua)->Result<Self,mlua::Error>{
match value{
mlua::Value::UserData(ud)=>Ok(ud.borrow::<Self>()?.clone()),
other=>Err(mlua::Error::runtime(format!("Expected {} got {:?}",stringify!($ty),other))),
}
}
}
};
}
macro_rules! type_from_lua_userdata_lua_lifetime{ macro_rules! type_from_lua_userdata_lua_lifetime{
($ty:ident)=>{ ($ty:ident)=>{
impl mlua::FromLua for $ty<'static>{ impl mlua::FromLua for $ty<'static>{

@ -3,19 +3,10 @@ mod macros;
mod runner; mod runner;
mod r#enum; mod r#enum;
mod task;
mod udim;
mod tween;
mod udim2;
mod color3; mod color3;
mod cframe; mod cframe;
mod number;
mod vector2;
mod vector3; mod vector3;
mod brickcolor;
mod tween_info;
pub mod instance; pub mod instance;
mod number_range;
mod script_signal; mod script_signal;
mod color_sequence; mod color_sequence;
mod number_sequence; mod number_sequence;

@ -1,43 +0,0 @@
// the goal of this module is to provide an intermediate type
// that is guaranteed to be some kind of number, and provide
// methods to coerce it into various more specific types.
#[derive(Clone,Copy)]
pub enum Number{
Integer(i32),
Number(f64),
}
macro_rules! impl_ty{
($ident:ident,$ty:ty)=>{
impl Number{
#[inline]
pub fn $ident(self)->$ty{
match self{
Self::Integer(int)=>int as $ty,
Self::Number(num)=>num as $ty,
}
}
}
impl From<Number> for $ty{
fn from(value:Number)->$ty{
value.$ident()
}
}
};
}
impl_ty!(to_u32,u32);
impl_ty!(to_i32,i32);
impl_ty!(to_f32,f32);
impl_ty!(to_u64,u64);
impl_ty!(to_i64,i64);
impl_ty!(to_f64,f64);
impl mlua::FromLua for Number{
fn from_lua(value:mlua::Value,_lua:&mlua::Lua)->Result<Self,mlua::Error>{
match value{
mlua::Value::Integer(int)=>Ok(Number::Integer(int)),
mlua::Value::Number(num)=>Ok(Number::Number(num)),
other=>Err(mlua::Error::runtime(format!("Expected {} got {:?}",stringify!(Number),other))),
}
}
}

@ -1,34 +0,0 @@
use super::number::Number;
#[derive(Clone)]
pub struct NumberRange(rbx_types::NumberRange);
impl NumberRange{
pub const fn new(min:f32,max:f32)->Self{
Self(rbx_types::NumberRange{min,max})
}
}
impl From<NumberRange> for rbx_types::NumberRange{
fn from(NumberRange(value):NumberRange)->rbx_types::NumberRange{
value
}
}
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
let table=lua.create_table()?;
table.raw_set("new",
lua.create_function(|_,(min,max):(Number,Option<Number>)|{
Ok(match max{
Some(max)=>NumberRange::new(min.into(),max.into()),
None=>NumberRange::new(min.into(),min.into()),
})
})?
)?;
globals.set("NumberRange",table)?;
Ok(())
}
impl mlua::UserData for NumberRange{}
type_from_lua_userdata_clone!(NumberRange);

@ -1,36 +1,31 @@
#[derive(Clone)] #[derive(Clone,Copy)]
pub struct NumberSequence(rbx_types::NumberSequence); pub struct NumberSequence{}
impl NumberSequence{ impl NumberSequence{
pub const fn new(keypoints:Vec<rbx_types::NumberSequenceKeypoint>)->Self{ pub const fn new()->Self{
Self(rbx_types::NumberSequence{keypoints}) Self{}
} }
} }
impl From<NumberSequence> for rbx_types::NumberSequence{ impl Into<rbx_types::NumberSequence> for NumberSequence{
fn from(NumberSequence(value):NumberSequence)->rbx_types::NumberSequence{ fn into(self)->rbx_types::NumberSequence{
value rbx_types::NumberSequence{
keypoints:Vec::new()
}
} }
} }
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{ pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
let table=lua.create_table()?; let number_sequence_table=lua.create_table()?;
table.raw_set("new", number_sequence_table.raw_set("new",
lua.create_function(|_,_:mlua::MultiValue| lua.create_function(|_,_:mlua::MultiValue|
Ok(NumberSequence::new(Vec::new())) Ok(NumberSequence::new())
)? )?
)?; )?;
globals.set("NumberSequence",table)?; globals.set("NumberSequence",number_sequence_table)?;
Ok(()) Ok(())
} }
impl mlua::UserData for NumberSequence{} impl mlua::UserData for NumberSequence{}
impl mlua::FromLua for NumberSequence{ type_from_lua_userdata!(NumberSequence);
fn from_lua(value:mlua::Value,_lua:&mlua::Lua)->Result<Self,mlua::Error>{
match value{
mlua::Value::UserData(ud)=>Ok(ud.borrow::<Self>()?.clone()),
other=>Err(mlua::Error::runtime(format!("Expected {} got {:?}",stringify!(NumberSequence),other))),
}
}
}

@ -1,5 +1,4 @@
use crate::context::Context; use crate::context::Context;
use crate::util::static_ustr;
#[cfg(feature="run-service")] #[cfg(feature="run-service")]
use crate::scheduler::scheduler_mut; use crate::scheduler::scheduler_mut;
@ -13,12 +12,14 @@ pub enum Error{
error:mlua::Error error:mlua::Error
}, },
RustLua(mlua::Error), RustLua(mlua::Error),
NoServices,
} }
impl std::fmt::Display for Error{ impl std::fmt::Display for Error{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
match self{ match self{
Self::Lua{source,error}=>write!(f,"lua error: source:\n{source}\n{error}"), Self::Lua{source,error}=>write!(f,"lua error: source:\n{source}\n{error}"),
Self::RustLua(error)=>write!(f,"rust-side lua error: {error}"), Self::RustLua(error)=>write!(f,"rust-side lua error: {error}"),
other=>write!(f,"{other:?}"),
} }
} }
} }
@ -30,19 +31,14 @@ fn init(lua:&mlua::Lua)->mlua::Result<()>{
//global environment //global environment
let globals=lua.globals(); let globals=lua.globals();
super::task::set_globals(lua,&globals)?; #[cfg(feature="run-service")]
crate::scheduler::set_globals(lua,&globals)?;
super::script_signal::set_globals(lua,&globals)?; super::script_signal::set_globals(lua,&globals)?;
super::r#enum::set_globals(lua,&globals)?; super::r#enum::set_globals(lua,&globals)?;
super::udim::set_globals(lua,&globals)?;
super::udim2::set_globals(lua,&globals)?;
super::color3::set_globals(lua,&globals)?; super::color3::set_globals(lua,&globals)?;
super::brickcolor::set_globals(lua,&globals)?;
super::vector2::set_globals(lua,&globals)?;
super::vector3::set_globals(lua,&globals)?; super::vector3::set_globals(lua,&globals)?;
super::cframe::set_globals(lua,&globals)?; super::cframe::set_globals(lua,&globals)?;
super::instance::instance::set_globals(lua,&globals)?; super::instance::instance::set_globals(lua,&globals)?;
super::tween_info::set_globals(lua,&globals)?;
super::number_range::set_globals(lua,&globals)?;
super::number_sequence::set_globals(lua,&globals)?; super::number_sequence::set_globals(lua,&globals)?;
super::color_sequence::set_globals(lua,&globals)?; super::color_sequence::set_globals(lua,&globals)?;
@ -58,21 +54,22 @@ impl Runner{
Ok(runner) Ok(runner)
} }
pub fn runnable_context<'a>(self,context:&'a mut Context)->Result<Runnable<'a>,Error>{ pub fn runnable_context<'a>(self,context:&'a mut Context)->Result<Runnable<'a>,Error>{
let services=context.find_services().ok_or(Error::NoServices)?;
self.runnable_context_with_services(context,&services)
}
pub fn runnable_context_with_services<'a>(self,context:&'a mut Context,services:&crate::context::Services)->Result<Runnable<'a>,Error>{
{ {
let globals=self.lua.globals(); let globals=self.lua.globals();
globals.set("game",super::instance::Instance::new_unchecked(context.services.game)).map_err(Error::RustLua)?; globals.set("game",super::instance::Instance::new(services.game)).map_err(Error::RustLua)?;
globals.set("workspace",super::instance::Instance::new_unchecked(context.services.workspace)).map_err(Error::RustLua)?; globals.set("workspace",super::instance::Instance::new(services.workspace)).map_err(Error::RustLua)?;
} }
// SAFETY: This is not a &'static mut WeakDom, //this makes set_app_data shut up about the lifetime
// but as long as Runnable<'a> holds the lifetime of &'a mut Context self.lua.set_app_data::<&'static mut rbx_dom_weak::WeakDom>(unsafe{core::mem::transmute(&mut context.dom)});
// it is a valid unique reference.
let ptr=&mut context.dom as *mut rbx_dom_weak::WeakDom;
self.lua.set_app_data::<crate::context::LuaAppData>(unsafe{&mut*ptr});
#[cfg(feature="run-service")] #[cfg(feature="run-service")]
self.lua.set_app_data::<crate::scheduler::Scheduler>(crate::scheduler::Scheduler::default()); self.lua.set_app_data::<crate::scheduler::Scheduler>(crate::scheduler::Scheduler::default());
Ok(Runnable{ Ok(Runnable{
lua:self.lua, lua:self.lua,
_lifetime:std::marker::PhantomData _lifetime:&std::marker::PhantomData
}) })
} }
} }
@ -80,11 +77,11 @@ impl Runner{
//Runnable is the same thing but has context set, which it holds the lifetime for. //Runnable is the same thing but has context set, which it holds the lifetime for.
pub struct Runnable<'a>{ pub struct Runnable<'a>{
lua:mlua::Lua, lua:mlua::Lua,
_lifetime:std::marker::PhantomData<&'a ()> _lifetime:&'a std::marker::PhantomData<()>
} }
impl Runnable<'_>{ impl Runnable<'_>{
pub fn drop_context(self)->Runner{ pub fn drop_context(self)->Runner{
self.lua.remove_app_data::<crate::context::LuaAppData>(); self.lua.remove_app_data::<&'static mut rbx_dom_weak::WeakDom>();
#[cfg(feature="run-service")] #[cfg(feature="run-service")]
self.lua.remove_app_data::<crate::scheduler::Scheduler>(); self.lua.remove_app_data::<crate::scheduler::Scheduler>();
Runner{ Runner{
@ -126,15 +123,20 @@ impl Runnable<'_>{
} }
#[cfg(feature="run-service")] #[cfg(feature="run-service")]
pub fn run_service_step(&self)->Result<(),mlua::Error>{ pub fn run_service_step(&self)->Result<(),mlua::Error>{
let render_stepped_signal=super::instance::instance::dom_mut(&self.lua,|dom|{ let render_stepped=super::instance::instance::dom_mut(&self.lua,|dom|{
let run_service=super::instance::instance::find_first_child_of_class(dom,dom.root(),"RunService").ok_or_else(||mlua::Error::runtime("RunService missing"))?; let run_service=super::instance::instance::find_first_child_of_class(dom,dom.root(),"RunService").ok_or_else(||mlua::Error::runtime("RunService missing"))?;
Ok(match run_service.userdata.get(&static_ustr("RenderStepped")){ super::instance::instance::instance_value_store_mut(&self.lua,|instance_value_store|{
Some(render_stepped)=>Some(render_stepped.borrow::<super::script_signal::ScriptSignal>()?.clone()), //unwrap because I trust my find_first_child_of_class function to
None=>None let mut instance_values=instance_value_store.get_or_create_instance_values(run_service).ok_or_else(||mlua::Error::runtime("RunService InstanceValues missing"))?;
let render_stepped=instance_values.get_or_create_value(&self.lua,"RenderStepped")?;
//let stepped=instance_values.get_or_create_value(&self.lua,"Stepped")?;
//let heartbeat=instance_values.get_or_create_value(&self.lua,"Heartbeat")?;
Ok(render_stepped)
}) })
})?; })?;
if let Some(render_stepped_signal)=render_stepped_signal{ if let Some(render_stepped)=render_stepped{
render_stepped_signal.fire(&mlua::MultiValue::new()); let signal:&super::script_signal::ScriptSignal=&*render_stepped.borrow()?;
signal.fire(&mlua::MultiValue::new());
} }
Ok(()) Ok(())
} }

@ -98,9 +98,12 @@ impl ScriptConnection{
impl mlua::UserData for ScriptSignal{ impl mlua::UserData for ScriptSignal{
fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){ fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){
methods.add_method("connect",|_lua,this,f:mlua::Function|Ok(this.connect(f))); methods.add_method("Connect",|_lua,this,f:mlua::Function|
methods.add_method("Connect",|_lua,this,f:mlua::Function|Ok(this.connect(f))); Ok(this.connect(f))
methods.add_method("Once",|_lua,this,f:mlua::Function|Ok(this.once(f))); );
methods.add_method("Once",|_lua,this,f:mlua::Function|
Ok(this.once(f))
);
// Fire is not allowed to be called from Lua // Fire is not allowed to be called from Lua
// methods.add_method("Fire",|_lua,this,args:mlua::MultiValue| // methods.add_method("Fire",|_lua,this,args:mlua::MultiValue|
// Ok(this.fire(args)) // Ok(this.fire(args))
@ -161,7 +164,6 @@ pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
.call::<mlua::Function>(())?; .call::<mlua::Function>(())?;
lua.register_userdata_type::<ScriptSignal>(|reg|{ lua.register_userdata_type::<ScriptSignal>(|reg|{
reg.add_field("wait",wait.clone());
reg.add_field("Wait",wait); reg.add_field("Wait",wait);
mlua::UserData::register(reg); mlua::UserData::register(reg);
})?; })?;

@ -1,42 +0,0 @@
#[cfg(not(feature="run-service"))]
fn no_op(_lua:&mlua::Lua,_time:Option<super::number::Number>)->mlua::Result<f64>{
Ok(0.0)
}
fn tick(_lua:&mlua::Lua,_:())->mlua::Result<f64>{
Ok(0.0)
}
// This is used to avoid calling coroutine.yield from the rust side.
const LUA_WAIT:&str=
"local coroutine_yield=coroutine.yield
local schedule_thread=schedule_thread
return function(dt)
schedule_thread(dt)
return coroutine_yield()
end";
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
let coroutine_table=globals.get::<mlua::Table>("coroutine")?;
#[cfg(feature="run-service")]
let schedule_thread=lua.create_function(crate::scheduler::schedule_thread)?;
#[cfg(not(feature="run-service"))]
let schedule_thread=lua.create_function(no_op)?;
//create wait function environment
let wait_env=lua.create_table()?;
wait_env.raw_set("coroutine",coroutine_table)?;
wait_env.raw_set("schedule_thread",schedule_thread)?;
//construct wait function from Lua code
let wait=lua.load(LUA_WAIT)
.set_name("wait")
.set_environment(wait_env)
.call::<mlua::Function>(())?;
globals.raw_set("wait",wait)?;
// TODO: move this somewhere it belongs
let tick=lua.create_function(tick)?;
globals.raw_set("tick",tick)?;
Ok(())
}

@ -1,35 +0,0 @@
use super::instance::Instance;
use super::tween_info::TweenInfo;
#[allow(dead_code)]
#[derive(Clone)]
pub struct Tween{
instance:Instance,
tween_info:TweenInfo,
goal:mlua::Table,
playback_state:rbx_types::Enum,
}
impl Tween{
pub fn create(
instance:Instance,
tween_info:TweenInfo,
goal:mlua::Table,
)->Self{
Self{
instance,
tween_info,
goal,
// Enum.PlaybackState.Begin
playback_state:rbx_types::Enum::from_u32(0),
}
}
pub fn play(&mut self){
}
}
impl mlua::UserData for Tween{
fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){
methods.add_method_mut("Play",|_,this,()|Ok(this.play()))
}
}
type_from_lua_userdata_clone!(Tween);

@ -1,45 +0,0 @@
use super::number::Number;
use super::r#enum::{CoerceEnum,Enums};
#[allow(dead_code)]
#[derive(Clone)]
pub struct TweenInfo{
time:f64,
easing_style:rbx_types::Enum,
easing_direction:rbx_types::Enum,
repeat_count:u32,
reverses:bool,
delay_time:f64,
}
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
let table=lua.create_table()?;
table.raw_set("new",
lua.create_function(|_,(time,easing_style,easing_direction,repeat_count,reverses,delay_time):(Option<Number>,Option<CoerceEnum>,Option<CoerceEnum>,Option<u32>,Option<bool>,Option<Number>)|{
Ok(TweenInfo{
time:time.map_or(1.0,Number::to_f64),
easing_style:match easing_style{
// Enum.EasingStyle.Quad
None=>rbx_types::Enum::from_u32(3),
Some(e)=>e.coerce_to(Enums.get("EasingStyle").unwrap())?.into(),
},
easing_direction:match easing_direction{
// Enum.EasingDirection.Out
None=>rbx_types::Enum::from_u32(1),
Some(e)=>e.coerce_to(Enums.get("EasingDirection").unwrap())?.into(),
},
repeat_count:repeat_count.unwrap_or(0),
reverses:reverses.unwrap_or(false),
delay_time:delay_time.map_or(0.0,Number::to_f64),
})
})?
)?;
globals.set("TweenInfo",table)?;
Ok(())
}
impl mlua::UserData for TweenInfo{}
type_from_lua_userdata_clone!(TweenInfo);

@ -1,36 +0,0 @@
use super::number::Number;
#[derive(Clone,Copy)]
pub struct UDim(rbx_types::UDim);
impl UDim{
pub fn new(scale:f32,offset:i32)->Self{
UDim(rbx_types::UDim::new(scale,offset))
}
}
impl From<rbx_types::UDim> for UDim{
fn from(value:rbx_types::UDim)->Self{
Self(value)
}
}
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
let table=lua.create_table()?;
table.raw_set("new",
lua.create_function(|_,(scale,offset):(Number,i32)|
Ok(UDim::new(scale.into(),offset))
)?
)?;
globals.set("UDim",table)?;
Ok(())
}
impl mlua::UserData for UDim{
fn add_fields<F:mlua::UserDataFields<Self>>(fields:&mut F){
fields.add_field_method_get("Scale",|_,UDim(this)|Ok(this.scale));
fields.add_field_method_get("Offset",|_,UDim(this)|Ok(this.offset));
}
}
type_from_lua_userdata!(UDim);

@ -1,40 +0,0 @@
use super::udim::UDim;
use super::number::Number;
#[derive(Clone,Copy)]
pub struct UDim2(rbx_types::UDim2);
impl UDim2{
pub fn new(sx:f32,ox:i32,sy:f32,oy:i32)->Self{
UDim2(rbx_types::UDim2::new(
rbx_types::UDim::new(sx,ox),
rbx_types::UDim::new(sy,oy),
))
}
}
impl From<UDim2> for rbx_types::UDim2{
fn from(UDim2(value):UDim2)->rbx_types::UDim2{
value
}
}
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
let table=lua.create_table()?;
table.raw_set("new",
lua.create_function(|_,(sx,ox,sy,oy):(Number,i32,Number,i32)|
Ok(UDim2::new(sx.into(),ox,sy.into(),oy))
)?
)?;
globals.set("UDim2",table)?;
Ok(())
}
impl mlua::UserData for UDim2{
fn add_fields<F:mlua::UserDataFields<Self>>(fields:&mut F){
fields.add_field_method_get("X",|_,UDim2(this)|Ok(UDim::from(this.x)));
fields.add_field_method_get("Y",|_,UDim2(this)|Ok(UDim::from(this.y)));
}
}
type_from_lua_userdata!(UDim2);

@ -1,93 +0,0 @@
use mlua::FromLua;
use super::number::Number;
#[derive(Clone,Copy)]
pub struct Vector2(glam::Vec2);
impl Vector2{
pub const fn new(x:f32,y:f32)->Self{
Self(glam::vec2(x,y))
}
}
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
let table=lua.create_table()?;
//Vector2.new
table.raw_set("new",
lua.create_function(|_,(x,y):(Option<Number>,Option<Number>)|
match (x,y){
(Some(x),Some(y))=>Ok(Vector2::new(x.into(),y.into())),
(None,None)=>Ok(Vector2(glam::Vec2::ZERO)),
_=>Err(mlua::Error::runtime("Unsupported arguments to Vector2.new")),
}
)?
)?;
globals.set("Vector2",table)?;
Ok(())
}
impl From<Vector2> for rbx_types::Vector2{
fn from(Vector2(v):Vector2)->rbx_types::Vector2{
rbx_types::Vector2::new(v.x,v.y)
}
}
impl From<rbx_types::Vector2> for Vector2{
fn from(value:rbx_types::Vector2)->Vector2{
Vector2::new(value.x,value.y)
}
}
impl mlua::UserData for Vector2{
fn add_fields<F:mlua::UserDataFields<Self>>(fields:&mut F){
fields.add_field_method_get("magnitude",|_,Vector2(this)|Ok(this.length()));
fields.add_field_method_get("Magnitude",|_,Vector2(this)|Ok(this.length()));
fields.add_field_method_get("unit",|_,Vector2(this)|Ok(Vector2(this.normalize())));
fields.add_field_method_get("Unit",|_,Vector2(this)|Ok(Vector2(this.normalize())));
fields.add_field_method_get("x",|_,Vector2(this)|Ok(this.x));
fields.add_field_method_get("X",|_,Vector2(this)|Ok(this.x));
fields.add_field_method_get("y",|_,Vector2(this)|Ok(this.y));
fields.add_field_method_get("Y",|_,Vector2(this)|Ok(this.y));
}
fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){
//methods.add_method("area",|_,this,()| Ok(this.length * this.width));
methods.add_meta_function(mlua::MetaMethod::Add,|_,(Vector2(this),Vector2(val)):(Self,Self)|Ok(Self(this+val)));
methods.add_meta_function(mlua::MetaMethod::Sub,|_,(Vector2(this),Vector2(val)):(Self,Self)|Ok(Self(this-val)));
methods.add_meta_function(mlua::MetaMethod::Mul,|lua,(lhs,rhs):(mlua::Value,mlua::Value)|{
match (lhs,rhs){
(mlua::Value::UserData(lhs),mlua::Value::UserData(rhs))=>lhs.borrow_scoped(|Vector2(lhs):&Vector2|rhs.borrow_scoped(|Vector2(rhs):&Vector2|Self(lhs*rhs)))?,
(lhs,mlua::Value::UserData(rhs))=>{
let lhs=Number::from_lua(lhs,lua)?;
rhs.borrow_scoped(|Vector2(rhs):&Vector2|Self(lhs.to_f32()*rhs))
},
(mlua::Value::UserData(lhs),rhs)=>{
let rhs=Number::from_lua(rhs,lua)?;
lhs.borrow_scoped(|Vector2(lhs):&Vector2|Self(lhs*rhs.to_f32()))
},
_=>Err(mlua::Error::runtime(format!("Expected Vector2")))
}
});
methods.add_meta_function(mlua::MetaMethod::Div,|_,(Vector2(this),val):(Self,mlua::Value)|{
match val{
mlua::Value::Integer(n)=>Ok(Self(this/(n as f32))),
mlua::Value::Number(n)=>Ok(Self(this/(n as f32))),
mlua::Value::UserData(ud)=>ud.borrow_scoped(|Vector2(rhs):&Vector2|Self(this/rhs)),
other=>Err(mlua::Error::runtime(format!("Attempt to divide Vector2 by {other:?}"))),
}
});
methods.add_meta_function(mlua::MetaMethod::ToString,|_,Vector2(this):Self|
Ok(format!("Vector2.new({},{})",
this.x,
this.y,
))
);
}
}
type_from_lua_userdata!(Vector2);

@ -1,7 +1,3 @@
use mlua::FromLua;
use super::number::Number;
#[derive(Clone,Copy)] #[derive(Clone,Copy)]
pub struct Vector3(pub(crate)glam::Vec3A); pub struct Vector3(pub(crate)glam::Vec3A);
@ -12,27 +8,23 @@ impl Vector3{
} }
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{ pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
let table=lua.create_table()?; let vector3_table=lua.create_table()?;
//Vector3.new //Vector3.new
table.raw_set("new", vector3_table.raw_set("new",
lua.create_function(|_,(x,y,z):(Option<Number>,Option<Number>,Option<Number>)| lua.create_function(|_,(x,y,z):(f32,f32,f32)|
match (x,y,z){ Ok(Vector3::new(x,y,z))
(Some(x),Some(y),Some(z))=>Ok(Vector3::new(x.into(),y.into(),z.into())),
(None,None,None)=>Ok(Vector3(glam::Vec3A::ZERO)),
_=>Err(mlua::Error::runtime("Unsupported arguments to Vector3.new")),
}
)? )?
)?; )?;
globals.set("Vector3",table)?; globals.set("Vector3",vector3_table)?;
Ok(()) Ok(())
} }
impl From<Vector3> for rbx_types::Vector3{ impl Into<rbx_types::Vector3> for Vector3{
fn from(Vector3(v):Vector3)->rbx_types::Vector3{ fn into(self)->rbx_types::Vector3{
rbx_types::Vector3::new(v.x,v.y,v.z) rbx_types::Vector3::new(self.0.x,self.0.y,self.0.z)
} }
} }
@ -44,50 +36,44 @@ impl From<rbx_types::Vector3> for Vector3{
impl mlua::UserData for Vector3{ impl mlua::UserData for Vector3{
fn add_fields<F:mlua::UserDataFields<Self>>(fields:&mut F){ fn add_fields<F:mlua::UserDataFields<Self>>(fields:&mut F){
fields.add_field_method_get("magnitude",|_,Vector3(this)|Ok(this.length())); fields.add_field_method_get("magnitude",|_,this|Ok(this.0.length()));
fields.add_field_method_get("Magnitude",|_,Vector3(this)|Ok(this.length())); fields.add_field_method_get("x",|_,this|Ok(this.0.x));
fields.add_field_method_get("unit",|_,Vector3(this)|Ok(Vector3(this.normalize()))); fields.add_field_method_set("x",|_,this,val|{
fields.add_field_method_get("Unit",|_,Vector3(this)|Ok(Vector3(this.normalize()))); this.0.x=val;
fields.add_field_method_get("x",|_,Vector3(this)|Ok(this.x)); Ok(())
fields.add_field_method_get("X",|_,Vector3(this)|Ok(this.x)); });
fields.add_field_method_get("y",|_,Vector3(this)|Ok(this.y)); fields.add_field_method_get("y",|_,this|Ok(this.0.y));
fields.add_field_method_get("Y",|_,Vector3(this)|Ok(this.y)); fields.add_field_method_set("y",|_,this,val|{
fields.add_field_method_get("z",|_,Vector3(this)|Ok(this.z)); this.0.y=val;
fields.add_field_method_get("Z",|_,Vector3(this)|Ok(this.z)); Ok(())
});
fields.add_field_method_get("z",|_,this|Ok(this.0.z));
fields.add_field_method_set("z",|_,this,val|{
this.0.z=val;
Ok(())
});
} }
fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){ fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){
//methods.add_method("area",|_,this,()| Ok(this.length * this.width)); //methods.add_method("area",|_,this,()| Ok(this.length * this.width));
methods.add_meta_function(mlua::MetaMethod::Add,|_,(Vector3(this),Vector3(val)):(Self,Self)|Ok(Self(this+val))); methods.add_meta_function(mlua::MetaMethod::Add,|_,(this,val):(Self,Self)|Ok(Self(this.0+val.0)));
methods.add_meta_function(mlua::MetaMethod::Sub,|_,(Vector3(this),Vector3(val)):(Self,Self)|Ok(Self(this-val))); methods.add_meta_function(mlua::MetaMethod::Div,|_,(this,val):(Self,mlua::Value)|{
methods.add_meta_function(mlua::MetaMethod::Mul,|lua,(lhs,rhs):(mlua::Value,mlua::Value)|{
match (lhs,rhs){
(mlua::Value::UserData(lhs),mlua::Value::UserData(rhs))=>lhs.borrow_scoped(|Vector3(lhs):&Vector3|rhs.borrow_scoped(|Vector3(rhs):&Vector3|Self(lhs*rhs)))?,
(lhs,mlua::Value::UserData(rhs))=>{
let lhs=Number::from_lua(lhs,lua)?;
rhs.borrow_scoped(|Vector3(rhs):&Vector3|Self(lhs.to_f32()*rhs))
},
(mlua::Value::UserData(lhs),rhs)=>{
let rhs=Number::from_lua(rhs,lua)?;
lhs.borrow_scoped(|Vector3(lhs):&Vector3|Self(lhs*rhs.to_f32()))
},
_=>Err(mlua::Error::runtime(format!("Expected Vector3")))
}
});
methods.add_meta_function(mlua::MetaMethod::Div,|_,(Vector3(this),val):(Self,mlua::Value)|{
match val{ match val{
mlua::Value::Integer(n)=>Ok(Self(this/(n as f32))), mlua::Value::Integer(n)=>Ok(Self(this.0/(n as f32))),
mlua::Value::Number(n)=>Ok(Self(this/(n as f32))), mlua::Value::Number(n)=>Ok(Self(this.0/(n as f32))),
mlua::Value::UserData(ud)=>ud.borrow_scoped(|Vector3(rhs):&Vector3|Self(this/rhs)), mlua::Value::UserData(ud)=>{
let rhs:Vector3=ud.take()?;
Ok(Self(this.0/rhs.0))
},
other=>Err(mlua::Error::runtime(format!("Attempt to divide Vector3 by {other:?}"))), other=>Err(mlua::Error::runtime(format!("Attempt to divide Vector3 by {other:?}"))),
} }
}); });
methods.add_meta_function(mlua::MetaMethod::ToString,|_,Vector3(this):Self| methods.add_meta_function(mlua::MetaMethod::ToString,|_,this:Self|
Ok(format!("Vector3.new({},{},{})", Ok(format!("Vector3.new({},{},{})",
this.x, this.0.x,
this.y, this.0.y,
this.z, this.0.z,
)) ))
); );
} }

@ -51,7 +51,7 @@ pub fn scheduler_mut<T>(lua:&mlua::Lua,mut f:impl FnMut(&mut crate::scheduler::S
f(&mut *scheduler) f(&mut *scheduler)
} }
pub fn schedule_thread(lua:&mlua::Lua,dt:mlua::Value)->Result<(),mlua::Error>{ fn schedule_thread(lua:&mlua::Lua,dt:mlua::Value)->Result<(),mlua::Error>{
let delay=match dt{ let delay=match dt{
mlua::Value::Integer(i)=>i.max(0) as u64*60, mlua::Value::Integer(i)=>i.max(0) as u64*60,
mlua::Value::Number(f)=>{ mlua::Value::Number(f)=>{
@ -75,3 +75,32 @@ pub fn schedule_thread(lua:&mlua::Lua,dt:mlua::Value)->Result<(),mlua::Error>{
Ok(()) Ok(())
}) })
} }
// This is used to avoid calling coroutine.yield from the rust side.
const LUA_WAIT:&str=
"local coroutine_yield=coroutine.yield
local schedule_thread=schedule_thread
return function(dt)
schedule_thread(dt)
return coroutine_yield()
end";
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
let coroutine_table=globals.get::<mlua::Table>("coroutine")?;
let schedule_thread=lua.create_function(schedule_thread)?;
//create wait function environment
let wait_env=lua.create_table()?;
wait_env.raw_set("coroutine",coroutine_table)?;
wait_env.raw_set("schedule_thread",schedule_thread)?;
//construct wait function from Lua code
let wait=lua.load(LUA_WAIT)
.set_name("wait")
.set_environment(wait_env)
.call::<mlua::Function>(())?;
globals.raw_set("wait",wait)?;
Ok(())
}

@ -1,3 +0,0 @@
pub fn static_ustr(s:&'static str)->rbx_dom_weak::Ustr{
rbx_dom_weak::ustr(s)
}

@ -1,11 +1,11 @@
[package] [package]
name = "strafesnet_snf" name = "strafesnet_snf"
version = "0.3.1" version = "0.3.0"
edition = "2024" edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
binrw = "0.15.0" binrw = "0.14.0"
id = { version = "0.1.0", registry = "strafesnet" } id = { version = "0.1.0", registry = "strafesnet" }
strafesnet_common = { version = "0.7.0", path = "../common", registry = "strafesnet" } strafesnet_common = { version = "0.6.0", path = "../common", registry = "strafesnet" }

@ -56,6 +56,8 @@ impl From<strafesnet_common::integer::Ratio64Vec2> for Ratio64Vec2{
} }
} }
pub type Angle32=i32;
pub type Planar64=i64; pub type Planar64=i64;
pub type Planar64Vec3=[i64;3]; pub type Planar64Vec3=[i64;3];
pub type Planar64Mat3=[i64;9];
pub type Planar64Affine3=[i64;12]; pub type Planar64Affine3=[i64;12];

@ -1,6 +1,6 @@
[package] [package]
name = "map-tool" name = "map-tool"
version = "1.7.2" version = "1.7.0"
edition = "2024" edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -13,19 +13,19 @@ futures = "0.3.31"
image = "0.25.2" image = "0.25.2"
image_dds = "0.7.1" image_dds = "0.7.1"
lazy-regex = "3.1.0" lazy-regex = "3.1.0"
rbx_asset = { version = "0.4.4", registry = "strafesnet" } rbx_asset = { version = "0.2.5", registry = "strafesnet" }
rbx_binary = { version = "1.1.0-sn4", registry = "strafesnet" } rbx_binary = { version = "0.7.4", registry = "strafesnet" }
rbx_dom_weak = { version = "3.1.0-sn4", registry = "strafesnet" } rbx_dom_weak = { version = "2.7.0", registry = "strafesnet" }
rbx_reflection_database = "1.0.0" rbx_reflection_database = { version = "0.2.10", registry = "strafesnet" }
rbx_xml = { version = "1.1.0-sn4", registry = "strafesnet" } rbx_xml = { version = "0.13.3", registry = "strafesnet" }
rbxassetid = { version = "0.1.0", registry = "strafesnet" } rbxassetid = { version = "0.1.0", registry = "strafesnet" }
strafesnet_bsp_loader = { version = "0.3.1", path = "../lib/bsp_loader", registry = "strafesnet" } strafesnet_bsp_loader = { version = "0.3.0", path = "../lib/bsp_loader", registry = "strafesnet" }
strafesnet_deferred_loader = { version = "0.5.1", path = "../lib/deferred_loader", registry = "strafesnet" } strafesnet_deferred_loader = { version = "0.5.0", path = "../lib/deferred_loader", registry = "strafesnet" }
strafesnet_rbx_loader = { version = "0.7.0", path = "../lib/rbx_loader", registry = "strafesnet" } strafesnet_rbx_loader = { version = "0.6.0", path = "../lib/rbx_loader", registry = "strafesnet" }
strafesnet_snf = { version = "0.3.1", path = "../lib/snf", registry = "strafesnet" } strafesnet_snf = { version = "0.3.0", path = "../lib/snf", registry = "strafesnet" }
thiserror = "2.0.11" thiserror = "2.0.11"
tokio = { version = "1.43.0", features = ["macros", "rt-multi-thread", "fs"] } tokio = { version = "1.43.0", features = ["macros", "rt-multi-thread", "fs"] }
vbsp = "0.9.1" vbsp = "0.8.0"
vbsp-entities-css = "0.6.0" vbsp-entities-css = "0.6.0"
vmdl = "0.2.0" vmdl = "0.2.0"
vmt-parser = "0.2.0" vmt-parser = "0.2.0"

@ -8,11 +8,6 @@ use strafesnet_deferred_loader::deferred_loader::LoadFailureMode;
use rbxassetid::RobloxAssetId; use rbxassetid::RobloxAssetId;
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
// disallow non-static lifetimes
fn static_ustr(s:&'static str)->rbx_dom_weak::Ustr{
rbx_dom_weak::ustr(s)
}
const DOWNLOAD_LIMIT:usize=16; const DOWNLOAD_LIMIT:usize=16;
#[derive(Subcommand)] #[derive(Subcommand)]
@ -32,12 +27,8 @@ pub struct RobloxToSNFSubcommand {
pub struct DownloadAssetsSubcommand{ pub struct DownloadAssetsSubcommand{
#[arg(required=true)] #[arg(required=true)]
roblox_files:Vec<PathBuf>, roblox_files:Vec<PathBuf>,
#[arg(long,group="cookie",required=true)] // #[arg(long)]
cookie_literal:Option<String>, // cookie_file:Option<String>,
#[arg(long,group="cookie",required=true)]
cookie_envvar:Option<String>,
#[arg(long,group="cookie",required=true)]
cookie_file:Option<PathBuf>,
} }
impl Commands{ impl Commands{
@ -46,26 +37,12 @@ impl Commands{
Commands::RobloxToSNF(subcommand)=>roblox_to_snf(subcommand.input_files,subcommand.output_folder).await, Commands::RobloxToSNF(subcommand)=>roblox_to_snf(subcommand.input_files,subcommand.output_folder).await,
Commands::DownloadAssets(subcommand)=>download_assets( Commands::DownloadAssets(subcommand)=>download_assets(
subcommand.roblox_files, subcommand.roblox_files,
cookie_from_args( rbx_asset::cookie::Cookie::new("".to_string()),
subcommand.cookie_literal,
subcommand.cookie_envvar,
subcommand.cookie_file,
).await?,
).await, ).await,
} }
} }
} }
async fn cookie_from_args(literal:Option<String>,environment:Option<String>,file:Option<PathBuf>)->AResult<rbx_asset::cookie::Cookie>{
let cookie=match (literal,environment,file){
(Some(cookie_literal),None,None)=>cookie_literal,
(None,Some(cookie_environment),None)=>std::env::var(cookie_environment)?,
(None,None,Some(cookie_file))=>tokio::fs::read_to_string(cookie_file).await?,
_=>Err(anyhow::Error::msg("Illegal cookie argument triple"))?,
};
Ok(rbx_asset::cookie::Cookie::new(cookie))
}
#[allow(unused)] #[allow(unused)]
#[derive(Debug)] #[derive(Debug)]
enum LoadDomError{ enum LoadDomError{
@ -111,45 +88,17 @@ SurfaceAppearance.NormalMap
SurfaceAppearance.RoughnessMap SurfaceAppearance.RoughnessMap
SurfaceAppearance.TexturePack SurfaceAppearance.TexturePack
*/ */
/* These properties now use Content fn accumulate_content_id(content_list:&mut HashSet<RobloxAssetId>,object:&Instance,property:&str){
BaseWrap.CageMeshContent if let Some(rbx_dom_weak::types::Variant::Content(content))=object.properties.get(property){
Decal.TextureContent let url:&str=content.as_ref();
ImageButton.ImageContent if let Ok(asset_id)=url.parse(){
ImageLabel.ImageContent content_list.insert(asset_id);
MeshPart.MeshContent }else{
MeshPart.TextureContent println!("Content failed to parse into AssetID: {:?}",content);
SurfaceAppearance.ColorMapContent }
SurfaceAppearance.MetalnessMapContent }else{
SurfaceAppearance.NormalMapContent
SurfaceAppearance.RoughnessMapContent
WrapLayer.ReferenceMeshContent
*/
fn accumulate_content(content_list:&mut HashSet<RobloxAssetId>,object:&Instance,property:&'static str){
let Some(rbx_dom_weak::types::Variant::Content(content))=object.properties.get(&static_ustr(property))else{
println!("property={} does not exist for class={}",property,object.class.as_str()); println!("property={} does not exist for class={}",property,object.class.as_str());
return; }
};
let rbx_dom_weak::types::ContentType::Uri(uri)=content.value()else{
println!("ContentType is not Uri");
return;
};
let Ok(asset_id)=uri.parse()else{
println!("Content failed to parse into AssetID: {:?}",content);
return;
};
content_list.insert(asset_id);
}
fn accumulate_content_id(content_list:&mut HashSet<RobloxAssetId>,object:&Instance,property:&'static str){
let Some(rbx_dom_weak::types::Variant::ContentId(content))=object.properties.get(&static_ustr(property))else{
println!("property={} does not exist for class={}",property,object.class.as_str());
return;
};
let Ok(asset_id)=content.as_str().parse()else{
println!("Content failed to parse into AssetID: {:?}",content);
return;
};
content_list.insert(asset_id);
} }
async fn read_entire_file(path:impl AsRef<Path>)->Result<Cursor<Vec<u8>>,std::io::Error>{ async fn read_entire_file(path:impl AsRef<Path>)->Result<Cursor<Vec<u8>>,std::io::Error>{
let mut file=tokio::fs::File::open(path).await?; let mut file=tokio::fs::File::open(path).await?;
@ -167,12 +116,12 @@ impl UniqueAssets{
fn collect(&mut self,object:&Instance){ fn collect(&mut self,object:&Instance){
match object.class.as_str(){ match object.class.as_str(){
"Beam"=>accumulate_content_id(&mut self.textures,object,"Texture"), "Beam"=>accumulate_content_id(&mut self.textures,object,"Texture"),
"Decal"=>accumulate_content(&mut self.textures,object,"TextureContent"), "Decal"=>accumulate_content_id(&mut self.textures,object,"Texture"),
"Texture"=>accumulate_content(&mut self.textures,object,"TextureContent"), "Texture"=>accumulate_content_id(&mut self.textures,object,"Texture"),
"FileMesh"=>accumulate_content_id(&mut self.textures,object,"TextureId"), "FileMesh"=>accumulate_content_id(&mut self.textures,object,"TextureId"),
"MeshPart"=>{ "MeshPart"=>{
accumulate_content(&mut self.textures,object,"TextureContent"); accumulate_content_id(&mut self.textures,object,"TextureID");
accumulate_content(&mut self.meshes,object,"MeshContent"); accumulate_content_id(&mut self.meshes,object,"MeshId");
}, },
"SpecialMesh"=>accumulate_content_id(&mut self.meshes,object,"MeshId"), "SpecialMesh"=>accumulate_content_id(&mut self.meshes,object,"MeshId"),
"ParticleEmitter"=>accumulate_content_id(&mut self.textures,object,"Texture"), "ParticleEmitter"=>accumulate_content_id(&mut self.textures,object,"Texture"),
@ -216,16 +165,16 @@ enum DownloadType{
impl DownloadType{ impl DownloadType{
fn path(&self)->PathBuf{ fn path(&self)->PathBuf{
match self{ match self{
DownloadType::Texture(RobloxAssetId(asset_id))=>format!("downloaded_textures/{asset_id}").into(), DownloadType::Texture(asset_id)=>format!("downloaded_textures/{}",asset_id.0.to_string()).into(),
DownloadType::Mesh(RobloxAssetId(asset_id))=>format!("meshes/{asset_id}").into(), DownloadType::Mesh(asset_id)=>format!("meshes/{}",asset_id.0.to_string()).into(),
DownloadType::Union(RobloxAssetId(asset_id))=>format!("unions/{asset_id}").into(), DownloadType::Union(asset_id)=>format!("unions/{}",asset_id.0.to_string()).into(),
} }
} }
fn asset_id(&self)->u64{ fn asset_id(&self)->u64{
match self{ match self{
&DownloadType::Texture(RobloxAssetId(asset_id))=>asset_id, DownloadType::Texture(asset_id)=>asset_id.0,
&DownloadType::Mesh(RobloxAssetId(asset_id))=>asset_id, DownloadType::Mesh(asset_id)=>asset_id.0,
&DownloadType::Union(RobloxAssetId(asset_id))=>asset_id, DownloadType::Union(asset_id)=>asset_id.0,
} }
} }
} }
@ -242,8 +191,9 @@ struct Stats{
failed_downloads:u32, failed_downloads:u32,
timed_out_downloads:u32, timed_out_downloads:u32,
} }
async fn download_retry(stats:&mut Stats,context:&rbx_asset::cookie::Context,download_instruction:DownloadType)->Result<DownloadResult,std::io::Error>{ async fn download_retry(stats:&mut Stats,context:&rbx_asset::cookie::CookieContext,download_instruction:DownloadType)->Result<DownloadResult,std::io::Error>{
stats.total_assets+=1; stats.total_assets+=1;
let download_instruction=download_instruction;
// check if file exists on disk // check if file exists on disk
let path=download_instruction.path(); let path=download_instruction.path();
if tokio::fs::try_exists(path.as_path()).await?{ if tokio::fs::try_exists(path.as_path()).await?{
@ -263,12 +213,11 @@ async fn download_retry(stats:&mut Stats,context:&rbx_asset::cookie::Context,dow
match asset_result{ match asset_result{
Ok(asset_result)=>{ Ok(asset_result)=>{
stats.downloaded_assets+=1; stats.downloaded_assets+=1;
let data=asset_result.to_vec()?; tokio::fs::write(path,&asset_result).await?;
tokio::fs::write(path,&data).await?; break Ok(DownloadResult::Data(asset_result));
break Ok(DownloadResult::Data(data));
}, },
Err(rbx_asset::cookie::GetError::Response(rbx_asset::types::ResponseError::Details{status_code,url_and_body}))=>{ Err(rbx_asset::cookie::GetError::Response(rbx_asset::ResponseError::StatusCodeWithUrlAndBody(scwuab)))=>{
if status_code.as_u16()==429{ if scwuab.status_code.as_u16()==429{
if retry==12{ if retry==12{
println!("Giving up asset download {asset_id}"); println!("Giving up asset download {asset_id}");
stats.timed_out_downloads+=1; stats.timed_out_downloads+=1;
@ -280,7 +229,7 @@ async fn download_retry(stats:&mut Stats,context:&rbx_asset::cookie::Context,dow
retry+=1; retry+=1;
}else{ }else{
stats.failed_downloads+=1; stats.failed_downloads+=1;
println!("weird status_code error: status_code={status_code} url={} body={}",url_and_body.url,url_and_body.body); println!("weird scuwab error: {scwuab:?}");
break Ok(DownloadResult::Failed); break Ok(DownloadResult::Failed);
} }
}, },
@ -303,7 +252,7 @@ enum ConvertTextureError{
#[error("DDS write error {0:?}")] #[error("DDS write error {0:?}")]
DDSWrite(#[from]image_dds::ddsfile::Error), DDSWrite(#[from]image_dds::ddsfile::Error),
} }
async fn convert_texture(RobloxAssetId(asset_id):RobloxAssetId,download_result:DownloadResult)->Result<(),ConvertTextureError>{ async fn convert_texture(asset_id:RobloxAssetId,download_result:DownloadResult)->Result<(),ConvertTextureError>{
let data=match download_result{ let data=match download_result{
DownloadResult::Cached(path)=>tokio::fs::read(path).await?, DownloadResult::Cached(path)=>tokio::fs::read(path).await?,
DownloadResult::Data(data)=>data, DownloadResult::Data(data)=>data,
@ -328,7 +277,7 @@ async fn convert_texture(RobloxAssetId(asset_id):RobloxAssetId,download_result:D
image_dds::Mipmaps::GeneratedAutomatic, image_dds::Mipmaps::GeneratedAutomatic,
)?; )?;
let file_name=format!("textures/{asset_id}.dds"); let file_name=format!("textures/{}.dds",asset_id.0);
let mut file=std::fs::File::create(file_name)?; let mut file=std::fs::File::create(file_name)?;
dds.write(&mut file)?; dds.write(&mut file)?;
Ok(()) Ok(())
@ -366,7 +315,7 @@ async fn download_assets(paths:Vec<PathBuf>,cookie:rbx_asset::cookie::Cookie)->A
// insert into global unique assets guy // insert into global unique assets guy
// add to download queue if the asset is globally unique and does not already exist on disk // add to download queue if the asset is globally unique and does not already exist on disk
let mut stats=Stats::default(); let mut stats=Stats::default();
let context=rbx_asset::cookie::Context::new(cookie); let context=rbx_asset::cookie::CookieContext::new(cookie);
let mut globally_unique_assets=UniqueAssets::default(); let mut globally_unique_assets=UniqueAssets::default();
// pop a job = retry_queue.pop_front() or ingest(recv.recv().await) // pop a job = retry_queue.pop_front() or ingest(recv.recv().await)
// SLOW MODE: // SLOW MODE:
@ -434,23 +383,17 @@ impl std::fmt::Display for ConvertError{
} }
} }
impl std::error::Error for ConvertError{} impl std::error::Error for ConvertError{}
async fn convert_to_snf(path:&Path,output_folder:PathBuf)->AResult<()>{
struct Errors{ let entire_file=tokio::fs::read(path).await?;
script_errors:Vec<strafesnet_rbx_loader::RunnerError>,
convert_errors:strafesnet_rbx_loader::RecoverableErrors,
}
fn convert_to_snf(path:&Path,output_folder:PathBuf)->Result<Errors,ConvertError>{
let entire_file=std::fs::read(path).map_err(ConvertError::IO)?;
let model=strafesnet_rbx_loader::read( let model=strafesnet_rbx_loader::read(
entire_file.as_slice() std::io::Cursor::new(entire_file)
).map_err(ConvertError::RobloxRead)?; ).map_err(ConvertError::RobloxRead)?;
let mut place=strafesnet_rbx_loader::Place::from(model); let mut place=model.into_place();
let script_errors=place.run_scripts().unwrap_or_else(|e|vec![e]); place.run_scripts();
let (map,convert_errors)=place.to_snf(LoadFailureMode::DefaultToNone).map_err(ConvertError::RobloxLoad)?; let map=place.to_snf(LoadFailureMode::DefaultToNone).map_err(ConvertError::RobloxLoad)?;
let mut dest=output_folder; let mut dest=output_folder;
dest.push(path.file_stem().unwrap()); dest.push(path.file_stem().unwrap());
@ -459,10 +402,7 @@ fn convert_to_snf(path:&Path,output_folder:PathBuf)->Result<Errors,ConvertError>
strafesnet_snf::map::write_map(file,map).map_err(ConvertError::SNFMap)?; strafesnet_snf::map::write_map(file,map).map_err(ConvertError::SNFMap)?;
Ok(Errors{ Ok(())
script_errors,
convert_errors,
})
} }
async fn roblox_to_snf(paths:Vec<std::path::PathBuf>,output_folder:PathBuf)->AResult<()>{ async fn roblox_to_snf(paths:Vec<std::path::PathBuf>,output_folder:PathBuf)->AResult<()>{
@ -471,25 +411,15 @@ async fn roblox_to_snf(paths:Vec<std::path::PathBuf>,output_folder:PathBuf)->ARe
let thread_limit=std::thread::available_parallelism()?.get(); let thread_limit=std::thread::available_parallelism()?.get();
let mut it=paths.into_iter(); let mut it=paths.into_iter();
static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0); static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
// This is wrong! Calling roblox_to_snf multiple times keeps adding permits
SEM.add_permits(thread_limit); SEM.add_permits(thread_limit);
while let (Ok(permit),Some(path))=(SEM.acquire().await,it.next()){ while let (Ok(permit),Some(path))=(SEM.acquire().await,it.next()){
let output_folder=output_folder.clone(); let output_folder=output_folder.clone();
tokio::task::spawn_blocking(move||{ tokio::spawn(async move{
let result=convert_to_snf(path.as_path(),output_folder); let result=convert_to_snf(path.as_path(),output_folder).await;
drop(permit); drop(permit);
match result{ match result{
Ok(errors)=>{ Ok(())=>(),
for error in errors.script_errors{
println!("Script error: {error}");
}
let error_count=errors.convert_errors.count();
if error_count!=0{
println!("Error count: {error_count}");
println!("Errors: {}",errors.convert_errors);
}
},
Err(e)=>println!("Convert error: {e:?}"), Err(e)=>println!("Convert error: {e:?}"),
} }
}); });

@ -28,5 +28,5 @@ strafesnet_rbx_loader = { path = "../lib/rbx_loader", registry = "strafesnet", o
strafesnet_session = { path = "../engine/session", registry = "strafesnet" } strafesnet_session = { path = "../engine/session", registry = "strafesnet" }
strafesnet_settings = { path = "../engine/settings", registry = "strafesnet" } strafesnet_settings = { path = "../engine/settings", registry = "strafesnet" }
strafesnet_snf = { path = "../lib/snf", registry = "strafesnet", optional = true } strafesnet_snf = { path = "../lib/snf", registry = "strafesnet", optional = true }
wgpu = "25.0.0" wgpu = "24.0.0"
winit = "0.30.7" winit = "0.30.7"

@ -2,12 +2,14 @@ pub type QNWorker<'a,Task>=CompatNWorker<'a,Task>;
pub type INWorker<'a,Task>=CompatNWorker<'a,Task>; pub type INWorker<'a,Task>=CompatNWorker<'a,Task>;
pub struct CompatNWorker<'a,Task>{ pub struct CompatNWorker<'a,Task>{
data:std::marker::PhantomData<Task>,
f:Box<dyn FnMut(Task)+Send+'a>, f:Box<dyn FnMut(Task)+Send+'a>,
} }
impl<'a,Task> CompatNWorker<'a,Task>{ impl<'a,Task> CompatNWorker<'a,Task>{
pub fn new(f:impl FnMut(Task)+Send+'a)->CompatNWorker<'a,Task>{ pub fn new(f:impl FnMut(Task)+Send+'a)->CompatNWorker<'a,Task>{
Self{ Self{
data:std::marker::PhantomData,
f:Box::new(f), f:Box::new(f),
} }
} }

@ -97,16 +97,11 @@ pub fn load<P:AsRef<std::path::Path>>(path:P)->Result<LoadFormat,LoadError>{
ReadFormat::SNFM(map)=>Ok(LoadFormat::Map(map)), ReadFormat::SNFM(map)=>Ok(LoadFormat::Map(map)),
#[cfg(feature="roblox")] #[cfg(feature="roblox")]
ReadFormat::Roblox(model)=>{ ReadFormat::Roblox(model)=>{
let mut place=strafesnet_rbx_loader::Place::from(model); let mut place=model.into_place();
let script_errors=place.run_scripts().unwrap(); place.run_scripts();
for error in script_errors{ Ok(LoadFormat::Map(
println!("Script error: {error}"); place.to_snf(LoadFailureMode::DefaultToNone).map_err(LoadError::LoadRoblox)?
} ))
let (map,errors)=place.to_snf(LoadFailureMode::DefaultToNone).map_err(LoadError::LoadRoblox)?;
if errors.count()!=0{
print!("Errors encountered while loading the map:\n{}",errors);
}
Ok(LoadFormat::Map(map))
}, },
#[cfg(feature="source")] #[cfg(feature="source")]
ReadFormat::Source(bsp)=>Ok(LoadFormat::Map( ReadFormat::Source(bsp)=>Ok(LoadFormat::Map(

@ -117,6 +117,7 @@ impl<'a> SetupContextPartial3<'a>{
// Make sure we use the texture resolution limits from the adapter, so we can support images the size of the surface. // Make sure we use the texture resolution limits from the adapter, so we can support images the size of the surface.
let needed_limits=strafesnet_graphics::graphics::required_limits().using_resolution(self.adapter.limits()); let needed_limits=strafesnet_graphics::graphics::required_limits().using_resolution(self.adapter.limits());
let trace_dir=std::env::var("WGPU_TRACE");
let (device, queue)=pollster::block_on(self.adapter let (device, queue)=pollster::block_on(self.adapter
.request_device( .request_device(
&wgpu::DeviceDescriptor { &wgpu::DeviceDescriptor {
@ -124,8 +125,8 @@ impl<'a> SetupContextPartial3<'a>{
required_features: (optional_features & self.adapter.features()) | required_features, required_features: (optional_features & self.adapter.features()) | required_features,
required_limits: needed_limits, required_limits: needed_limits,
memory_hints:wgpu::MemoryHints::Performance, memory_hints:wgpu::MemoryHints::Performance,
trace: wgpu::Trace::Off,
}, },
trace_dir.ok().as_ref().map(std::path::Path::new),
)) ))
.expect("Unable to find a suitable GPU adapter!"); .expect("Unable to find a suitable GPU adapter!");

@ -1 +0,0 @@
mangohud ../target/release/strafe-client bhop_maps/5692098704.snfm "$@"

@ -1 +0,0 @@
RUST_BACKTRACE=1 mangohud ../target/debug/strafe-client "$@"