Compare commits

..

2 Commits

Author SHA1 Message Date
d1a5d342ee physics: hardcoded cylinder 2025-03-19 16:35:01 -07:00
9e0c1b3c12 physics: cylinder hitbox 2025-03-19 16:35:01 -07:00
67 changed files with 1896 additions and 3540 deletions

1053
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -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"

@ -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,7 @@
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;
enum Transition<M:MeshQuery>{ enum Transition<M:MeshQuery>{
Miss, Miss,
Next(FEV<M>,GigaTime), Next(FEV<M>,GigaTime),
@ -29,47 +27,6 @@ impl<M:MeshQuery> CrawlResult<M>{
} }
} }
// TODO: move predict_collision_face_out algorithm in here or something
/// 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> 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
@ -78,9 +35,9 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
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>>,
<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>{
//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 +50,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 +65,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;
} }
@ -119,11 +76,10 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
}, },
&FEV::Edge(edge_id)=>{ &FEV::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
@ -131,8 +87,8 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
//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 +99,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 +115,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 +128,19 @@ 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){ match self.next_transition(body_time,mesh,relative_body,time_limit){
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),
} }
} }

File diff suppressed because one or more lines are too long

@ -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,12 +857,6 @@ impl Default for PhysicsState{
} }
impl PhysicsState{ impl PhysicsState{
pub fn new_with_body(body:Body)->Self{
Self{
body,
..Self::default()
}
}
pub fn camera_body(&self)->Body{ pub fn camera_body(&self)->Body{
Body{ Body{
position:self.body.position+self.style.camera_offset, position:self.body.position+self.style.camera_offset,
@ -1012,30 +1000,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){
@ -1199,7 +1188,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 +1600,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 +1620,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 +1636,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{
@ -1716,7 +1692,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 +1902,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 +1915,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(){
@ -19,6 +13,40 @@ fn main(){
} }
} }
#[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"

@ -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{
@ -657,7 +656,7 @@ pub mod mat3{
} }
} }
#[derive(Clone,Copy,Debug,Default,Hash,Eq,PartialEq)] #[derive(Clone,Copy,Default,Hash,Eq,PartialEq)]
pub struct Planar64Affine3{ pub struct Planar64Affine3{
pub matrix3:Planar64Mat3,//includes scale above 1 pub matrix3:Planar64Mat3,//includes scale above 1
pub translation:Planar64Vec3, pub translation:Planar64Vec3,

@ -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 "$@"