Compare commits

...

159 Commits

Author SHA1 Message Date
c797c32151 print advance_time 2025-08-26 16:40:07 -07:00
95df720255 do not strafe tick 2025-08-26 16:40:07 -07:00
e34cb0db53 print instruction query and execute 2025-08-26 16:40:07 -07:00
64c98493eb mark collision end and culling 2025-08-26 16:40:07 -07:00
6b10a01a8c debug 2025-08-26 16:40:07 -07:00
0f39cdb89f fix bug 3, break everything else 2025-08-26 16:40:00 -07:00
cfd9550566 common: truncate vertex precision to 16 bits in MeshBuilder
The physics algorithm expects vertices to align exactly with faces.  Since the face normal is calculated via the cross product of vertex positions, this allows the face normals to be exact with respect to the vertex positions.
2025-08-26 16:09:15 -07:00
bd16720b5a rbx_loader: refactor mesh convert to use MeshBuilder 2025-08-26 15:55:55 -07:00
633f767d0f snf: disable demo code 2025-08-26 15:55:55 -07:00
602b63e953 fix lifetime lints 2025-08-26 15:55:55 -07:00
6abe622885 fix dead code lints 2025-08-26 15:55:55 -07:00
0ab23dde2b move dev config to strafe-client only 2025-08-26 13:26:14 -07:00
657a2530dc update deps 2025-08-19 21:59:25 -07:00
c4bd034928 update rbx_mesh with CSGMDL5 support 2025-07-23 23:26:36 -07:00
bbcdac8879 rbx_loader: fix dead code lints 2025-07-23 23:26:36 -07:00
38b3f3d7a3 use expect instead of allow 2025-07-19 02:25:55 -07:00
eb80c8b9b5 update deps 2025-06-12 04:56:48 -07:00
63714f190d update snf binrw 2025-05-26 15:36:49 -07:00
f50dfb9399 update rbx_mesh 2025-05-26 15:33:38 -07:00
9db39d2a62 physics: face crawler opti 2025-05-23 14:40:06 -07:00
9e2e1d9d4a it: add physics bug tests, use cargo test -- --ignored 2025-05-22 15:37:10 -07:00
6f1548403a it: split into modules 2025-05-22 13:41:43 -07:00
5f3e998b3d map-tool: v1.7.2 provide download cookie 2025-05-20 16:30:44 -07:00
2d8792be4f tools: add clarion shortcut 2025-05-20 15:52:41 -07:00
15d33eb49d map-tool: cookie arg, now required by roblox 2025-05-20 15:35:03 -07:00
156dacb838 map-tool: v1.7.1 rbx_loader error reports 2025-05-16 16:04:55 -07:00
a7f0e431cb snf: v0.3.1 update common 2025-05-16 15:58:25 -07:00
0ed3cb2adb bsp_loader: v0.3.1 update common 2025-05-16 15:56:19 -07:00
bac43eab66 rbx_loader: v0.7.0 error reporting 2025-05-16 15:55:56 -07:00
2a257236fd deferred_loader: v0.5.1 update common 2025-05-16 15:55:47 -07:00
8fe2c20635 common: v0.7.0 misc 2025-05-16 15:49:12 -07:00
2da7ccce7c fixed_wide: v0.2.1 impl Display for FixedFromFloatError 2025-05-16 15:45:21 -07:00
89e6d11630 update deps 2025-05-16 15:42:19 -07:00
6abb40b6d2 roblox_emulator: v0.5.1 2025-05-16 15:35:54 -07:00
4dcf06c44c rbx_loader: support Seat, VehicleSeat, SpawnLocation 2025-05-16 15:26:59 -07:00
0171a711d9 rbx_loader: skip terrain 2025-05-16 15:08:48 -07:00
3eb702eaea roblox_emulator: give terrain BasePart properties to dodge error 2025-05-16 15:08:48 -07:00
92878a4ae8 strafe-client: print error report 2025-05-16 15:08:48 -07:00
88dcf40d77 map-tool: print error report 2025-05-16 15:08:48 -07:00
df9fcb5b02 rbx_loader: impl Display for RecoverableErrors 2025-05-16 15:08:48 -07:00
b07064cc9d common: impl Display for ModeId 2025-05-16 15:08:43 -07:00
6e435e46ac fixed_wide: impl Display for FixedFromFloatError 2025-05-16 14:21:32 -07:00
ce0368590d roblox_emulator: unused error variant 2025-05-16 14:21:32 -07:00
f896e6cfff rbx_loader: report script errors 2025-05-16 14:21:32 -07:00
dcc91db6f7 rbx_loader: refactor to make RecoverableError report 2025-05-16 14:21:32 -07:00
b6d6878137 physics: body double clone/copy fixups 2025-05-14 18:15:26 -07:00
81c9e3470b physics: use Bounds 2025-05-14 18:15:26 -07:00
b45e02c487 integer: export Parity trait 2025-05-14 17:26:41 -07:00
8698ca4a7e integer: time bitshift operations 2025-05-14 17:26:41 -07:00
8f04953326 physics: simplify face crawler trait bound 2025-05-14 16:52:51 -07:00
ae81d8ceaf derive Debug for many structs 2025-05-14 16:52:51 -07:00
2ecaeb1615 physics: test_collision_small_mv 2025-05-14 13:51:14 -07:00
768cd4ad1a physics: deref can be coerced 2025-05-13 17:26:10 -07:00
708462441a physics: clean up PhysicsMesh generation 2025-05-13 16:08:51 -07:00
da3ab52fe0 physics: do not require complete_mesh as first submesh
This removes a silent assumption about the input meshes and moves the branching from submeshes() to complete_mesh()
2025-05-13 15:45:30 -07:00
20f3e79cde rbx_loader: anything that uses velocity property should not be a booster 2025-05-09 20:56:42 -07:00
217f7fd7c3 physics: recalculate acceleration in collision_{start|end}_intersect 2025-05-09 20:56:42 -07:00
1e4d98f386 physics: use scratch vector in vert_edges 2025-05-09 20:56:42 -07:00
71bce361e6 rbx_loader: default meshpart mesh to empty string 2025-05-08 16:02:22 -07:00
d36c184f7e rbx_loader: auto-scale union graphics to fit size 2025-05-08 15:33:37 -07:00
2c3f257f0e rbx_loader: move mesh size detection into mesh convert 2025-05-08 15:33:37 -07:00
9e7e115809 deferred_loader: load generic mesh 2025-05-08 15:31:32 -07:00
2ea60b07fe rbx_loader: default physics for unions 2025-05-08 15:31:32 -07:00
2fe884175e rbx_loader: export primitive cube info 2025-05-07 15:57:19 -07:00
9f570d0f3e rbx_loader: prepare union convert for default physics 2025-05-07 15:57:19 -07:00
6e6bafe719 use .entry() match in generate_models 2025-05-02 19:36:34 -07:00
5f0ddc2f28 stop using transmute and document unsafe replacement code 2025-04-27 15:16:57 -07:00
ba1c1ec8c6 roblox_emulator: move lifetime inside PhantomData, saving 8 bytes 2025-04-24 13:51:28 -07:00
67cafd8cbb update deps including Decal.TextureContent 2025-04-23 18:05:09 -07:00
ca88eb1cad roblox_emulator: convert numbers to string in Instance.__newindex 2025-04-23 17:28:08 -07:00
2bf34fd04c roblox_emulator: move coerce method onto CoerceEnum struct 2025-04-23 17:19:59 -07:00
b5da30fd9a silence some warnings 2025-04-23 16:07:28 -07:00
95fffbbf42 roblox_emulator: bump version 2025-04-23 16:04:03 -07:00
9a636d5b50 roblox_emulator: Terrain.FillCylinder 2025-04-23 15:52:46 -07:00
2c8954c2b4 roblox_emulator: add Instance.Destroy aliases 2025-04-23 15:52:46 -07:00
985a703811 roblox_emulator: add CFrame.new(nil) constructor 2025-04-23 15:43:59 -07:00
37dbe35222 roblox_emulator: coerce enums in Terrain functions 2025-04-23 15:41:31 -07:00
f0de24d161 roblox_emulator: more BrickColor constructors 2025-04-23 15:39:34 -07:00
7d5ff55803 roblox_emulator: add Terrain:FillBall stub 2025-04-23 15:27:24 -07:00
7fffe05751 roblox_emulator: Color3.FromRGB alias 2025-04-23 15:22:16 -07:00
2ac38efef6 roblox_emulator: ClickDetector.MouseClick stub 2025-04-23 15:03:10 -07:00
5881e593b2 roblox_emulator: implement Instance.GetFullName 2025-04-23 15:03:10 -07:00
64a4499544 roblox_emulator: export mlua error 2025-04-23 15:03:10 -07:00
fca9e1c325 roblox_emulator: implement Error trait for ServicesError 2025-04-23 15:03:10 -07:00
6a88003b09 roblox_emulator: CFrame.FromEulerAnglesXYZ aliases 2025-04-23 15:03:10 -07:00
08b358c192 roblox_emulator: Vector2 2025-04-23 14:19:18 -07:00
62d9bcff81 roblox_emulator: implement __newindex CFrame 2025-04-23 14:12:21 -07:00
08f9163605 roblox_emulator: Tween Create & Play stub 2025-04-23 14:07:52 -07:00
b507624d91 roblox_emulator: shove tick into task module 2025-04-23 13:56:47 -07:00
d8358ec25c roblox_emulator: create task module 2025-04-23 13:56:47 -07:00
e673a12beb roblox_emulator: support more Vector3.new argument variants 2025-04-23 13:56:47 -07:00
18269423a5 roblox_emulator: implement TweenInfo 2025-04-23 13:56:47 -07:00
1d82799400 roblox_emulator: implement CoerceEnum 2025-04-23 13:28:47 -07:00
3a9fdebb4d roblox_emulator: refactor Enums 2025-04-23 13:28:47 -07:00
f8ef17e3f5 roblox_emulator: TweenService:Create() stub 2025-04-23 12:23:35 -07:00
8b2f37d3d5 roblox_emulator: Sound:Play() stub 2025-04-23 12:23:21 -07:00
16dfe7524f roblox_emulator: deduplicate static_ustr 2025-04-23 12:11:02 -07:00
3d399635d7 roblox_emulator: refactor nil instances 2025-04-23 12:04:46 -07:00
8bfc201d1f roblox_emulator: fix EnumItems.GetEnumItems 2025-04-23 12:04:46 -07:00
bb8e131464 roblox_emulator: use extended instances 2025-04-23 11:42:11 -07:00
05bbe05979 use forked rbx-dom for extended instances 2025-04-23 11:17:16 -07:00
17a2199f36 roblox_emulator: add ScriptSignal:wait alias for :Wait 2025-04-23 02:12:31 -07:00
84db6503f9 roblox_emulator: implement EnumItem.__eq 2025-04-23 02:00:09 -07:00
5b5f356863 roblox_emulator: implement __index Enum 2025-04-23 01:54:18 -07:00
d07571519e roblox_emulator: remove EnumItem::new 2025-04-23 01:38:51 -07:00
6464343428 roblox_emulator: rename Enum stuff to somewhat match Roblox 2025-04-23 01:38:27 -07:00
534b45d7dd roblox_emulator: EnumItems:GetEnumItems & Enum.Name 2025-04-23 01:32:10 -07:00
3ff8ccf58d roblox_emulator: implement Instance event stubs 2025-04-23 01:17:08 -07:00
67d2a3e398 roblox_emulator: implement __newindex UDim2 2025-04-23 01:15:08 -07:00
c1f8a13888 roblox_emulator: implement Vector3.__mul 2025-04-23 01:15:08 -07:00
e11af82443 roblox_emulator: tweak From impls 2025-04-23 01:15:08 -07:00
5b1d5502d8 roblox_emulator: fixup CFrame 2025-04-23 01:01:30 -07:00
f48d86fe56 roblox_emulator: replace coerce_float32 with a special Number type 2025-04-23 01:01:30 -07:00
877964be1f roblox_emulator: implement case aliases for CFrame 2025-04-22 22:53:37 -07:00
62d983ef2a roblox_emulator: implement NumberRange 2025-04-22 22:49:57 -07:00
8b7a4ee8c1 roblox_emulator: fixup NumberSequence 2025-04-22 22:49:41 -07:00
7ebcfa7bc3 roblox_emulator: stub BasePart.Touched & TouchEnded 2025-04-22 22:29:36 -07:00
c98f53a151 roblox_emulator: implement IsAncestorOf & IsDescendantOf 2025-04-22 22:26:46 -07:00
94025b52e2 roblox_emulator: .parent alias to .Parent 2025-04-22 21:43:57 -07:00
213d2f3f6a roblox_emulator: Vector3.__sub + .Unit 2025-04-22 21:35:26 -07:00
438a22bd86 roblox_emulator: fixup CFrame 2025-04-22 21:33:02 -07:00
e6fb8437b3 roblox_emulator: implement __newindex Int32 2025-04-22 21:26:26 -07:00
8b6166f48e roblox_emulator: support Workspace & MaterialService via GetService 2025-04-22 21:26:26 -07:00
3f89f2c1c5 roblox_emulator: implement FindFirstChildWhichIsA 2025-04-22 21:19:24 -07:00
c6ebb179a1 roblox_emulator: use common function pointer for lazy user data 2025-04-22 21:06:18 -07:00
14fa450e3e roblox_emulator: implement Players service stub 2025-04-22 21:04:29 -07:00
448f40a5d8 roblox_emulator: stub Terrain.SetMaterialColor 2025-04-22 20:57:09 -07:00
4ecdcec17c roblox_emulator: implement Instance.Clone 2025-04-22 20:53:49 -07:00
261b88ada6 roblox_emulator: isA alias for IsA 2025-04-22 19:56:56 -07:00
5ea0a1b07d roblox_emulator: implement __newindex Instance 2025-04-22 19:42:54 -07:00
3b8d32913e roblox_emulator: implement __index Color3 2025-04-22 19:42:35 -07:00
3928431ac7 roblox_emulator: implement __index String 2025-04-22 19:29:06 -07:00
6ecf403a4f roblox_emulator: add :connect alias to ScriptSignal.Connect 2025-04-22 19:28:26 -07:00
1f7ee20c1e roblox_emulator: implement Stepped, Heartbeat for RunService 2025-04-22 19:22:01 -07:00
19c30c8701 roblox_emulator: implement :children alias for GetChildren 2025-04-22 19:18:46 -07:00
dabb25b3d3 roblox_emulator: use coerce_float32 in CFrame 2025-04-22 19:15:26 -07:00
78593200eb roblox_emulator: fixup Vector3 2025-04-22 19:15:04 -07:00
7649d30b55 roblox_emulator: do not take UserData 2025-04-22 19:11:24 -07:00
6af8dd3c1f roblox_emulator: implement upper case methods on Vector3 2025-04-22 18:52:55 -07:00
861a8afc47 roblox_emulator: remove Vector3 component setters 2025-04-22 18:52:24 -07:00
f0b2470039 roblox_emulator: implement :service alias for GetService 2025-04-22 18:44:39 -07:00
c0ad20d54c roblox_emulator: implement UDim & UDim2 2025-04-22 18:44:21 -07:00
9aea73e134 roblox_emulator: implement BrickColor 2025-04-22 18:22:48 -07:00
b34d3f89f9 roblox_emulator: implement __index bool 2025-04-22 18:12:47 -07:00
5600948d8a roblox_emulator: fix error message 2025-04-22 18:12:47 -07:00
d722bcb46f roblox_emulator: implement __newindex ContentId 2025-04-22 18:12:47 -07:00
081c95190f roblox_emulator: ignore disabled scripts and ModuleScripts 2025-04-22 18:02:58 -07:00
d1f4e2132f common: do a minute amount of error reporting 2025-04-22 17:47:07 -07:00
5317d96243 roblox_emulator: rename Instance constructors 2025-04-22 17:28:44 -07:00
8bb20ffe81 roblox_emulator: nil instances 2025-04-22 17:28:44 -07:00
03b16db10a roblox_emulator: use a type alias to minimize errors 2025-04-22 16:08:45 -07:00
f0d4915fba roblox_emulator: move services into Context 2025-04-22 15:31:56 -07:00
6e8da50b35 rbx_loader: tweak model read 2025-04-22 14:55:20 -07:00
615372aad5 rbx_loader: stronger Place & Model types 2025-04-22 14:55:20 -07:00
08d47b0f63 roblox_emulator: do not create ustr if there is going to be an error 2025-04-22 14:00:30 -07:00
08b5445838 roblox_emulator: fix subtle ustr bugs 2025-04-22 14:00:29 -07:00
fa07d16cf4 refactor some util functions to use new rbx-dom features 2025-04-18 11:17:03 -07:00
327688d79e rbx-dom: guard rail ustr footguns 2025-04-18 11:17:03 -07:00
1662d814ec rbx_loader: disable roblox_emulator run-service feature 2025-04-18 10:28:48 -07:00
75 changed files with 3347 additions and 1930 deletions

1343
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -953,6 +953,7 @@ impl GraphicsState{
}),
store:wgpu::StoreOp::Store,
},
depth_slice:None,
})],
depth_stencil_attachment:Some(wgpu::RenderPassDepthStencilAttachment{
view:&self.depth_view,

View File

@@ -18,6 +18,17 @@ 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>
where Time<T>:Copy,
@@ -50,6 +61,7 @@ impl<T> Body<T>
self.velocity+(self.acceleration*dt).map(|elem|elem.divide().clamp_1())
}
pub fn advance_time(&mut self,time:Time<T>){
println!("advance_time");
self.position=self.extrapolated_position(time);
self.velocity=self.extrapolated_velocity(time);
self.time=time;
@@ -91,6 +103,7 @@ impl<T> Body<T>
self.acceleration.map(|elem|(dt*elem).divide().clamp())+self.velocity
}
pub fn advance_time_ratio_dt(&mut self,dt:crate::model::GigaTime){
println!("advance_time_ratio_dt");
self.position=self.extrapolated_position_ratio_dt(dt);
self.velocity=self.extrapolated_velocity_ratio_dt(dt);
self.time+=dt.into();

View File

@@ -1,7 +1,10 @@
use crate::model::{GigaTime,FEV,MeshQuery,DirectedEdge};
use crate::model::{into_giga_time,GigaTime,FEV,MeshQuery,DirectedEdge};
use strafesnet_common::integer::{Fixed,Ratio,vec3::Vector3};
use crate::physics::{Time,Body};
use core::ops::Bound;
#[derive(Debug)]
enum Transition<M:MeshQuery>{
Miss,
Next(FEV<M>,GigaTime),
@@ -27,17 +30,60 @@ 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>
where
// This is hardcoded for MinkowskiMesh lol
M::Face:Copy,
M::Edge:Copy,
M::Vert:Copy,
M:std::fmt::Debug,
F:std::fmt::Display,
F:core::ops::Mul<Fixed<1,32>,Output=Fixed<4,128>>,
<F as core::ops::Mul<Fixed<1,32>>>::Output:core::iter::Sum,
<M as MeshQuery>::Offset:core::ops::Sub<<F as std::ops::Mul<Fixed<1,32>>>::Output>,
M::Offset:core::ops::Sub<<F as std::ops::Mul<Fixed<1,32>>>::Output>,
{
fn next_transition(&self,body_time:GigaTime,mesh:&M,body:&Body,mut best_time:GigaTime)->Transition<M>{
fn next_transition(&self,mesh:&M,body:&Body,lower_bound:Bound<GigaTime>,mut upper_bound:Bound<GigaTime>)->Transition<M>{
//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.
let mut best_transition=Transition::Miss;
@@ -47,11 +93,16 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
//n=face.normal d=face.dot
//n.a t^2+n.v t+n.p-d==0
let (n,d)=mesh.face_nd(face_id);
println!("Face n={} d={}",n,d);
//TODO: use higher precision 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)){
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
best_time=dt;
let low=low(&lower_bound,&dt);
let upp=upp(&dt,&upper_bound);
let into=n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative();
println!("dt={} low={low} upp={upp} into={into}",dt.divide());
if low&&upp&&into{
upper_bound=Bound::Included(dt);
best_transition=Transition::Hit(face_id,dt);
break;
}
@@ -65,8 +116,8 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
//WARNING: precision is swept under the rug!
//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()){
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
best_time=dt;
if low(&lower_bound,&dt)&&upp(&dt,&upper_bound)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
upper_bound=Bound::Included(dt);
best_transition=Transition::Next(FEV::Edge(directed_edge_id.as_undirected()),dt);
break;
}
@@ -76,19 +127,26 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
},
&FEV::Edge(edge_id)=>{
//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 &[ev0,ev1]=edge_verts.as_ref();
let delta_pos=body.position*2-(mesh.vert(ev0)+mesh.vert(ev1));
let (v0,v1)=(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(){
let face_n=mesh.face_nd(edge_face_id).0;
//edge_n gets parity from the order of edge_faces
let n=face_n.cross(edge_n)*((i as i64)*2-1);
let d=n.dot(delta_pos).wrap_4();
println!("Edge Face={:?} boundary_n={} boundary_d={}",edge_face_id,n,d>>1);
//WARNING yada yada d *2
//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()){
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
best_time=dt;
for dt in Fixed::<4,128>::zeroes2(d,n.dot(body.velocity).wrap_4()*2,n.dot(body.acceleration).wrap_4()){
let low=low(&lower_bound,&dt);
let upp=upp(&dt,&upper_bound);
let into=n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative();
println!("dt={} low={low} upp={upp} into={into}",dt.divide());
if low&&upp&&into{
upper_bound=Bound::Included(dt);
best_transition=Transition::Next(FEV::Face(edge_face_id),dt);
break;
}
@@ -99,9 +157,9 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
//vertex normal gets parity from vert index
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)){
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
if low(&lower_bound,&dt)&&upp(&dt,&upper_bound)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
let dt=Ratio::new(dt.num.widen_4(),dt.den.widen_4());
best_time=dt;
upper_bound=Bound::Included(dt);
best_transition=Transition::Next(FEV::Vert(vert_id),dt);
break;
}
@@ -115,9 +173,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
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)){
if body_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
if low(&lower_bound,&dt)&&upp(&dt,&upper_bound)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
let dt=Ratio::new(dt.num.widen_4(),dt.den.widen_4());
best_time=dt;
upper_bound=Bound::Included(dt);
best_transition=Transition::Next(FEV::Edge(directed_edge_id.as_undirected()),dt);
break;
}
@@ -128,19 +186,16 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
}
best_transition
}
pub fn crawl(mut self,mesh:&M,relative_body:&Body,start_time:Time,time_limit:Time)->CrawlResult<M>{
let mut 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())
};
pub fn crawl(mut self,mesh:&M,relative_body:&Body,lower_bound:Bound<&Time>,upper_bound:Bound<&Time>)->CrawlResult<M>{
let mut lower_bound=lower_bound.map(|&t|into_giga_time(t,relative_body.time));
let upper_bound=upper_bound.map(|&t|into_giga_time(t,relative_body.time));
println!("crawl begin={self:?}");
for _ in 0..20{
match self.next_transition(body_time,mesh,relative_body,time_limit){
let transition=self.next_transition(mesh,relative_body,lower_bound,upper_bound);
println!("transition={transition:?}");
match transition{
Transition::Miss=>return CrawlResult::Miss(self),
Transition::Next(next_fev,next_time)=>(self,body_time)=(next_fev,next_time),
Transition::Next(next_fev,next_time)=>(self,lower_bound)=(next_fev,Bound::Included(next_time)),
Transition::Hit(face,time)=>return CrawlResult::Hit(face,time),
}
}

View File

@@ -1,5 +1,5 @@
use std::collections::{HashSet,HashMap};
use core::ops::Range;
use core::ops::{Bound,RangeBounds};
use strafesnet_common::integer::vec3::Vector3;
use strafesnet_common::model::{self,MeshId,PolygonIter};
use strafesnet_common::integer::{self,vec3,Fixed,Planar64,Planar64Vec3,Ratio};
@@ -68,16 +68,17 @@ pub enum FEV<M:MeshQuery>{
}
//use Unit32 #[repr(C)] for map files
#[derive(Clone,Hash,Eq,PartialEq)]
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq)]
struct Face{
normal:Planar64Vec3,
dot:Planar64,
}
#[derive(Debug)]
struct Vert(Planar64Vec3);
pub trait MeshQuery{
type Face:Copy;
type Edge:Copy+DirectedEdge;
type Vert:Copy;
type Face:Copy+std::fmt::Debug;
type Edge:Copy+DirectedEdge+std::fmt::Debug;
type Vert:Copy+std::fmt::Debug;
// Vertex must be Planar64Vec3 because it represents an actual position
type Normal;
type Offset;
@@ -97,18 +98,22 @@ pub trait MeshQuery{
fn vert_edges(&self,vert_id:Self::Vert)->impl AsRef<[Self::Edge]>;
fn vert_faces(&self,vert_id:Self::Vert)->impl AsRef<[Self::Face]>;
}
#[derive(Debug)]
struct FaceRefs{
edges:Vec<SubmeshDirectedEdgeId>,
//verts:Vec<VertId>,
}
#[derive(Debug)]
struct EdgeRefs{
faces:[SubmeshFaceId;2],//left, right
verts:[SubmeshVertId;2],//bottom, top
}
#[derive(Debug)]
struct VertRefs{
faces:Vec<SubmeshFaceId>,
edges:Vec<SubmeshDirectedEdgeId>,
}
#[derive(Debug)]
pub struct PhysicsMeshData{
//this contains all real and virtual faces used in both the complete mesh and convex submeshes
//faces are sorted such that all faces that belong to the complete mesh appear first, and then
@@ -118,6 +123,7 @@ pub struct PhysicsMeshData{
faces:Vec<Face>,//MeshFaceId indexes this list
verts:Vec<Vert>,//MeshVertId indexes this list
}
#[derive(Debug)]
pub struct PhysicsMeshTopology{
//mapping of local ids to PhysicsMeshData ids
faces:Vec<MeshFaceId>,//SubmeshFaceId indexes this list
@@ -143,10 +149,12 @@ impl From<MeshId> for PhysicsMeshId{
pub struct PhysicsSubmeshId(u32);
pub struct PhysicsMesh{
data:PhysicsMeshData,
complete_mesh:PhysicsMeshTopology,
//Most objects in roblox maps are already convex, so the list length is 0
//as soon as the mesh is divided into 2 submeshes, the list length jumps to 2.
//length 1 is unnecessary since the complete mesh would be a duplicate of the only submesh, but would still function properly
// The complete mesh is unused at this time.
// complete_mesh:PhysicsMeshTopology,
// Submeshes are guaranteed to be convex and may contain
// "virtual" faces which are not part of the complete mesh.
// Physics calculations should never resolve to hitting
// a virtual face.
submeshes:Vec<PhysicsMeshTopology>,
}
impl PhysicsMesh{
@@ -210,19 +218,24 @@ impl PhysicsMesh{
};
Self{
data,
complete_mesh:mesh_topology,
submeshes:Vec::new(),
// complete_mesh:mesh_topology.clone(),
submeshes:vec![mesh_topology],
}
}
pub fn unit_cylinder()->Self{
Self::unit_cube()
}
#[inline]
pub const fn complete_mesh(&self)->&PhysicsMeshTopology{
&self.complete_mesh
pub fn complete_mesh(&self)->&PhysicsMeshTopology{
// If there is exactly one submesh, then the complete mesh is identical to it.
if self.submeshes.len()==1{
self.submeshes.first().unwrap()
}else{
panic!("PhysicsMesh complete mesh is not known");
}
}
#[inline]
pub const fn complete_mesh_view(&self)->PhysicsMeshView{
pub fn complete_mesh_view(&self)->PhysicsMeshView<'_>{
PhysicsMeshView{
data:&self.data,
topology:self.complete_mesh(),
@@ -230,21 +243,16 @@ impl PhysicsMesh{
}
#[inline]
pub fn submeshes(&self)->&[PhysicsMeshTopology]{
//the complete mesh is already a convex mesh when len()==0, len()==1 is invalid but will still work
if self.submeshes.len()==0{
std::slice::from_ref(&self.complete_mesh)
}else{
&self.submeshes.as_slice()
}
&self.submeshes
}
#[inline]
pub fn submesh_view(&self,submesh_id:PhysicsSubmeshId)->PhysicsMeshView{
pub fn submesh_view(&self,submesh_id:PhysicsSubmeshId)->PhysicsMeshView<'_>{
PhysicsMeshView{
data:&self.data,
topology:&self.submeshes()[submesh_id.get() as usize],
}
}
pub fn submesh_views(&self)->impl Iterator<Item=PhysicsMeshView>{
pub fn submesh_views(&self)->impl Iterator<Item=PhysicsMeshView<'_>>{
self.submeshes().iter().map(|topology|PhysicsMeshView{
data:&self.data,
topology,
@@ -313,14 +321,20 @@ impl TryFrom<&model::Mesh> for PhysicsMesh{
if mesh.unique_pos.len()==0{
return Err(PhysicsMeshError::ZeroVertices);
}
// An empty physics mesh is a waste of resources
if mesh.physics_groups.len()==0{
return Err(PhysicsMeshError::NoPhysicsGroups);
}
let verts=mesh.unique_pos.iter().copied().map(Vert).collect();
//TODO: fix submeshes
//flat map mesh.physics_groups[$1].groups.polys()[$2] as face_id
//lower face_id points to upper face_id
//the same face is not allowed to be in multiple polygon groups
// because SubmeshFaceId -> CompleteMeshFaceId -> SubmeshFaceId is ambiguous
// when multiple SubmeshFaceId point to one MeshFaceId
let mut faces=Vec::new();
let mut face_id_from_face=HashMap::new();
let mut mesh_topologies:Vec<PhysicsMeshTopology>=mesh.physics_groups.iter().map(|physics_group|{
let mesh_topologies:Vec<PhysicsMeshTopology>=mesh.physics_groups.iter().map(|physics_group|{
//construct submesh
let mut submesh_faces=Vec::new();//these contain a map from submeshId->meshId
let mut submesh_verts=Vec::new();
@@ -381,15 +395,11 @@ impl TryFrom<&model::Mesh> for PhysicsMesh{
normal:(normal/len as i64).divide().narrow_1().unwrap(),
dot:(dot/(len*len) as i64).narrow_1().unwrap(),
};
let face_id=match face_id_from_face.get(&face){
Some(&face_id)=>face_id,
None=>{
let face_id=MeshFaceId::new(faces.len() as u32);
face_id_from_face.insert(face.clone(),face_id);
faces.push(face);
face_id
}
};
let face_id=*face_id_from_face.entry(face).or_insert_with(||{
let face_id=MeshFaceId::new(faces.len() as u32);
faces.push(face);
face_id
});
submesh_faces.push(face_id);
face_ref_guys.push(FaceRefEdges(face_edges));
}
@@ -397,16 +407,16 @@ impl TryFrom<&model::Mesh> for PhysicsMesh{
PhysicsMeshTopology{
faces:submesh_faces,
verts:submesh_verts,
face_topology:face_ref_guys.into_iter().map(|face_ref_guy|{
FaceRefs{edges:face_ref_guy.0}
face_topology:face_ref_guys.into_iter().map(|FaceRefEdges(edges)|{
FaceRefs{edges}
}).collect(),
edge_topology:edge_pool.edge_guys.into_iter().map(|(edge_ref_verts,edge_ref_faces)|
EdgeRefs{faces:edge_ref_faces.0,verts:edge_ref_verts.0}
edge_topology:edge_pool.edge_guys.into_iter().map(|(EdgeRefVerts(verts),EdgeRefFaces(faces))|
EdgeRefs{faces,verts}
).collect(),
vert_topology:vert_ref_guys.into_iter().map(|vert_ref_guy|
vert_topology:vert_ref_guys.into_iter().map(|VertRefGuy{edges,faces}|
VertRefs{
edges:vert_ref_guy.edges.into_iter().collect(),
faces:vert_ref_guy.faces.into_iter().collect(),
edges:edges.into_iter().collect(),
faces:faces.into_iter().collect(),
}
).collect(),
}
@@ -416,12 +426,13 @@ impl TryFrom<&model::Mesh> for PhysicsMesh{
faces,
verts,
},
complete_mesh:mesh_topologies.pop().ok_or(PhysicsMeshError::NoPhysicsGroups)?,
// complete_mesh:None,
submeshes:mesh_topologies,
})
}
}
#[derive(Debug)]
pub struct PhysicsMeshView<'a>{
data:&'a PhysicsMeshData,
topology:&'a PhysicsMeshTopology,
@@ -458,6 +469,7 @@ impl MeshQuery for PhysicsMeshView<'_>{
}
}
#[derive(Debug)]
pub struct PhysicsMeshTransform{
pub vertex:integer::Planar64Affine3,
pub normal:integer::mat3::Matrix3<Fixed<2,64>>,
@@ -473,6 +485,7 @@ impl PhysicsMeshTransform{
}
}
#[derive(Debug)]
pub struct TransformedMesh<'a>{
view:PhysicsMeshView<'a>,
transform:&'a PhysicsMeshTransform,
@@ -490,9 +503,6 @@ impl TransformedMesh<'_>{
pub fn verts<'a>(&'a self)->impl Iterator<Item=vec3::Vector3<Fixed<2,64>>>+'a{
self.view.data.verts.iter().map(|&Vert(pos)|self.transform.vertex.transform_point3(pos))
}
pub fn faces(&self)->impl Iterator<Item=SubmeshFaceId>{
(0..self.view.topology.faces.len() as u32).map(SubmeshFaceId::new)
}
fn farthest_vert(&self,dir:Planar64Vec3)->SubmeshVertId{
//this happens to be well-defined. there are no virtual virtices
SubmeshVertId::new(
@@ -598,6 +608,7 @@ pub enum MinkowskiFace{
//FaceFace
}
#[derive(Debug)]
pub struct MinkowskiMesh<'a>{
mesh0:TransformedMesh<'a>,
mesh1:TransformedMesh<'a>,
@@ -615,6 +626,10 @@ enum EV{
}
pub type GigaTime=Ratio<Fixed<4,128>,Fixed<4,128>>;
pub fn into_giga_time(time:Time,relative_to:Time)->GigaTime{
let r=(time-relative_to).to_ratio();
Ratio::new(r.num.widen_4(),r.den.widen_4())
}
impl MinkowskiMesh<'_>{
pub fn minkowski_sum<'a>(mesh0:TransformedMesh<'a>,mesh1:TransformedMesh<'a>)->MinkowskiMesh<'a>{
@@ -685,7 +700,7 @@ impl MinkowskiMesh<'_>{
}
}
/// This function drops a vertex down to an edge or a face if the path from infinity did not cross any vertex-edge boundaries but the point is supposed to have already crossed a boundary down from a vertex
fn infinity_fev(&self,infinity_dir:Planar64Vec3,point:Planar64Vec3)->FEV::<MinkowskiMesh>{
fn infinity_fev(&self,infinity_dir:Planar64Vec3,point:Planar64Vec3)->FEV::<MinkowskiMesh<'_>>{
//start on any vertex
//cross uncrossable vertex-edge boundaries until you find the closest vertex or edge
//cross edge-face boundary if it's uncrossable
@@ -730,44 +745,46 @@ impl MinkowskiMesh<'_>{
//
// Most of the calculation time is just calculating the starting point
// for the "actual" crawling algorithm below (predict_collision_{in|out}).
fn closest_fev_not_inside(&self,mut infinity_body:Body,start_time:Time)->Option<FEV<MinkowskiMesh>>{
fn closest_fev_not_inside(&self,mut infinity_body:Body,start_time:Bound<&Time>)->Option<FEV<MinkowskiMesh<'_>>>{
infinity_body.infinity_dir().and_then(|dir|{
let infinity_fev=self.infinity_fev(-dir,infinity_body.position);
//a line is simpler to solve than a parabola
infinity_body.velocity=dir;
infinity_body.acceleration=vec3::ZERO;
//crawl in from negative infinity along a tangent line to get the closest fev
// TODO: change crawl_fev args to delta time? Optional values?
infinity_fev.crawl(self,&infinity_body,Time::MIN/4,start_time).miss()
infinity_fev.crawl(self,&infinity_body,Bound::Unbounded,start_time).miss()
})
}
pub fn predict_collision_in(&self,relative_body:&Body,Range{start:start_time,end:time_limit}:Range<Time>)->Option<(MinkowskiFace,GigaTime)>{
self.closest_fev_not_inside(relative_body.clone(),start_time).and_then(|fev|{
pub fn predict_collision_in(&self,relative_body:&Body,range:impl RangeBounds<Time>)->Option<(MinkowskiFace,GigaTime)>{
println!("@@@BEGIN SETUP@@@");
self.closest_fev_not_inside(*relative_body,range.start_bound()).and_then(|fev|{
println!("@@@BEGIN REAL CRAWL@@@");
//continue forwards along the body parabola
fev.crawl(self,relative_body,start_time,time_limit).hit()
fev.crawl(self,relative_body,range.start_bound(),range.end_bound()).hit()
})
}
pub fn predict_collision_out(&self,relative_body:&Body,Range{start:start_time,end:time_limit}:Range<Time>)->Option<(MinkowskiFace,GigaTime)>{
//create an extrapolated body at time_limit
let infinity_body=-relative_body.clone();
self.closest_fev_not_inside(infinity_body,-time_limit).and_then(|fev|{
pub fn predict_collision_out(&self,relative_body:&Body,range:impl RangeBounds<Time>)->Option<(MinkowskiFace,GigaTime)>{
let (lower_bound,upper_bound)=(range.start_bound(),range.end_bound());
// swap and negate bounds to do a time inversion
let (lower_bound,upper_bound)=(upper_bound.map(|&t|-t),lower_bound.map(|&t|-t));
let infinity_body=-relative_body;
self.closest_fev_not_inside(infinity_body,lower_bound.as_ref()).and_then(|fev|{
//continue backwards along the body parabola
fev.crawl(self,&infinity_body,-time_limit,-start_time).hit()
fev.crawl(self,&infinity_body,lower_bound.as_ref(),upper_bound.as_ref()).hit()
//no need to test -time<time_limit because of the first step
.map(|(face,time)|(face,-time))
})
}
pub fn predict_collision_face_out(&self,relative_body:&Body,Range{start:start_time,end:time_limit}:Range<Time>,contact_face_id:MinkowskiFace)->Option<(MinkowskiEdge,GigaTime)>{
pub fn predict_collision_face_out(&self,relative_body:&Body,range:impl RangeBounds<Time>,contact_face_id:MinkowskiFace)->Option<(MinkowskiDirectedEdge,GigaTime)>{
// TODO: make better
use crate::face_crawler::{low,upp};
//no algorithm needed, there is only one state and two cases (Edge,None)
//determine when it passes an edge ("sliding off" case)
let start_time={
let r=(start_time-relative_body.time).to_ratio();
let start_time=range.start_bound().map(|&t|{
let r=(t-relative_body.time).to_ratio();
Ratio::new(r.num,r.den)
};
let mut best_time={
let r=(time_limit-relative_body.time).to_ratio();
Ratio::new(r.num.widen_4(),r.den.widen_4())
};
});
let mut best_time=range.end_bound().map(|&t|into_giga_time(t,relative_body.time));
let mut best_edge=None;
let face_n=self.face_nd(contact_face_id).0;
for &directed_edge_id in self.face_edges(contact_face_id).as_ref(){
@@ -780,18 +797,19 @@ impl MinkowskiMesh<'_>{
//WARNING: truncated precision
//wrap for speed
for dt in Fixed::<4,128>::zeroes2(((n.dot(relative_body.position))*2-d).wrap_4(),n.dot(relative_body.velocity).wrap_4()*2,n.dot(relative_body.acceleration).wrap_4()){
if start_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(relative_body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
best_time=dt;
best_edge=Some(directed_edge_id);
if low(&start_time,&dt)&&upp(&dt,&best_time)&&n.dot(relative_body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
best_time=Bound::Included(dt);
best_edge=Some((directed_edge_id,dt));
break;
}
}
}
best_edge.map(|e|(e.as_undirected(),best_time))
best_edge
}
fn infinity_in(&self,infinity_body:Body)->Option<(MinkowskiFace,GigaTime)>{
let infinity_fev=self.infinity_fev(-infinity_body.velocity,infinity_body.position);
infinity_fev.crawl(self,&infinity_body,Time::MIN/4,infinity_body.time).hit()
// Bound::Included means that the surface of the mesh is included in the mesh
infinity_fev.crawl(self,&infinity_body,Bound::Unbounded,Bound::Included(&infinity_body.time)).hit()
}
pub fn is_point_in_mesh(&self,point:Planar64Vec3)->bool{
let infinity_body=Body::new(point,vec3::Y,vec3::ZERO,Time::ZERO);
@@ -929,10 +947,10 @@ impl MeshQuery for MinkowskiMesh<'_>{
}
fn edge_verts(&self,edge_id:MinkowskiEdge)->impl AsRef<[MinkowskiVert;2]>{
AsRefHelper(match edge_id{
MinkowskiEdge::VertEdge(v0,e1)=>(*self.mesh1.edge_verts(e1).as_ref()).map(|vert_id1|
MinkowskiEdge::VertEdge(v0,e1)=>self.mesh1.edge_verts(e1).as_ref().map(|vert_id1|
MinkowskiVert::VertVert(v0,vert_id1)
),
MinkowskiEdge::EdgeVert(e0,v1)=>(*self.mesh0.edge_verts(e0).as_ref()).map(|vert_id0|
MinkowskiEdge::EdgeVert(e0,v1)=>self.mesh0.edge_verts(e0).as_ref().map(|vert_id0|
MinkowskiVert::VertVert(vert_id0,v1)
),
})
@@ -942,38 +960,43 @@ impl MeshQuery for MinkowskiMesh<'_>{
MinkowskiVert::VertVert(v0,v1)=>{
let mut edges=Vec::new();
//detect shared volume when the other mesh is mirrored along a test edge dir
let v0f=self.mesh0.vert_faces(v0);
let v1f=self.mesh1.vert_faces(v1);
let v0f_n:Vec<_>=v0f.as_ref().iter().map(|&face_id|self.mesh0.face_nd(face_id).0).collect();
let v1f_n:Vec<_>=v1f.as_ref().iter().map(|&face_id|self.mesh1.face_nd(face_id).0).collect();
let the_len=v0f.as_ref().len()+v1f.as_ref().len();
let v0f_thing=self.mesh0.vert_faces(v0);
let v1f_thing=self.mesh1.vert_faces(v1);
let v0f=v0f_thing.as_ref();
let v1f=v1f_thing.as_ref();
let v0f_n:Vec<_>=v0f.iter().map(|&face_id|self.mesh0.face_nd(face_id).0).collect();
let v1f_n:Vec<_>=v1f.iter().map(|&face_id|self.mesh1.face_nd(face_id).0).collect();
// scratch vector
let mut face_normals=Vec::with_capacity(v0f.len()+v1f.len());
face_normals.clone_from(&v0f_n);
for &directed_edge_id in self.mesh0.vert_edges(v0).as_ref(){
let n=self.mesh0.directed_edge_n(directed_edge_id);
let nn=n.dot(n);
// TODO: there's gotta be a better way to do this
//make a set of faces
let mut face_normals=Vec::with_capacity(the_len);
//add mesh0 faces as-is
face_normals.clone_from(&v0f_n);
// drop faces beyond v0f_n
face_normals.truncate(v0f.len());
// make a set of faces from mesh0's perspective
for face_n in &v1f_n{
//add reflected mesh1 faces
//wrap for speed
face_normals.push(*face_n-(n*face_n.dot(n)*2/nn).divide().wrap_3());
}
if is_empty_volume(face_normals){
if is_empty_volume(&face_normals){
edges.push(MinkowskiDirectedEdge::EdgeVert(directed_edge_id,v1));
}
}
face_normals.clone_from(&v1f_n);
for &directed_edge_id in self.mesh1.vert_edges(v1).as_ref(){
let n=self.mesh1.directed_edge_n(directed_edge_id);
let nn=n.dot(n);
let mut face_normals=Vec::with_capacity(the_len);
face_normals.clone_from(&v1f_n);
// drop faces beyond v1f_n
face_normals.truncate(v1f.len());
// make a set of faces from mesh1's perspective
for face_n in &v0f_n{
//wrap for speed
face_normals.push(*face_n-(n*face_n.dot(n)*2/nn).divide().wrap_3());
}
if is_empty_volume(face_normals){
if is_empty_volume(&face_normals){
edges.push(MinkowskiDirectedEdge::VertEdge(v0,directed_edge_id));
}
}
@@ -983,11 +1006,12 @@ impl MeshQuery for MinkowskiMesh<'_>{
}
fn vert_faces(&self,_vert_id:MinkowskiVert)->impl AsRef<[MinkowskiFace]>{
unimplemented!();
vec![]
#[expect(unreachable_code)]
Vec::new()
}
}
fn is_empty_volume(normals:Vec<Vector3<Fixed<3,96>>>)->bool{
fn is_empty_volume(normals:&[Vector3<Fixed<3,96>>])->bool{
let len=normals.len();
for i in 0..len-1{
for j in i+1..len{
@@ -1013,6 +1037,6 @@ fn is_empty_volume(normals:Vec<Vector3<Fixed<3,96>>>)->bool{
#[test]
fn test_is_empty_volume(){
assert!(!is_empty_volume([vec3::X.widen_3(),vec3::Y.widen_3(),vec3::Z.widen_3()].to_vec()));
assert!(is_empty_volume([vec3::X.widen_3(),vec3::Y.widen_3(),vec3::Z.widen_3(),vec3::NEG_X.widen_3()].to_vec()));
assert!(!is_empty_volume(&[vec3::X.widen_3(),vec3::Y.widen_3(),vec3::Z.widen_3()]));
assert!(is_empty_volume(&[vec3::X.widen_3(),vec3::Y.widen_3(),vec3::Z.widen_3(),vec3::NEG_X.widen_3()]));
}

View File

@@ -28,6 +28,7 @@ pub enum InternalInstruction{
CollisionStart(Collision,model_physics::GigaTime),
CollisionEnd(Collision,model_physics::GigaTime),
StrafeTick,
// TODO: add GigaTime to ReachWalkTargetVelocity
ReachWalkTargetVelocity,
// Water,
}
@@ -100,6 +101,7 @@ enum TransientAcceleration{
time:Time,
},
//walk target will never be reached
#[expect(dead_code)]
Unreachable{
acceleration:Planar64Vec3,
}
@@ -184,6 +186,7 @@ struct PhysicsModels{
intersect_attributes:HashMap<IntersectAttributesId,gameplay_attributes::IntersectAttributes>,
}
impl PhysicsModels{
#[expect(dead_code)]
fn clear(&mut self){
self.meshes.clear();
self.contact_models.clear();
@@ -191,7 +194,7 @@ impl PhysicsModels{
self.contact_attributes.clear();
self.intersect_attributes.clear();
}
fn mesh(&self,convex_mesh_id:ConvexMeshId)->TransformedMesh{
fn mesh(&self,convex_mesh_id:ConvexMeshId)->TransformedMesh<'_>{
let (mesh_id,transform)=match convex_mesh_id.model_id{
PhysicsModelId::Contact(model_id)=>{
let model=&self.contact_models[&model_id];
@@ -208,14 +211,14 @@ impl PhysicsModels{
)
}
//it's a bit weird to have three functions, but it's always going to be one of these
fn contact_mesh(&self,contact:&ContactCollision)->TransformedMesh{
fn contact_mesh(&self,contact:&ContactCollision)->TransformedMesh<'_>{
let model=&self.contact_models[&contact.model_id];
TransformedMesh::new(
self.meshes[&model.mesh_id].submesh_view(contact.submesh_id),
&model.transform
)
}
fn intersect_mesh(&self,intersect:&IntersectCollision)->TransformedMesh{
fn intersect_mesh(&self,intersect:&IntersectCollision)->TransformedMesh<'_>{
let model=&self.intersect_models[&intersect.model_id];
TransformedMesh::new(
self.meshes[&model.mesh_id].submesh_view(intersect.submesh_id),
@@ -284,6 +287,7 @@ impl PhysicsCamera{
fn rotation(&self)->Planar64Mat3{
self.get_rotation(self.clamped_mouse_pos)
}
#[expect(dead_code)]
fn simulate_move_rotation(&self,mouse_delta:glam::IVec2)->Planar64Mat3{
self.get_rotation(self.clamped_mouse_pos+mouse_delta)
}
@@ -351,6 +355,7 @@ mod gameplay{
pub fn unordered_checkpoint_count(&self)->u32{
self.unordered_checkpoints.len() as u32
}
#[expect(dead_code)]
pub fn set_mode_id(&mut self,mode_id:gameplay_modes::ModeId){
self.clear();
self.mode_id=mode_id;
@@ -417,7 +422,7 @@ impl HitboxMesh{
}
}
#[inline]
const fn transformed_mesh(&self)->TransformedMesh{
fn transformed_mesh(&self)->TransformedMesh<'_>{
TransformedMesh::new(self.mesh.complete_mesh_view(),&self.transform)
}
}
@@ -485,6 +490,7 @@ enum MoveState{
Air,
Walk(ContactMoveState),
Ladder(ContactMoveState),
#[expect(dead_code)]
Water,
Fly,
}
@@ -863,6 +869,12 @@ impl Default for PhysicsState{
}
impl PhysicsState{
pub fn new_with_body(body:Body)->Self{
Self{
body,
..Self::default()
}
}
pub fn camera_body(&self)->Body{
Body{
position:self.body.position+self.style.camera_offset,
@@ -1006,31 +1018,30 @@ impl PhysicsData{
let mut used_meshes=Vec::new();
let mut physics_mesh_id_from_model_mesh_id=HashMap::<MeshId,PhysicsMeshId>::new();
for (model_id,model) in map.models.iter().enumerate(){
//TODO: use .entry().or_insert_with(||{
let attr_id=if let Some(&attr_id)=physics_attr_id_from_model_attr_id.get(&model.attributes){
attr_id
}else{
//check if it's real
match map.attributes.get(model.attributes.get() as usize).and_then(|m_attr|{
PhysicsCollisionAttributes::try_from(m_attr).map_or(None,|p_attr|{
let attr_id=match p_attr{
PhysicsCollisionAttributes::Contact(attr)=>{
let attr_id=ContactAttributesId::new(used_contact_attributes.len() as u32);
used_contact_attributes.push(attr);
PhysicsAttributesId::Contact(attr_id)
},
PhysicsCollisionAttributes::Intersect(attr)=>{
let attr_id=IntersectAttributesId::new(used_intersect_attributes.len() as u32);
used_intersect_attributes.push(attr);
PhysicsAttributesId::Intersect(attr_id)
},
};
physics_attr_id_from_model_attr_id.insert(model.attributes,attr_id);
Some(attr_id)
})
}){
Some(attr_id)=>attr_id,
None=>continue,
let attr_id=match physics_attr_id_from_model_attr_id.entry(model.attributes){
std::collections::hash_map::Entry::Occupied(entry)=>*entry.get(),
std::collections::hash_map::Entry::Vacant(entry)=>{
//check if it's real
match map.attributes.get(model.attributes.get() as usize).and_then(|m_attr|{
PhysicsCollisionAttributes::try_from(m_attr).map_or(None,|p_attr|{
let attr_id=match p_attr{
PhysicsCollisionAttributes::Contact(attr)=>{
let attr_id=ContactAttributesId::new(used_contact_attributes.len() as u32);
used_contact_attributes.push(attr);
PhysicsAttributesId::Contact(attr_id)
},
PhysicsCollisionAttributes::Intersect(attr)=>{
let attr_id=IntersectAttributesId::new(used_intersect_attributes.len() as u32);
used_intersect_attributes.push(attr);
PhysicsAttributesId::Intersect(attr_id)
},
};
Some(*entry.insert(attr_id))
})
}){
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){
@@ -1125,10 +1136,11 @@ impl PhysicsData{
//this is the one who asks
fn next_instruction_internal(state:&PhysicsState,data:&PhysicsData,time_limit:Time)->Option<TimedInstruction<InternalInstruction,Time>>{
println!("==== next_instruction_internal ====");
//JUST POLLING!!! NO MUTATION
let mut collector=instruction::InstructionCollector::new(time_limit);
collector.collect(state.next_move_instruction());
// collector.collect(state.next_move_instruction());
//check for collision ends
state.touching.predict_collision_end(&mut collector,&data.models,&data.hitbox_mesh,&state.body,state.time);
@@ -1140,6 +1152,7 @@ impl PhysicsData{
//let relative_body=state.body.relative_to(&Body::ZERO);
let relative_body=&state.body;
data.bvh.sample_aabb(&aabb,&mut |&convex_mesh_id|{
println!("Sampling object id={:?}",convex_mesh_id.model_id);
//no checks are needed because of the time limits.
let model_mesh=data.models.mesh(convex_mesh_id);
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,data.hitbox_mesh.transformed_mesh());
@@ -1194,7 +1207,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)
}
while let Some(&intersect)=touching.intersects.iter().next(){
collision_end_intersect(touching,mode,run,models.intersect_attr(intersect.model_id),intersect,time);
collision_end_intersect(move_state,body,touching,models,hitbox_mesh,style,camera,input_state,mode,run,models.intersect_attr(intersect.model_id),intersect,time);
}
//find all models in the teleport region
let mut aabb=aabb::Aabb::default();
@@ -1255,6 +1268,7 @@ fn set_velocity_cull(body:&mut Body,touching:&mut TouchingState,models:&PhysicsM
let n=contact_normal(models,hitbox_mesh,contact);
let r=n.dot(v).is_positive();
if r{
println!("culled {:?}",contact.model_id);
culled=true;
}
!r
@@ -1265,6 +1279,7 @@ fn set_velocity_cull(body:&mut Body,touching:&mut TouchingState,models:&PhysicsM
fn set_velocity(body:&mut Body,touching:&TouchingState,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,v:Planar64Vec3){
body.velocity=touching.constrain_velocity(models,hitbox_mesh,v);
}
#[expect(dead_code)]
fn set_acceleration_cull(body:&mut Body,touching:&mut TouchingState,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,a:Planar64Vec3)->bool{
//This is not correct but is better than what I have
let mut culled=false;
@@ -1606,6 +1621,7 @@ fn collision_start_intersect(
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);
}
@@ -1621,6 +1637,7 @@ fn collision_end_contact(
_attr:&gameplay_attributes::ContactAttributes,
contact:ContactCollision,
){
println!("collision_end {:?}",contact.model_id);
touching.remove(&Collision::Contact(contact));//remove contact before calling contact_constrain_acceleration
//check ground
//TODO do better
@@ -1638,7 +1655,14 @@ fn collision_end_contact(
}
}
fn collision_end_intersect(
move_state:&mut MoveState,
body:&mut Body,
touching:&mut TouchingState,
models:&PhysicsModels,
hitbox_mesh:&HitboxMesh,
style:&StyleModifiers,
camera:&PhysicsCamera,
input_state:&InputState,
mode:Option<&gameplay_modes::Mode>,
run:&mut run::Run,
_attr:&gameplay_attributes::IntersectAttributes,
@@ -1646,6 +1670,7 @@ fn collision_end_intersect(
time:Time,
){
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{
let zone=mode.get_zone(intersect.model_id.into());
match zone{
@@ -1660,18 +1685,16 @@ fn collision_end_intersect(
}
}
fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedInstruction<InternalInstruction,Time>){
println!("\n==== atomic_internal_instruction ====");
state.time=ins.time;
let (should_advance_body,goober_time)=match ins.instruction{
match ins.instruction{
// collisions advance the body precisely
InternalInstruction::CollisionStart(_,dt)
|InternalInstruction::CollisionEnd(_,dt)=>(true,Some(dt)),
InternalInstruction::StrafeTick
|InternalInstruction::ReachWalkTargetVelocity=>(true,None),
};
if should_advance_body{
match goober_time{
Some(dt)=>state.body.advance_time_ratio_dt(dt),
None=>state.body.advance_time(state.time),
}
|InternalInstruction::CollisionEnd(_,dt)=>state.body.advance_time_ratio_dt(dt),
// this advances imprecisely
InternalInstruction::ReachWalkTargetVelocity=>state.body.advance_time(state.time),
// strafe tick decides for itself whether to advance the body.
InternalInstruction::StrafeTick=>(),
}
match ins.instruction{
InternalInstruction::CollisionStart(collision,_)=>{
@@ -1702,7 +1725,7 @@ fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:Tim
contact
),
Collision::Intersect(intersect)=>collision_end_intersect(
&mut state.touching,
&mut state.move_state,&mut state.body,&mut state.touching,&data.models,&data.hitbox_mesh,&state.style,&state.camera,&state.input_state,
data.modes.get_mode(state.mode_state.get_mode_id()),
&mut state.run,
data.models.intersect_attr(intersect.model_id),
@@ -1718,6 +1741,8 @@ fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:Tim
let masked_controls=strafe_settings.mask(controls);
let control_dir=state.style.get_control_dir(masked_controls);
if control_dir!=vec3::ZERO{
// manually advance time
state.body.advance_time(state.time);
let camera_mat=state.camera.simulate_move_rotation_y(state.input_state.lerp_delta(state.time).x);
if let Some(ticked_velocity)=strafe_settings.tick_velocity(state.body.velocity,(camera_mat*control_dir).with_length(Planar64::ONE).divide().wrap_1()){
//this is wrong but will work ig
@@ -1912,7 +1937,7 @@ mod test{
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>){
test_collision_axis_aligned(relative_body.clone(),expected_collision_time);
test_collision_axis_aligned(relative_body,expected_collision_time);
test_collision_rotated(relative_body,expected_collision_time);
}
#[test]
@@ -1925,6 +1950,15 @@ mod test{
),Some(Time::from_secs(2)));
}
#[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(){
test_collision(Body::new(
int3(3,5,0),

View File

@@ -192,7 +192,7 @@ fn get_push_ray_3(point:Planar64Vec3,c0:&Contact,c1:&Contact,c2:&Contact)->Optio
const fn get_best_push_ray_and_conts_0<'a>(point:Planar64Vec3)->(Ray,Conts<'a>){
(get_push_ray_0(point),Conts::new_const())
}
fn get_best_push_ray_and_conts_1(point:Planar64Vec3,c0:&Contact)->Option<(Ray,Conts)>{
fn get_best_push_ray_and_conts_1(point:Planar64Vec3,c0:&Contact)->Option<(Ray,Conts<'_>)>{
get_push_ray_1(point,c0)
.map(|ray|(ray,Conts::from_iter([c0])))
}

1
integration-testing/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/test_files

View File

@@ -0,0 +1,28 @@
#[expect(dead_code)]
#[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)
}
}

View File

@@ -1,7 +1,13 @@
use std::io::Cursor;
use std::path::Path;
mod error;
mod util;
#[cfg(test)]
mod tests;
use std::time::Instant;
use error::ReplayError;
use util::read_entire_file;
use strafesnet_physics::physics::{PhysicsData,PhysicsState,PhysicsContext};
fn main(){
@@ -13,40 +19,6 @@ 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>{
println!("loading map file..");
let data=read_entire_file("../tools/bhop_maps/5692113331.snfm")?;

View File

@@ -0,0 +1,78 @@
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(())
}

View File

@@ -0,0 +1,7 @@
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))
}

View File

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

View File

@@ -187,7 +187,7 @@ fn planes_to_faces(face_list:std::collections::HashSet<Face>)->Result<Faces,Plan
}
}
#[allow(dead_code)]
#[expect(dead_code)]
#[derive(Debug)]
pub enum BrushToMeshError{
SliceBrushSides,

View File

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

View File

@@ -5,7 +5,6 @@ use strafesnet_deferred_loader::{loader::Loader,texture::Texture};
use crate::{Bsp,Vpk};
#[allow(dead_code)]
#[derive(Debug)]
pub enum TextureError{
Io(std::io::Error),
@@ -41,7 +40,6 @@ impl Loader for TextureLoader{
}
}
#[allow(dead_code)]
#[derive(Debug)]
pub enum MeshError{
Io(std::io::Error),

View File

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

View File

@@ -140,6 +140,15 @@ impl ModeId{
pub const MAIN:Self=Self(0);
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)]
pub struct Mode{
style:gameplay_style::StyleModifiers,
@@ -440,11 +449,11 @@ impl ModesBuilder{
}
NormalizedModes::new(modes.into_iter().map(|mode_builder|NormalizedMode(mode_builder.mode)).collect())
}
pub fn insert_mode(&mut self,mode_id:ModeId,mode:Mode){
assert!(self.modes.insert(mode_id,mode).is_none(),"Cannot replace existing mode");
pub fn insert_mode(&mut self,mode_id:ModeId,mode:Mode)->Result<(),ExistingEntryError>{
error_if_exists(self.modes.insert(mode_id,mode))
}
pub fn insert_stage(&mut self,mode_id:ModeId,stage_id:StageId,stage:Stage){
assert!(self.stages.entry(mode_id).or_insert(HashMap::new()).insert(stage_id,stage).is_none(),"Cannot replace existing stage");
pub fn insert_stage(&mut self,mode_id:ModeId,stage_id:StageId,stage:Stage)->Result<(),ExistingEntryError>{
error_if_exists(self.stages.entry(mode_id).or_insert(HashMap::new()).insert(stage_id,stage))
}
pub fn push_mode_update(&mut self,mode_id:ModeId,mode_update:ModeUpdate){
self.mode_updates.push((mode_id,mode_update));
@@ -453,3 +462,12 @@ impl ModesBuilder{
// 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(())
}
}

View File

@@ -1,5 +1,5 @@
pub use fixed_wide::fixed::*;
pub use ratio_ops::ratio::{Ratio,Divide};
pub use ratio_ops::ratio::{Ratio,Divide,Parity};
//integer units
@@ -132,20 +132,21 @@ 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)))
}
}
impl<T> std::ops::Div<i64> for Time<T>{
type Output=Self;
#[inline]
fn div(self,rhs:i64)->Self::Output{
Self::raw(self.0/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)
macro_rules! impl_time_i64_rhs_operator {
($op:ident,$method:ident)=>{
impl<T> core::ops::$op<i64> for Time<T>{
type Output=Self;
#[inline]
fn $method(self,rhs:i64)->Self::Output{
Self::raw(self.0.$method(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{
type Output=Ratio<Fixed<2,64>,Planar64>;
fn mul(self,rhs:Time<T>)->Self::Output{
@@ -656,7 +657,7 @@ pub mod mat3{
}
}
#[derive(Clone,Copy,Default,Hash,Eq,PartialEq)]
#[derive(Clone,Copy,Debug,Default,Hash,Eq,PartialEq)]
pub struct Planar64Affine3{
pub matrix3:Planar64Mat3,//includes scale above 1
pub translation:Planar64Vec3,

View File

@@ -1,6 +1,6 @@
use std::collections::HashMap;
use crate::integer::{Planar64Vec3,Planar64Affine3};
use crate::integer::{Planar64,Planar64Vec3,Planar64Affine3};
use crate::gameplay_attributes;
pub type TextureCoordinate=glam::Vec2;
@@ -168,6 +168,11 @@ impl MeshBuilder{
}
}
pub fn acquire_pos_id(&mut self,pos:Planar64Vec3)->PositionId{
// Truncate the 16 most precise bits of the vertex positions.
// This allows the normal vectors to exactly represent the face.
// Remove this in Mesh V2
const MASK:Planar64=Planar64::raw(!((1<<16)-1));
let pos=pos.map(|c|c&MASK);
*self.pos_id_from.entry(pos).or_insert_with(||{
let pos_id=PositionId::new(self.unique_pos.len() as u32);
self.unique_pos.push(pos);

View File

@@ -1,6 +1,6 @@
[package]
name = "strafesnet_deferred_loader"
version = "0.5.0"
version = "0.5.1"
edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
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
[dependencies]
strafesnet_common = { version = "0.6.0", path = "../common", registry = "strafesnet" }
strafesnet_common = { version = "0.7.0", path = "../common", registry = "strafesnet" }

View File

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

View File

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

View File

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

View File

@@ -233,6 +233,11 @@ 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 {
( $decode:ident, $input: ty, $mantissa_bits:expr ) => {
impl<const N:usize,const F:usize> TryFrom<$input> for Fixed<N,F>{

View File

@@ -204,13 +204,19 @@ macro_rules! impl_matrix_named_fields_shape {
type Target=$struct_outer<Vector<$size_inner,T>>;
#[inline]
fn deref(&self)->&Self::Target{
unsafe{core::mem::transmute(&self.array)}
// This cast is valid because Matrix has #[repr(transparent)]
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>{
#[inline]
fn deref_mut(&mut self)->&mut Self::Target{
unsafe{core::mem::transmute(&mut self.array)}
// This cast is valid because Matrix has #[repr(transparent)]
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}
}
}
}

View File

@@ -330,13 +330,19 @@ macro_rules! impl_vector_named_fields {
type Target=$struct<T>;
#[inline]
fn deref(&self)->&Self::Target{
unsafe{core::mem::transmute(&self.array)}
// This cast is valid because Vector has #[repr(transparent)]
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>{
#[inline]
fn deref_mut(&mut self)->&mut Self::Target{
unsafe{core::mem::transmute(&mut self.array)}
// This cast is valid because Vector has #[repr(transparent)]
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}
}
}
}

View File

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

245
lib/rbx_loader/src/error.rs Normal file
View File

@@ -0,0 +1,245 @@
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())
}
}

View File

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

View File

@@ -1,5 +1,4 @@
use std::io::Read;
use rbx_dom_weak::ustr;
use rbxassetid::{RobloxAssetId,RobloxAssetIdParseErr};
use strafesnet_common::model::Mesh;
use strafesnet_deferred_loader::{loader::Loader,texture::Texture};
@@ -7,6 +6,11 @@ use strafesnet_deferred_loader::{loader::Loader,texture::Texture};
use crate::data::RobloxMeshBytes;
use crate::rbx::RobloxPartDescription;
// 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>{
let mut file=std::fs::File::open(path)?;
let mut data=Vec::new();
@@ -14,7 +18,6 @@ fn read_entire_file(path:impl AsRef<std::path::Path>)->Result<Vec<u8>,std::io::E
Ok(data)
}
#[allow(dead_code)]
#[derive(Debug)]
pub enum TextureError{
Io(std::io::Error),
@@ -55,7 +58,6 @@ impl Loader for TextureLoader{
}
}
#[allow(dead_code)]
#[derive(Debug)]
pub enum MeshError{
Io(std::io::Error),
@@ -114,7 +116,7 @@ pub struct MeshIndex<'a>{
content:&'a str,
}
impl MeshIndex<'_>{
pub fn file_mesh(content:&str)->MeshIndex{
pub fn file_mesh(content:&str)->MeshIndex<'_>{
MeshIndex{
mesh_type:MeshType::FileMesh,
content,
@@ -139,6 +141,12 @@ impl MeshIndex<'_>{
}
}
#[derive(Clone)]
pub struct MeshWithSize{
pub(crate) mesh:Mesh,
pub(crate) size:strafesnet_common::integer::Planar64Vec3,
}
pub struct MeshLoader;
impl MeshLoader{
pub fn new()->Self{
@@ -148,7 +156,7 @@ impl MeshLoader{
impl Loader for MeshLoader{
type Error=MeshError;
type Index<'a>=MeshIndex<'a>;
type Resource=Mesh;
type Resource=MeshWithSize;
fn load<'a>(&mut self,index:Self::Index<'a>)->Result<Self::Resource,Self::Error>{
let mesh=match index.mesh_type{
MeshType::FileMesh=>{
@@ -172,12 +180,12 @@ impl Loader for MeshLoader{
return Err(MeshError::MissingInstance);
};
if physics_data.is_empty(){
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get(&ustr("PhysicsData")){
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get(&static_ustr("PhysicsData")){
physics_data=data.as_ref();
}
}
if mesh_data.is_empty(){
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get(&ustr("MeshData")){
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get(&static_ustr("MeshData")){
mesh_data=data.as_ref();
}
}

View File

@@ -1,9 +1,12 @@
use std::collections::HashMap;
use rbx_mesh::mesh::{Vertex2,Vertex2Truncated};
use strafesnet_common::{integer::vec3,model::{self,ColorId,IndexedVertex,NormalId,PolygonGroup,PolygonList,PositionId,RenderConfigId,TextureCoordinateId,VertexId}};
use strafesnet_common::aabb::Aabb;
use strafesnet_common::integer::vec3;
use strafesnet_common::model::{self,ColorId,IndexedVertex,PolygonGroup,PolygonList,RenderConfigId,VertexId};
use crate::loader::MeshWithSize;
#[allow(dead_code)]
#[derive(Debug)]
pub enum Error{
Planar64Vec3(strafesnet_common::integer::Planar64TryFromFloatError),
@@ -16,69 +19,44 @@ impl std::fmt::Display for Error{
}
impl std::error::Error for Error{}
fn ingest_vertices2<
AcquirePosId,
AcquireTexId,
AcquireNormalId,
AcquireColorId,
AcquireVertexId,
>(
fn ingest_vertices2(
vertices:Vec<Vertex2>,
acquire_pos_id:&mut AcquirePosId,
acquire_tex_id:&mut AcquireTexId,
acquire_normal_id:&mut AcquireNormalId,
acquire_color_id:&mut AcquireColorId,
acquire_vertex_id:&mut AcquireVertexId,
)->Result<HashMap<rbx_mesh::mesh::VertexId2,VertexId>,Error>
where
AcquirePosId:FnMut([f32;3])->Result<PositionId,Error>,
AcquireTexId:FnMut([f32;2])->TextureCoordinateId,
AcquireNormalId:FnMut([f32;3])->Result<NormalId,Error>,
AcquireColorId:FnMut([f32;4])->ColorId,
AcquireVertexId:FnMut(IndexedVertex)->VertexId,
{
mb:&mut model::MeshBuilder,
)->Result<HashMap<rbx_mesh::mesh::VertexId2,VertexId>,Error>{
//this monster is collecting a map of old_vertices_index -> unique_vertices_index
//while also doing the inserting unique entries into lists simultaneously
Ok(vertices.into_iter().enumerate().map(|(vertex_id,vertex)|Ok((
rbx_mesh::mesh::VertexId2(vertex_id as u32),
acquire_vertex_id(IndexedVertex{
pos:acquire_pos_id(vertex.pos)?,
tex:acquire_tex_id(vertex.tex),
normal:acquire_normal_id(vertex.norm)?,
color:acquire_color_id(vertex.color.map(|f|f as f32/255.0f32))
}),
))).collect::<Result<_,_>>()?)
{
let vertex=IndexedVertex{
pos:mb.acquire_pos_id(vec3::try_from_f32_array(vertex.pos)?),
tex:mb.acquire_tex_id(glam::Vec2::from_array(vertex.tex)),
normal:mb.acquire_normal_id(vec3::try_from_f32_array(vertex.norm)?),
color:mb.acquire_color_id(glam::Vec4::from_array(vertex.color.map(|f|f as f32/255.0f32)))
};
mb.acquire_vertex_id(vertex)
}
))).collect::<Result<_,_>>().map_err(Error::Planar64Vec3)?)
}
fn ingest_vertices_truncated2<
AcquirePosId,
AcquireTexId,
AcquireNormalId,
AcquireVertexId,
>(
fn ingest_vertices_truncated2(
vertices:Vec<Vertex2Truncated>,
acquire_pos_id:&mut AcquirePosId,
acquire_tex_id:&mut AcquireTexId,
acquire_normal_id:&mut AcquireNormalId,
mb:&mut model::MeshBuilder,
static_color_id:ColorId,//pick one color and fill everything with it
acquire_vertex_id:&mut AcquireVertexId,
)->Result<HashMap<rbx_mesh::mesh::VertexId2,VertexId>,Error>
where
AcquirePosId:FnMut([f32;3])->Result<PositionId,Error>,
AcquireTexId:FnMut([f32;2])->TextureCoordinateId,
AcquireNormalId:FnMut([f32;3])->Result<NormalId,Error>,
AcquireVertexId:FnMut(IndexedVertex)->VertexId,
{
)->Result<HashMap<rbx_mesh::mesh::VertexId2,VertexId>,Error>{
//this monster is collecting a map of old_vertices_index -> unique_vertices_index
//while also doing the inserting unique entries into lists simultaneously
Ok(vertices.into_iter().enumerate().map(|(vertex_id,vertex)|Ok((
rbx_mesh::mesh::VertexId2(vertex_id as u32),
acquire_vertex_id(IndexedVertex{
pos:acquire_pos_id(vertex.pos)?,
tex:acquire_tex_id(vertex.tex),
normal:acquire_normal_id(vertex.norm)?,
color:static_color_id
}),
))).collect::<Result<_,_>>()?)
{
let vertex=IndexedVertex{
pos:mb.acquire_pos_id(vec3::try_from_f32_array(vertex.pos)?),
tex:mb.acquire_tex_id(glam::Vec2::from_array(vertex.tex)),
normal:mb.acquire_normal_id(vec3::try_from_f32_array(vertex.norm)?),
color:static_color_id,
};
mb.acquire_vertex_id(vertex)
}
))).collect::<Result<_,_>>().map_err(Error::Planar64Vec3)?)
}
fn ingest_faces2_lods3(
@@ -95,123 +73,74 @@ fn ingest_faces2_lods3(
))
}
pub fn convert(roblox_mesh_bytes:crate::data::RobloxMeshBytes)->Result<model::Mesh,Error>{
pub fn convert(roblox_mesh_bytes:crate::data::RobloxMeshBytes)->Result<MeshWithSize,Error>{
//generate that mesh boi
let mut unique_pos=Vec::new();
let mut pos_id_from=HashMap::new();
let mut unique_tex=Vec::new();
let mut tex_id_from=HashMap::new();
let mut unique_normal=Vec::new();
let mut normal_id_from=HashMap::new();
let mut unique_color=Vec::new();
let mut color_id_from=HashMap::new();
let mut unique_vertices=Vec::new();
let mut vertex_id_from=HashMap::new();
let mut polygon_groups=Vec::new();
let mut acquire_pos_id=|pos|{
let p=vec3::try_from_f32_array(pos).map_err(Error::Planar64Vec3)?;
Ok(PositionId::new(*pos_id_from.entry(p).or_insert_with(||{
let pos_id=unique_pos.len();
unique_pos.push(p);
pos_id
}) as u32))
};
let mut acquire_tex_id=|tex|{
let h=bytemuck::cast::<[f32;2],[u32;2]>(tex);
TextureCoordinateId::new(*tex_id_from.entry(h).or_insert_with(||{
let tex_id=unique_tex.len();
unique_tex.push(glam::Vec2::from_array(tex));
tex_id
}) as u32)
};
let mut acquire_normal_id=|normal|{
let n=vec3::try_from_f32_array(normal).map_err(Error::Planar64Vec3)?;
Ok(NormalId::new(*normal_id_from.entry(n).or_insert_with(||{
let normal_id=unique_normal.len();
unique_normal.push(n);
normal_id
}) as u32))
};
let mut acquire_color_id=|color|{
let h=bytemuck::cast::<[f32;4],[u32;4]>(color);
ColorId::new(*color_id_from.entry(h).or_insert_with(||{
let color_id=unique_color.len();
unique_color.push(glam::Vec4::from_array(color));
color_id
}) as u32)
};
let mut acquire_vertex_id=|vertex:IndexedVertex|{
VertexId::new(*vertex_id_from.entry(vertex.clone()).or_insert_with(||{
let vertex_id=unique_vertices.len();
unique_vertices.push(vertex);
vertex_id
}) as u32)
};
let mut mb=model::MeshBuilder::new();
match rbx_mesh::read_versioned(roblox_mesh_bytes.cursor()).map_err(Error::RbxMesh)?{
rbx_mesh::mesh::VersionedMesh::Version1(mesh)=>{
let color_id=acquire_color_id([1.0f32;4]);
rbx_mesh::mesh::Mesh::V1(mesh)=>{
let color_id=mb.acquire_color_id(glam::Vec4::ONE);
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{
pos:acquire_pos_id(vertex.pos)?,
tex:acquire_tex_id([vertex.tex[0],vertex.tex[1]]),
normal:acquire_normal_id(vertex.norm)?,
color:color_id,
}));
let mut ingest_vertex1=|vertex:&rbx_mesh::mesh::Vertex1|{
let vertex=IndexedVertex{
pos:mb.acquire_pos_id(vec3::try_from_f32_array(vertex.pos)?),
tex:mb.acquire_tex_id(glam::vec2(vertex.tex[0],vertex.tex[1])),
normal:mb.acquire_normal_id(vec3::try_from_f32_array(vertex.norm)?),
color:color_id,
};
Ok(mb.acquire_vertex_id(vertex))
};
Ok(vec![ingest_vertex1(&trip[0])?,ingest_vertex1(&trip[1])?,ingest_vertex1(&trip[2])?])
}).collect::<Result<_,_>>()?)));
}).collect::<Result<_,_>>().map_err(Error::Planar64Vec3)?)));
},
rbx_mesh::mesh::VersionedMesh::Version2(mesh)=>{
rbx_mesh::mesh::Mesh::V2(mesh)=>{
let vertex_id_map=match mesh.header.sizeof_vertex{
rbx_mesh::mesh::SizeOfVertex2::Truncated=>{
//pick white and make all the vertices white
let color_id=acquire_color_id([1.0f32;4]);
ingest_vertices_truncated2(mesh.vertices_truncated,&mut acquire_pos_id,&mut acquire_tex_id,&mut acquire_normal_id,color_id,&mut acquire_vertex_id)
let color_id=mb.acquire_color_id(glam::Vec4::ONE);
ingest_vertices_truncated2(mesh.vertices_truncated,&mut mb,color_id)
},
rbx_mesh::mesh::SizeOfVertex2::Full=>ingest_vertices2(mesh.vertices,&mut acquire_pos_id,&mut acquire_tex_id,&mut acquire_normal_id,&mut acquire_color_id,&mut acquire_vertex_id),
rbx_mesh::mesh::SizeOfVertex2::Full=>ingest_vertices2(mesh.vertices,&mut mb),
}?;
//one big happy group for all the faces
polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(mesh.faces.into_iter().map(|face|
vec![vertex_id_map[&face.0],vertex_id_map[&face.1],vertex_id_map[&face.2]]
).collect())));
},
rbx_mesh::mesh::VersionedMesh::Version3(mesh)=>{
rbx_mesh::mesh::Mesh::V3(mesh)=>{
let vertex_id_map=match mesh.header.sizeof_vertex{
rbx_mesh::mesh::SizeOfVertex2::Truncated=>{
let color_id=acquire_color_id([1.0f32;4]);
ingest_vertices_truncated2(mesh.vertices_truncated,&mut acquire_pos_id,&mut acquire_tex_id,&mut acquire_normal_id,color_id,&mut acquire_vertex_id)
let color_id=mb.acquire_color_id(glam::Vec4::ONE);
ingest_vertices_truncated2(mesh.vertices_truncated,&mut mb,color_id)
},
rbx_mesh::mesh::SizeOfVertex2::Full=>ingest_vertices2(mesh.vertices,&mut acquire_pos_id,&mut acquire_tex_id,&mut acquire_normal_id,&mut acquire_color_id,&mut acquire_vertex_id),
rbx_mesh::mesh::SizeOfVertex2::Full=>ingest_vertices2(mesh.vertices,&mut mb),
}?;
ingest_faces2_lods3(&mut polygon_groups,&vertex_id_map,&mesh.faces,&mesh.lods);
},
rbx_mesh::mesh::VersionedMesh::Version4(mesh)=>{
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
)?;
rbx_mesh::mesh::Mesh::V4(mesh)=>{
let vertex_id_map=ingest_vertices2(mesh.vertices,&mut mb)?;
ingest_faces2_lods3(&mut polygon_groups,&vertex_id_map,&mesh.faces,&mesh.lods);
},
rbx_mesh::mesh::VersionedMesh::Version5(mesh)=>{
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
)?;
rbx_mesh::mesh::Mesh::V5(mesh)=>{
let vertex_id_map=ingest_vertices2(mesh.vertices,&mut mb)?;
ingest_faces2_lods3(&mut polygon_groups,&vertex_id_map,&mesh.faces,&mesh.lods);
},
}
Ok(model::Mesh{
unique_pos,
unique_normal,
unique_tex,
unique_color,
unique_vertices,
let mesh=mb.build(
polygon_groups,
//these should probably be moved to the model...
//but what if models want to use the same texture
graphics_groups:vec![model::IndexedGraphicsGroup{
vec![model::IndexedGraphicsGroup{
render:RenderConfigId::new(0),
//the lowest lod is highest quality
groups:vec![model::PolygonGroupId::new(0)]
}],
//disable physics
physics_groups:Vec::new(),
})
Vec::new(),
);
let mut aabb=Aabb::default();
for &point in &mesh.unique_pos{
aabb.grow(point);
}
Ok(MeshWithSize{mesh,size:aabb.size()})
}

View File

@@ -56,7 +56,7 @@ const CUBE_DEFAULT_TEXTURE_COORDS:[TextureCoordinate;4]=[
TextureCoordinate::new(1.0,1.0),
TextureCoordinate::new(0.0,1.0),
];
const CUBE_DEFAULT_VERTICES:[Planar64Vec3;8]=[
pub const CUBE_DEFAULT_VERTICES:[Planar64Vec3;8]=[
vec3::int(-1,-1, 1),//0 left bottom back
vec3::int( 1,-1, 1),//1 right bottom back
vec3::int( 1, 1, 1),//2 right top back
@@ -66,7 +66,7 @@ const CUBE_DEFAULT_VERTICES:[Planar64Vec3;8]=[
vec3::int( 1,-1,-1),//6 right bottom front
vec3::int(-1,-1,-1),//7 left bottom front
];
const CUBE_DEFAULT_NORMALS:[Planar64Vec3;6]=[
pub const CUBE_DEFAULT_NORMALS:[Planar64Vec3;6]=[
vec3::int( 1, 0, 0),//CubeFace::Right
vec3::int( 0, 1, 0),//CubeFace::Top
vec3::int( 0, 0, 1),//CubeFace::Back
@@ -121,8 +121,7 @@ impl FaceDescription{
}
}
}
pub fn unit_cube(CubeFaceDescription(face_descriptions):CubeFaceDescription)->Mesh{
const CUBE_DEFAULT_POLYS:[[[u32;2];4];6]=[
pub const CUBE_DEFAULT_POLYS:[[[u32;2];4];6]=[
// right (1, 0, 0)
[
[6,2],//[vertex,tex]
@@ -166,6 +165,7 @@ pub fn unit_cube(CubeFaceDescription(face_descriptions):CubeFaceDescription)->Me
[7,2],
],
];
pub fn unit_cube(CubeFaceDescription(face_descriptions):CubeFaceDescription)->Mesh{
let mut generated_pos=Vec::new();
let mut generated_tex=Vec::new();
let mut generated_normal=Vec::new();

View File

@@ -1,73 +1,67 @@
use std::collections::HashMap;
use crate::loader::MeshIndex;
use crate::error::{RecoverableErrors,CFrameError,CFrameErrorType,DuplicateStageError,InstancePath,NormalIdError,Planar64ConvertError,ParseIntContext,ShapeError};
use crate::loader::{MeshWithSize,MeshIndex};
use crate::primitives::{self,CubeFace,CubeFaceDescription,WedgeFaceDescription,CornerWedgeFaceDescription,FaceDescription,Primitives};
use rbx_dom_weak::ustr;
use strafesnet_common::aabb::Aabb;
use strafesnet_common::map;
use strafesnet_common::model;
use strafesnet_common::gameplay_modes::{NormalizedModes,Mode,ModeId,ModeUpdate,ModesBuilder,Stage,StageElement,StageElementBehaviour,StageId,Zone};
use strafesnet_common::gameplay_style;
use strafesnet_common::gameplay_attributes as attr;
use strafesnet_common::integer::{self,vec3,Planar64,Planar64Vec3,Planar64Mat3,Planar64Affine3};
use strafesnet_common::integer::{self,vec3,Planar64TryFromFloatError,Planar64,Planar64Vec3,Planar64Mat3,Planar64Affine3};
use strafesnet_common::model::RenderConfigId;
use strafesnet_deferred_loader::deferred_loader::{RenderConfigDeferredLoader,MeshDeferredLoader};
use strafesnet_deferred_loader::mesh::Meshes;
use strafesnet_deferred_loader::texture::{RenderConfigs,Texture};
fn class_is_a(class: &str, superclass: &str) -> bool {
if class==superclass {
return true
}
let class_descriptor=rbx_reflection_database::get().classes.get(class);
if let Some(descriptor) = &class_descriptor {
if let Some(class_super) = &descriptor.superclass {
return class_is_a(&class_super, superclass)
}
}
false
}
fn recursive_collect_superclass(objects: &mut std::vec::Vec<rbx_dom_weak::types::Ref>,dom: &rbx_dom_weak::WeakDom, instance: &rbx_dom_weak::Instance, superclass: &str){
let mut stack=vec![instance];
while let Some(item)=stack.pop(){
for &referent in item.children(){
if let Some(c)=dom.get_by_ref(referent){
if class_is_a(c.class.as_str(),superclass){
objects.push(c.referent());//copy ref
}
stack.push(c);
}
}
}
}
fn planar64_affine3_from_roblox(cf:&rbx_dom_weak::types::CFrame,size:&rbx_dom_weak::types::Vector3)->Planar64Affine3{
Planar64Affine3::new(
Planar64Mat3::from_cols([
vec3::try_from_f32_array([cf.orientation.x.x,cf.orientation.y.x,cf.orientation.z.x]).unwrap()
*integer::try_from_f32(size.x/2.0).unwrap(),
vec3::try_from_f32_array([cf.orientation.x.y,cf.orientation.y.y,cf.orientation.z.y]).unwrap()
*integer::try_from_f32(size.y/2.0).unwrap(),
vec3::try_from_f32_array([cf.orientation.x.z,cf.orientation.y.z,cf.orientation.z.z]).unwrap()
*integer::try_from_f32(size.z/2.0).unwrap(),
].map(|t|t.narrow_1().unwrap())),
vec3::try_from_f32_array([cf.position.x,cf.position.y,cf.position.z]).unwrap()
)
// disallow non-static lifetimes
fn static_ustr(s:&'static str)->rbx_dom_weak::Ustr{
rbx_dom_weak::ustr(s)
}
fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:model::ModelId,modes_builder:&mut ModesBuilder,wormhole_in_model_to_id:&mut HashMap<model::ModelId,u32>,wormhole_id_to_out_model:&mut HashMap<u32,model::ModelId>)->attr::CollisionAttributes{
fn planar64_affine3_from_roblox(cf:&rbx_dom_weak::types::CFrame,size:&rbx_dom_weak::types::Vector3)->Result<Planar64Affine3,Planar64TryFromFloatError>{
Ok(Planar64Affine3::new(
Planar64Mat3::from_cols([
(vec3::try_from_f32_array([cf.orientation.x.x,cf.orientation.y.x,cf.orientation.z.x])?
*integer::try_from_f32(size.x/2.0)?).narrow_1().unwrap(),//.map_err(Planar64ConvertError::Narrow)?
(vec3::try_from_f32_array([cf.orientation.x.y,cf.orientation.y.y,cf.orientation.z.y])?
*integer::try_from_f32(size.y/2.0)?).narrow_1().unwrap(),//.map_err(Planar64ConvertError::Narrow)?
(vec3::try_from_f32_array([cf.orientation.x.z,cf.orientation.y.z,cf.orientation.z.z])?
*integer::try_from_f32(size.z/2.0)?).narrow_1().unwrap(),//.map_err(Planar64ConvertError::Narrow)?
]),
vec3::try_from_f32_array([cf.position.x,cf.position.y,cf.position.z])?
))
}
enum GetAttributesError{
ModeIdParseInt(ParseIntContext),
DuplicateMode(ModeId),
StageIdParseInt(ParseIntContext),
DuplicateStage(DuplicateStageError),
WormholeOutIdParseInt(ParseIntContext),
DuplicateWormholeOut(u32),
WormholeInIdParseInt(ParseIntContext),
JumpLimitParseInt(ParseIntContext),
}
fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:model::ModelId,modes_builder:&mut ModesBuilder,wormhole_in_model_to_id:&mut HashMap<model::ModelId,u32>,wormhole_id_to_out_model:&mut HashMap<u32,model::ModelId>)->Result<attr::CollisionAttributes,GetAttributesError>{
let mut general=attr::GeneralAttributes::default();
let mut intersecting=attr::IntersectingAttributes::default();
let mut contacting=attr::ContactingAttributes::default();
let mut force_can_collide=can_collide;
let mut force_intersecting=false;
let mut allow_booster=true;
match name{
"Water"=>{
force_can_collide=false;
allow_booster=false;
//TODO: read stupid CustomPhysicalProperties
intersecting.water=Some(attr::IntersectingWater{density:Planar64::ONE,viscosity:Planar64::ONE/10,velocity});
},
"Accelerator"=>{
//although the new game supports collidable accelerators, this is a roblox compatability map loader
force_can_collide=false;
// Accelerator is not allowed to be booster in roblox
allow_booster=false;
general.accelerator=Some(attr::Accelerator{acceleration:velocity});
},
// "UnorderedCheckpoint"=>general.teleport_behaviour=Some(model::TeleportBehaviour::StageElement(attr::StageElement{
@@ -76,17 +70,21 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
// force:false,
// behaviour:model::StageElementBehaviour::Unordered
// })),
"SetVelocity"=>general.trajectory=Some(attr::SetTrajectory::Velocity(velocity)),
"SetVelocity"=>{
allow_booster=false;
general.trajectory=Some(attr::SetTrajectory::Velocity(velocity));
},
"MapStart"=>{
force_can_collide=false;
force_intersecting=true;
let mode_id=ModeId::MAIN;
modes_builder.insert_mode(
ModeId::MAIN,
mode_id,
Mode::empty(
gameplay_style::StyleModifiers::roblox_bhop(),
model_id
)
);
).map_err(|_|GetAttributesError::DuplicateMode(mode_id))?;
},
"MapFinish"=>{
force_can_collide=false;
@@ -126,26 +124,30 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
"BonusStart"=>{
force_can_collide=false;
force_intersecting=true;
let mode_id=ModeId::new(ParseIntContext::parse(&captures[2]).map_err(GetAttributesError::ModeIdParseInt)?);
modes_builder.insert_mode(
ModeId::new(captures[2].parse::<u32>().unwrap()),
mode_id,
Mode::empty(
gameplay_style::StyleModifiers::roblox_bhop(),
model_id
)
);
).map_err(|_|GetAttributesError::DuplicateMode(mode_id))?;
},
"WormholeOut"=>{
//the PhysicsModelId has to exist for it to be teleported to!
force_intersecting=true;
//this object is not special in strafe client, but the roblox mapping needs to be converted to model id
assert!(wormhole_id_to_out_model.insert(captures[2].parse::<u32>().unwrap(),model_id).is_none(),"Cannot have multiple WormholeOut with same id");
let wormhole_id=ParseIntContext::parse(&captures[2]).map_err(GetAttributesError::WormholeOutIdParseInt)?;
if wormhole_id_to_out_model.insert(wormhole_id,model_id).is_some(){
return Err(GetAttributesError::DuplicateWormholeOut(wormhole_id));
}
},
_=>(),
}
}else if let Some(captures)=lazy_regex::regex!(r"^(Force)?(Spawn|SpawnAt|Trigger|Teleport|Platform)(\d+)$")
.captures(other){
force_intersecting=true;
let stage_id=StageId::new(captures[3].parse::<u32>().unwrap());
let stage_id=StageId::new(ParseIntContext::parse(&captures[3]).map_err(GetAttributesError::StageIdParseInt)?);
let stage_element=StageElement::new(
//stage_id:
stage_id,
@@ -157,11 +159,12 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
//behaviour:
match &captures[2]{
"Spawn"=>{
let mode_id=ModeId::MAIN;
modes_builder.insert_stage(
ModeId::MAIN,
mode_id,
stage_id,
Stage::empty(model_id),
);
).map_err(|_|GetAttributesError::DuplicateStage(DuplicateStageError{mode_id,stage_id}))?;
//TODO: let denormalize handle this
StageElementBehaviour::SpawnAt
},
@@ -171,7 +174,7 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
"Trigger"=>{force_can_collide=false;StageElementBehaviour::Trigger},
"Teleport"=>{force_can_collide=false;StageElementBehaviour::Teleport},
"Platform"=>StageElementBehaviour::Platform,
_=>panic!("regex1[2] messed up bad"),
_=>unreachable!("regex1[2] messed up bad"),
},
None
);
@@ -194,30 +197,33 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
StageId::FIRST,
false,
StageElementBehaviour::Check,
Some(captures[2].parse::<u8>().unwrap())
Some(ParseIntContext::parse(&captures[2]).map_err(GetAttributesError::JumpLimitParseInt)?)
)
),
),
"WormholeIn"=>{
force_can_collide=false;
force_intersecting=true;
assert!(wormhole_in_model_to_id.insert(model_id,captures[2].parse::<u32>().unwrap()).is_none(),"Impossible");
let wormhole_id=ParseIntContext::parse(&captures[2]).map_err(GetAttributesError::WormholeInIdParseInt)?;
// It is impossible for two different objects to have the same model id
assert!(wormhole_in_model_to_id.insert(model_id,wormhole_id).is_none(),"Impossible");
},
_=>panic!("regex2[1] messed up bad"),
_=>unreachable!("regex2[1] messed up bad"),
}
}else if let Some(captures)=lazy_regex::regex!(r"^Bonus(Finish|Anticheat)(\d+)$")
.captures(other){
force_can_collide=false;
force_intersecting=true;
let mode_id=ModeId::new(ParseIntContext::parse(&captures[2]).map_err(GetAttributesError::ModeIdParseInt)?);
modes_builder.push_mode_update(
ModeId::new(captures[2].parse::<u32>().unwrap()),
mode_id,
ModeUpdate::zone(
model_id,
//zone:
match &captures[1]{
"Finish"=>Zone::Finish,
"Anticheat"=>Zone::Anticheat,
_=>panic!("regex3[1] messed up bad"),
_=>unreachable!("regex3[1] messed up bad"),
},
),
);
@@ -236,10 +242,10 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
}
}
//need some way to skip this
if velocity!=vec3::ZERO{
if allow_booster&&velocity!=vec3::ZERO{
general.booster=Some(attr::Booster::Velocity(velocity));
}
match force_can_collide{
Ok(match force_can_collide{
true=>{
match name{
"Bounce"=>contacting.contact_behaviour=Some(attr::ContactingBehaviour::Elastic(u32::MAX)),
@@ -257,7 +263,7 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
}else{
attr::CollisionAttributes::Decoration
},
}
})
}
#[derive(Clone,Copy)]
@@ -403,39 +409,46 @@ fn get_content_url(content:&rbx_dom_weak::types::Content)->Option<&str>{
}
fn get_texture_description<'a>(
temp_objects:&mut Vec<rbx_dom_weak::types::Ref>,
render_config_deferred_loader:&mut RenderConfigDeferredLoader<&'a str>,
recoverable_errors:&mut RecoverableErrors,
db:&rbx_reflection::ReflectionDatabase,
dom:&'a rbx_dom_weak::WeakDom,
object:&rbx_dom_weak::Instance,
size:&rbx_dom_weak::types::Vector3,
)->RobloxPartDescription{
//use the biggest one and cut it down later...
let mut part_texture_description=RobloxPartDescription::default();
temp_objects.clear();
recursive_collect_superclass(temp_objects,&dom,object,"Decal");
for &mut decal_ref in temp_objects{
let Some(decal)=dom.get_by_ref(decal_ref) else{
println!("Decal get_by_ref failed");
continue;
};
let decal=&db.classes["Decal"];
let decals=object.children().iter().filter_map(|&referent|{
let instance=dom.get_by_ref(referent)?;
db.classes.get(instance.class.as_str()).is_some_and(|class|
db.has_superclass(class,decal)
).then_some(instance)
});
for decal in decals{
// decals should always have these properties,
// but it is not guaranteed by the rbx_dom_weak data structure.
let (
Some(rbx_dom_weak::types::Variant::ContentId(content)),
Some(rbx_dom_weak::types::Variant::Content(content)),
Some(rbx_dom_weak::types::Variant::Enum(normalid)),
Some(rbx_dom_weak::types::Variant::Color3(decal_color3)),
Some(rbx_dom_weak::types::Variant::Float32(decal_transparency)),
)=(
decal.properties.get(&ustr("Texture")),
decal.properties.get(&ustr("Face")),
decal.properties.get(&ustr("Color3")),
decal.properties.get(&ustr("Transparency")),
decal.properties.get(&static_ustr("TextureContent")),
decal.properties.get(&static_ustr("Face")),
decal.properties.get(&static_ustr("Color3")),
decal.properties.get(&static_ustr("Transparency")),
)else{
println!("Decal is missing a required property");
recoverable_errors.decal_property.push(InstancePath::new(dom,decal));
continue;
};
let texture_id=Some(content.as_str());
let texture_id=get_content_url(content);
let render_id=render_config_deferred_loader.acquire_render_config_id(texture_id);
let Ok(cube_face)=normalid.to_u32().try_into()else{
println!("NormalId is invalid");
recoverable_errors.normal_id.push(NormalIdError{
path:InstancePath::new(dom,decal),
normal_id:normalid.to_u32(),
});
continue;
};
let (roblox_texture_color,roblox_texture_transform)=if decal.class=="Texture"{
@@ -446,10 +459,10 @@ fn get_texture_description<'a>(
Some(&rbx_dom_weak::types::Variant::Float32(studs_per_tile_u)),
Some(&rbx_dom_weak::types::Variant::Float32(studs_per_tile_v)),
) = (
decal.properties.get(&ustr("OffsetStudsU")),
decal.properties.get(&ustr("OffsetStudsV")),
decal.properties.get(&ustr("StudsPerTileU")),
decal.properties.get(&ustr("StudsPerTileV")),
decal.properties.get(&static_ustr("OffsetStudsU")),
decal.properties.get(&static_ustr("OffsetStudsV")),
decal.properties.get(&static_ustr("StudsPerTileU")),
decal.properties.get(&static_ustr("StudsPerTileV")),
)
{
let (size_u,size_v)=match cube_face{
@@ -472,6 +485,7 @@ fn get_texture_description<'a>(
}
)
}else{
recoverable_errors.texture_property.push(InstancePath::new(dom,decal));
(glam::Vec4::ONE,RobloxTextureTransform::identity())
}
}else{
@@ -525,6 +539,8 @@ pub fn convert<'a>(
render_config_deferred_loader:&mut RenderConfigDeferredLoader<&'a str>,
mesh_deferred_loader:&mut MeshDeferredLoader<MeshIndex<'a>>,
)->PartialMap1<'a>{
let mut recoverable_errors=RecoverableErrors::default();
let mut deferred_models_deferred_attributes=Vec::new();
let mut deferred_unions_deferred_attributes=Vec::new();
let mut primitive_models_deferred_attributes=Vec::new();
@@ -534,63 +550,83 @@ pub fn convert<'a>(
//just going to leave it like this for now instead of reworking the data structures for this whole thing
let textureless_render_group=render_config_deferred_loader.acquire_render_config_id(None);
let mut object_refs=Vec::new();
let mut temp_objects=Vec::new();
recursive_collect_superclass(&mut object_refs, &dom, dom.root(),"BasePart");
for object_ref in object_refs {
if let Some(object)=dom.get_by_ref(object_ref){
if let (
Some(rbx_dom_weak::types::Variant::CFrame(cf)),
Some(rbx_dom_weak::types::Variant::Vector3(size)),
Some(rbx_dom_weak::types::Variant::Vector3(velocity)),
Some(rbx_dom_weak::types::Variant::Float32(transparency)),
Some(rbx_dom_weak::types::Variant::Color3uint8(color3)),
Some(rbx_dom_weak::types::Variant::Bool(can_collide)),
) = (
object.properties.get(&ustr("CFrame")),
object.properties.get(&ustr("Size")),
object.properties.get(&ustr("Velocity")),
object.properties.get(&ustr("Transparency")),
object.properties.get(&ustr("Color")),
object.properties.get(&ustr("CanCollide")),
)
{
let model_transform=planar64_affine3_from_roblox(cf,size);
let db=rbx_reflection_database::get();
let basepart=&db.classes["BasePart"];
let baseparts=dom.descendants().filter(|&instance|
db.classes.get(instance.class.as_str()).is_some_and(|class|
db.has_superclass(class,basepart)
)
);
for object in baseparts{
let (
Some(rbx_dom_weak::types::Variant::CFrame(cf)),
Some(rbx_dom_weak::types::Variant::Vector3(size)),
Some(rbx_dom_weak::types::Variant::Vector3(velocity)),
Some(rbx_dom_weak::types::Variant::Float32(transparency)),
Some(rbx_dom_weak::types::Variant::Color3uint8(color3)),
Some(&rbx_dom_weak::types::Variant::Bool(can_collide)),
) = (
object.properties.get(&static_ustr("CFrame")),
object.properties.get(&static_ustr("Size")),
object.properties.get(&static_ustr("Velocity")),
object.properties.get(&static_ustr("Transparency")),
object.properties.get(&static_ustr("Color")),
object.properties.get(&static_ustr("CanCollide")),
)else{
recoverable_errors.basepart_property.push(InstancePath::new(dom,object));
continue;
};
let model_transform=match planar64_affine3_from_roblox(cf,size){
Ok(model_transform)=>{
if model_transform.matrix3.det().is_zero(){
let mut parent_ref=object.parent();
let mut full_path=object.name.clone();
while let Some(parent)=dom.get_by_ref(parent_ref){
full_path=format!("{}.{}",parent.name,full_path);
parent_ref=parent.parent();
}
println!("Zero determinant CFrame at location {}",full_path);
println!("matrix3:{}",model_transform.matrix3);
recoverable_errors.basepart_cframe.push(CFrameError{
path:InstancePath::new(dom,object),
error:CFrameErrorType::ZeroDeterminant,
});
continue;
}
model_transform
},
Err(e)=>{
recoverable_errors.basepart_cframe.push(CFrameError{
path:InstancePath::new(dom,object),
error:CFrameErrorType::Convert(e),
});
continue;
}
};
//TODO: also detect "CylinderMesh" etc here
let shape=match object.class.as_str(){
"Part"=>if let Some(rbx_dom_weak::types::Variant::Enum(shape))=object.properties.get(&ustr("Shape")){
Shape::Primitive(shape.to_u32().try_into().expect("Funky roblox PartType"))
}else{
panic!("Part has no Shape!");
},
"TrussPart"=>Shape::Primitive(Primitives::Cube),
"WedgePart"=>Shape::Primitive(Primitives::Wedge),
"CornerWedgePart"=>Shape::Primitive(Primitives::CornerWedge),
"MeshPart"=>Shape::MeshPart,
"UnionOperation"=>Shape::PhysicsData,
_=>{
println!("Unsupported BasePart ClassName={}; defaulting to cube",object.class);
Shape::Primitive(Primitives::Cube)
}
//TODO: also detect "CylinderMesh" etc here
let shape=match object.class.as_str(){
"Part"|"Seat"|"SpawnLocation"=>{
let Some(rbx_dom_weak::types::Variant::Enum(shape))=object.properties.get(&static_ustr("Shape"))else{
recoverable_errors.part_property.push(InstancePath::new(dom,object));
continue;
};
let Ok(shape)=shape.to_u32().try_into()else{
recoverable_errors.part_shape.push(ShapeError{
path:InstancePath::new(dom,object),
shape:shape.to_u32(),
});
continue;
};
Shape::Primitive(shape)
},
"TrussPart"|"VehicleSeat"=>Shape::Primitive(Primitives::Cube),
"WedgePart"=>Shape::Primitive(Primitives::Wedge),
"CornerWedgePart"=>Shape::Primitive(Primitives::CornerWedge),
"MeshPart"=>Shape::MeshPart,
"UnionOperation"=>Shape::PhysicsData,
"Terrain"=>continue,
_=>{
recoverable_errors.unsupported_class.insert(object.class.as_str().to_owned());
Shape::Primitive(Primitives::Cube)
}
};
let (availability,mesh_id)=match shape{
Shape::Primitive(primitive_shape)=>{
//TODO: TAB TAB
let part_texture_description=get_texture_description(&mut temp_objects,render_config_deferred_loader,dom,object,size);
let (availability,mesh_id)=match shape{
Shape::Primitive(primitive_shape)=>{
let part_texture_description=get_texture_description(render_config_deferred_loader,&mut recoverable_errors,db,dom,object,size);
//obscure rust syntax "slice pattern"
let RobloxPartDescription([
f0,//Cube::Right
@@ -639,66 +675,83 @@ pub fn convert<'a>(
mesh_id
};
(MeshAvailability::Immediate,mesh_id)
},
Shape::MeshPart=>if let (
Some(rbx_dom_weak::types::Variant::Content(mesh_content)),
Some(rbx_dom_weak::types::Variant::Content(texture_content)),
)=(
// mesh must exist
object.properties.get(&ustr("MeshContent")),
// texture is allowed to be none
object.properties.get(&ustr("TextureContent")),
){
let mesh_asset_id=get_content_url(mesh_content).expect("MeshPart Mesh is not a Uri");
let texture_asset_id=get_content_url(texture_content);
(
MeshAvailability::DeferredMesh(render_config_deferred_loader.acquire_render_config_id(texture_asset_id)),
mesh_deferred_loader.acquire_mesh_id(MeshIndex::file_mesh(mesh_asset_id)),
)
}else{
panic!("Mesh has no Mesh or Texture");
},
Shape::PhysicsData=>{
let mut content="";
let mut mesh_data:&[u8]=&[];
let mut physics_data:&[u8]=&[];
if let Some(rbx_dom_weak::types::Variant::ContentId(asset_id))=object.properties.get(&ustr("AssetId")){
content=asset_id.as_ref();
}
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=object.properties.get(&ustr("MeshData")){
mesh_data=data.as_ref();
}
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=object.properties.get(&ustr("PhysicsData")){
physics_data=data.as_ref();
}
let part_texture_description=get_texture_description(&mut temp_objects,render_config_deferred_loader,dom,object,size);
let mesh_index=MeshIndex::union(content,mesh_data,physics_data,size,part_texture_description.clone());
let mesh_id=mesh_deferred_loader.acquire_mesh_id(mesh_index);
(MeshAvailability::DeferredUnion(part_texture_description),mesh_id)
},
},
Shape::MeshPart=>{
let (
Some(rbx_dom_weak::types::Variant::Content(mesh_content)),
Some(rbx_dom_weak::types::Variant::Content(texture_content)),
)=(
// mesh must exist
object.properties.get(&static_ustr("MeshContent")),
// texture is allowed to be none
object.properties.get(&static_ustr("TextureContent")),
)else{
recoverable_errors.meshpart_property.push(InstancePath::new(dom,object));
continue;
};
let model_deferred_attributes=ModelDeferredAttributes{
mesh:mesh_id,
transform:model_transform,
color:glam::vec4(color3.r as f32/255f32, color3.g as f32/255f32, color3.b as f32/255f32, 1.0-*transparency),
deferred_attributes:GetAttributesArgs{
name:object.name.as_str(),
can_collide:*can_collide,
velocity:vec3::try_from_f32_array([velocity.x,velocity.y,velocity.z]).unwrap(),
},
let mesh_asset_id=match get_content_url(mesh_content){
Some(mesh_asset_id)=>mesh_asset_id,
None=>{
recoverable_errors.meshpart_content.push(InstancePath::new(dom,object));
// Return an empty string which will fail to parse as an asset id
""
}
};
match availability{
MeshAvailability::Immediate=>primitive_models_deferred_attributes.push(model_deferred_attributes),
MeshAvailability::DeferredMesh(render)=>deferred_models_deferred_attributes.push(DeferredModelDeferredAttributes{
render,
model:model_deferred_attributes
}),
MeshAvailability::DeferredUnion(part_texture_description)=>deferred_unions_deferred_attributes.push(DeferredUnionDeferredAttributes{
render:part_texture_description,
model:model_deferred_attributes,
}),
let texture_asset_id=get_content_url(texture_content);
(
MeshAvailability::DeferredMesh(render_config_deferred_loader.acquire_render_config_id(texture_asset_id)),
mesh_deferred_loader.acquire_mesh_id(MeshIndex::file_mesh(mesh_asset_id)),
)
},
Shape::PhysicsData=>{
let mut content="";
let mut mesh_data:&[u8]=&[];
let mut physics_data:&[u8]=&[];
if let Some(rbx_dom_weak::types::Variant::ContentId(asset_id))=object.properties.get(&static_ustr("AssetId")){
content=asset_id.as_ref();
}
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=object.properties.get(&static_ustr("MeshData")){
mesh_data=data.as_ref();
}
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=object.properties.get(&static_ustr("PhysicsData")){
physics_data=data.as_ref();
}
let part_texture_description=get_texture_description(render_config_deferred_loader,&mut recoverable_errors,db,dom,object,size);
let mesh_index=MeshIndex::union(content,mesh_data,physics_data,size,part_texture_description.clone());
let mesh_id=mesh_deferred_loader.acquire_mesh_id(mesh_index);
(MeshAvailability::DeferredUnion(part_texture_description),mesh_id)
},
};
let velocity=match vec3::try_from_f32_array([velocity.x,velocity.y,velocity.z]){
Ok(velocity)=>velocity,
Err(e)=>{
recoverable_errors.basepart_velocity.push(Planar64ConvertError{
path:InstancePath::new(dom,object),
error:e,
});
continue;
}
};
let model_deferred_attributes=ModelDeferredAttributes{
mesh:mesh_id,
transform:model_transform,
color:glam::vec4(color3.r as f32/255f32,color3.g as f32/255f32,color3.b as f32/255f32,1.0-*transparency),
deferred_attributes:GetAttributesArgs{
name:object.name.as_str(),
can_collide,
velocity,
},
};
match availability{
MeshAvailability::Immediate=>primitive_models_deferred_attributes.push(model_deferred_attributes),
MeshAvailability::DeferredMesh(render)=>deferred_models_deferred_attributes.push(DeferredModelDeferredAttributes{
render,
model:model_deferred_attributes
}),
MeshAvailability::DeferredUnion(part_texture_description)=>deferred_unions_deferred_attributes.push(DeferredUnionDeferredAttributes{
render:part_texture_description,
model:model_deferred_attributes,
}),
}
}
PartialMap1{
@@ -706,25 +759,26 @@ pub fn convert<'a>(
primitive_models_deferred_attributes,
deferred_models_deferred_attributes,
deferred_unions_deferred_attributes,
recoverable_errors,
}
}
struct MeshWithAabb{
mesh:model::Mesh,
aabb:Aabb,
struct MeshIdWithSize{
mesh:model::MeshId,
size:Planar64Vec3,
}
fn acquire_mesh_id_from_render_config_id<'a>(
primitive_meshes:&mut Vec<model::Mesh>,
mesh_id_from_render_config_id:&mut HashMap<model::MeshId,HashMap<RenderConfigId,model::MeshId>>,
loaded_meshes:&'a HashMap<model::MeshId,MeshWithAabb>,
loaded_meshes:&'a HashMap<model::MeshId,MeshWithSize>,
old_mesh_id:model::MeshId,
render:RenderConfigId,
)->Option<(model::MeshId,&'a Aabb)>{
)->Option<MeshIdWithSize>{
//ignore meshes that fail to load completely for now
loaded_meshes.get(&old_mesh_id).map(|mesh_with_aabb|(
*mesh_id_from_render_config_id.entry(old_mesh_id).or_insert_with(||HashMap::new())
loaded_meshes.get(&old_mesh_id).map(|&MeshWithSize{ref mesh,size}|MeshIdWithSize{
mesh:*mesh_id_from_render_config_id.entry(old_mesh_id).or_insert_with(||HashMap::new())
.entry(render).or_insert_with(||{
let mesh_id=model::MeshId::new(primitive_meshes.len() as u32);
let mut mesh_clone=mesh_with_aabb.mesh.clone();
let mut mesh_clone=mesh.clone();
//set the render group lool
if let Some(graphics_group)=mesh_clone.graphics_groups.first_mut(){
graphics_group.render=render;
@@ -732,22 +786,22 @@ fn acquire_mesh_id_from_render_config_id<'a>(
primitive_meshes.push(mesh_clone);
mesh_id
}),
&mesh_with_aabb.aabb,
))
size,
})
}
fn acquire_union_id_from_render_config_id<'a>(
primitive_meshes:&mut Vec<model::Mesh>,
union_id_from_render_config_id:&mut HashMap<model::MeshId,HashMap<RobloxPartDescription,model::MeshId>>,
loaded_meshes:&'a HashMap<model::MeshId,MeshWithAabb>,
loaded_meshes:&'a HashMap<model::MeshId,MeshWithSize>,
old_union_id:model::MeshId,
part_texture_description:RobloxPartDescription,
)->Option<(model::MeshId,&'a Aabb)>{
)->Option<MeshIdWithSize>{
//ignore uniones that fail to load completely for now
loaded_meshes.get(&old_union_id).map(|union_with_aabb|(
*union_id_from_render_config_id.entry(old_union_id).or_insert_with(||HashMap::new())
loaded_meshes.get(&old_union_id).map(|&MeshWithSize{ref mesh,size}|MeshIdWithSize{
mesh:*union_id_from_render_config_id.entry(old_union_id).or_insert_with(||HashMap::new())
.entry(part_texture_description.clone()).or_insert_with(||{
let union_id=model::MeshId::new(primitive_meshes.len() as u32);
let mut union_clone=union_with_aabb.mesh.clone();
let mut union_clone=mesh.clone();
//set the render groups
for (graphics_group,maybe_face_texture_description) in union_clone.graphics_groups.iter_mut().zip(part_texture_description.0){
if let Some(face_texture_description)=maybe_face_texture_description{
@@ -757,19 +811,20 @@ fn acquire_union_id_from_render_config_id<'a>(
primitive_meshes.push(union_clone);
union_id
}),
&union_with_aabb.aabb,
))
size,
})
}
pub struct PartialMap1<'a>{
primitive_meshes:Vec<model::Mesh>,
primitive_models_deferred_attributes:Vec<ModelDeferredAttributes<'a>>,
deferred_models_deferred_attributes:Vec<DeferredModelDeferredAttributes<'a>>,
deferred_unions_deferred_attributes:Vec<DeferredUnionDeferredAttributes<'a>>,
recoverable_errors:RecoverableErrors,
}
impl PartialMap1<'_>{
pub fn add_meshpart_meshes_and_calculate_attributes(
mut self,
meshpart_meshes:Meshes,
meshpart_meshes:Meshes<MeshWithSize>,
)->PartialMap2{
//calculate attributes
let mut modes_builder=ModesBuilder::default();
@@ -781,22 +836,14 @@ impl PartialMap1<'_>{
//decode roblox meshes
//generate mesh_id_map based on meshes that failed to load
let loaded_meshes:HashMap<model::MeshId,MeshWithAabb>=
meshpart_meshes.consume().map(|(old_mesh_id,mesh)|{
let mut aabb=strafesnet_common::aabb::Aabb::default();
for &pos in &mesh.unique_pos{
aabb.grow(pos);
}
(old_mesh_id,MeshWithAabb{
mesh,
aabb,
})
}).collect();
let loaded_meshes:HashMap<model::MeshId,MeshWithSize>=
meshpart_meshes.consume().collect();
// SAFETY: I have no idea what I'm doing and this is definitely unsound in some subtle way
// I just want to chain iterators together man
let aint_no_way=core::cell::UnsafeCell::new(&mut self.primitive_meshes);
let mut model_counter=0;
let mut mesh_id_from_render_config_id=HashMap::new();
let mut union_id_from_render_config_id=HashMap::new();
//now that the meshes are loaded, these models can be generated
@@ -804,23 +851,22 @@ impl PartialMap1<'_>{
self.deferred_models_deferred_attributes.into_iter().flat_map(|deferred_model_deferred_attributes|{
//meshes need to be cloned from loaded_meshes with a new id when they are used with a new render_id
//insert into primitive_meshes
let (mesh,aabb)=acquire_mesh_id_from_render_config_id(
let MeshIdWithSize{mesh,size:mesh_size}=acquire_mesh_id_from_render_config_id(
unsafe{*aint_no_way.get()},
&mut mesh_id_from_render_config_id,
&loaded_meshes,
deferred_model_deferred_attributes.model.mesh,
deferred_model_deferred_attributes.render
)?;
let size=aabb.size();
Some(ModelDeferredAttributes{
mesh,
deferred_attributes:deferred_model_deferred_attributes.model.deferred_attributes,
color:deferred_model_deferred_attributes.model.color,
transform:Planar64Affine3::new(
Planar64Mat3::from_cols([
(deferred_model_deferred_attributes.model.transform.matrix3.x_axis*2/size.x).divide().narrow_1().unwrap(),
(deferred_model_deferred_attributes.model.transform.matrix3.y_axis*2/size.y).divide().narrow_1().unwrap(),
(deferred_model_deferred_attributes.model.transform.matrix3.z_axis*2/size.z).divide().narrow_1().unwrap(),
(deferred_model_deferred_attributes.model.transform.matrix3.x_axis*2/mesh_size.x).divide().narrow_1().unwrap(),
(deferred_model_deferred_attributes.model.transform.matrix3.y_axis*2/mesh_size.y).divide().narrow_1().unwrap(),
(deferred_model_deferred_attributes.model.transform.matrix3.z_axis*2/mesh_size.z).divide().narrow_1().unwrap(),
]),
deferred_model_deferred_attributes.model.transform.translation
),
@@ -828,14 +874,13 @@ impl PartialMap1<'_>{
}).chain(self.deferred_unions_deferred_attributes.into_iter().flat_map(|deferred_union_deferred_attributes|{
//meshes need to be cloned from loaded_meshes with a new id when they are used with a new render_id
//insert into primitive_meshes
let (mesh,aabb)=acquire_union_id_from_render_config_id(
let MeshIdWithSize{mesh,size}=acquire_union_id_from_render_config_id(
unsafe{*aint_no_way.get()},
&mut union_id_from_render_config_id,
&loaded_meshes,
deferred_union_deferred_attributes.model.mesh,
deferred_union_deferred_attributes.render
)?;
let size=aabb.size();
Some(ModelDeferredAttributes{
mesh,
deferred_attributes:deferred_union_deferred_attributes.model.deferred_attributes,
@@ -851,61 +896,78 @@ impl PartialMap1<'_>{
})
}))
.chain(self.primitive_models_deferred_attributes.into_iter())
.enumerate().map(|(model_id,model_deferred_attributes)|{
let model_id=model::ModelId::new(model_id as u32);
ModelOwnedAttributes{
.filter_map(|model_deferred_attributes|{
let model_id=model::ModelId::new(model_counter);
let attributes=match get_attributes(
&model_deferred_attributes.deferred_attributes.name,
model_deferred_attributes.deferred_attributes.can_collide,
model_deferred_attributes.deferred_attributes.velocity,
model_id,
&mut modes_builder,
&mut wormhole_in_model_to_id,
&mut wormhole_id_to_out_model,
){
Ok(attributes)=>attributes,
Err(e)=>{
match e{
GetAttributesError::ModeIdParseInt(e)=>self.recoverable_errors.mode_id_parse_int.push(e),
GetAttributesError::DuplicateMode(mode_id)=>{self.recoverable_errors.duplicate_mode.insert(mode_id);},
GetAttributesError::StageIdParseInt(e)=>self.recoverable_errors.stage_id_parse_int.push(e),
GetAttributesError::DuplicateStage(duplicate_stage)=>{self.recoverable_errors.duplicate_stage.insert(duplicate_stage);},
GetAttributesError::WormholeOutIdParseInt(e)=>self.recoverable_errors.wormhole_out_id_parse_int.push(e),
GetAttributesError::DuplicateWormholeOut(wormhole_id)=>{self.recoverable_errors.duplicate_wormhole_out.insert(wormhole_id);},
GetAttributesError::WormholeInIdParseInt(e)=>self.recoverable_errors.wormhole_in_id_parse_int.push(e),
GetAttributesError::JumpLimitParseInt(e)=>self.recoverable_errors.jump_limit_parse_int.push(e),
}
return None;
}
};
model_counter+=1;
Some(ModelOwnedAttributes{
mesh:model_deferred_attributes.mesh,
attributes:get_attributes(
&model_deferred_attributes.deferred_attributes.name,
model_deferred_attributes.deferred_attributes.can_collide,
model_deferred_attributes.deferred_attributes.velocity,
model_id,
&mut modes_builder,
&mut wormhole_in_model_to_id,
&mut wormhole_id_to_out_model,
),
attributes,
color:model_deferred_attributes.color,
transform:model_deferred_attributes.transform,
}
})
}).collect();
let models=models_owned_attributes.into_iter().enumerate().map(|(model_id,mut model_owned_attributes)|{
//TODO: TAB
let model_id=model::ModelId::new(model_id as u32);
//update attributes with wormhole id
//TODO: errors/prints
if let Some(wormhole_id)=wormhole_in_model_to_id.get(&model_id){
if let Some(&wormhole_out_model_id)=wormhole_id_to_out_model.get(wormhole_id){
match &mut model_owned_attributes.attributes{
attr::CollisionAttributes::Contact(attr::ContactAttributes{contacting:_,general})
|attr::CollisionAttributes::Intersect(attr::IntersectAttributes{intersecting:_,general})
=>general.wormhole=Some(attr::Wormhole{destination_model:wormhole_out_model_id}),
attr::CollisionAttributes::Decoration=>println!("Not a wormhole"),
let model_id=model::ModelId::new(model_id as u32);
//update attributes with wormhole id
//TODO: errors/prints
if let Some(wormhole_id)=wormhole_in_model_to_id.get(&model_id){
if let Some(&wormhole_out_model_id)=wormhole_id_to_out_model.get(wormhole_id){
match &mut model_owned_attributes.attributes{
attr::CollisionAttributes::Contact(attr::ContactAttributes{contacting:_,general})
|attr::CollisionAttributes::Intersect(attr::IntersectAttributes{intersecting:_,general})
=>general.wormhole=Some(attr::Wormhole{destination_model:wormhole_out_model_id}),
attr::CollisionAttributes::Decoration=>println!("Not a wormhole"),
}
}
}
}
//index the attributes
let attributes_id=if let Some(&attributes_id)=attributes_id_from_attributes.get(&model_owned_attributes.attributes){
attributes_id
}else{
let attributes_id=attr::CollisionAttributesId::new(unique_attributes.len() as u32);
attributes_id_from_attributes.insert(model_owned_attributes.attributes.clone(),attributes_id);
unique_attributes.push(model_owned_attributes.attributes);
attributes_id
};
model::Model{
mesh:model_owned_attributes.mesh,
transform:model_owned_attributes.transform,
color:model_owned_attributes.color,
attributes:attributes_id,
//index the attributes
let attributes_id=if let Some(&attributes_id)=attributes_id_from_attributes.get(&model_owned_attributes.attributes){
attributes_id
}else{
let attributes_id=attr::CollisionAttributesId::new(unique_attributes.len() as u32);
attributes_id_from_attributes.insert(model_owned_attributes.attributes.clone(),attributes_id);
unique_attributes.push(model_owned_attributes.attributes);
attributes_id
};
model::Model{
mesh:model_owned_attributes.mesh,
transform:model_owned_attributes.transform,
color:model_owned_attributes.color,
attributes:attributes_id,
}
}).collect();
PartialMap2{
meshes:self.primitive_meshes,
models,
modes:modes_builder.build_normalized(),
attributes:unique_attributes,
recoverable_errors:self.recoverable_errors,
}
}).collect();
PartialMap2{
meshes:self.primitive_meshes,
models,
modes:modes_builder.build_normalized(),
attributes:unique_attributes,
}
}
}
@@ -914,12 +976,13 @@ pub struct PartialMap2{
models:Vec<model::Model>,
modes:NormalizedModes,
attributes:Vec<strafesnet_common::gameplay_attributes::CollisionAttributes>,
recoverable_errors:RecoverableErrors,
}
impl PartialMap2{
pub fn add_render_configs_and_textures(
self,
render_configs:RenderConfigs,
)->map::CompleteMap{
)->(map::CompleteMap,RecoverableErrors){
let (textures,render_configs)=render_configs.consume();
let (textures,texture_id_map):(Vec<Vec<u8>>,HashMap<model::TextureId,model::TextureId>)
=textures.into_iter().enumerate().map(|(new_texture_id,(old_texture_id,Texture::ImageDDS(texture)))|{
@@ -938,14 +1001,17 @@ impl PartialMap2{
);
render_config
}).collect();
map::CompleteMap{
modes:self.modes,
attributes:self.attributes,
meshes:self.meshes,
models:self.models,
//the roblox legacy texture thing always works
textures,
render_configs,
}
(
map::CompleteMap{
modes:self.modes,
attributes:self.attributes,
meshes:self.meshes,
models:self.models,
//the roblox legacy texture thing always works
textures,
render_configs,
},
self.recoverable_errors,
)
}
}

View File

@@ -1,9 +1,12 @@
use rbx_mesh::mesh_data::{NormalId2 as MeshDataNormalId2,VertexId as MeshDataVertexId};
use crate::loader::MeshWithSize;
use crate::rbx::RobloxPartDescription;
use crate::primitives::{CUBE_DEFAULT_VERTICES,CUBE_DEFAULT_POLYS,FaceDescription};
use rbx_mesh::mesh_data::{VertexId as MeshDataVertexId,NormalId as MeshDataNormalId,NormalId2 as MeshDataNormalId2,NormalId5 as MeshDataNormalId5};
use rbx_mesh::physics_data::VertexId as PhysicsDataVertexId;
use strafesnet_common::model::{self,IndexedVertex,PolygonGroup,PolygonGroupId,PolygonList,RenderConfigId};
use strafesnet_common::model::{self,IndexedVertex,MeshBuilder,PolygonGroup,PolygonGroupId,PolygonList,RenderConfigId,VertexId};
use strafesnet_common::integer::vec3;
#[allow(dead_code)]
#[derive(Debug)]
pub enum Error{
Block,
@@ -21,7 +24,7 @@ impl std::fmt::Display for Error{
// wacky state machine to make sure all vertices in a face agree upon what NormalId to use.
// Roblox duplicates this information per vertex when it should only exist per-face.
enum MeshDataNormalStatus{
Agree(MeshDataNormalId2),
Agree(MeshDataNormalId),
Conflicting,
}
struct MeshDataNormalChecker{
@@ -31,7 +34,7 @@ impl MeshDataNormalChecker{
fn new()->Self{
Self{status:None}
}
fn check(&mut self,normal:MeshDataNormalId2){
fn check(&mut self,normal:MeshDataNormalId){
self.status=match self.status.take(){
None=>Some(MeshDataNormalStatus::Agree(normal)),
Some(MeshDataNormalStatus::Agree(old_normal))=>{
@@ -44,7 +47,7 @@ impl MeshDataNormalChecker{
Some(MeshDataNormalStatus::Conflicting)=>Some(MeshDataNormalStatus::Conflicting),
};
}
fn into_agreed_normal(self)->Option<MeshDataNormalId2>{
fn into_agreed_normal(self)->Option<MeshDataNormalId>{
self.status.and_then(|status|match status{
MeshDataNormalStatus::Agree(normal)=>Some(normal),
MeshDataNormalStatus::Conflicting=>None,
@@ -52,18 +55,127 @@ impl MeshDataNormalChecker{
}
}
fn build_mesh2(
mb:&mut MeshBuilder,
polygon_groups_normal_id:&mut [Vec<Vec<VertexId>>;NORMAL_FACES],
cube_face_description:&[Option<FaceDescription>;NORMAL_FACES],
mesh:rbx_mesh::mesh_data::Mesh2,
)->Result<(),Error>{
//autoscale to size, idk what roblox is doing with the graphics mesh size
let mut pos_min=glam::Vec3::MAX;
let mut pos_max=glam::Vec3::MIN;
for vertex in &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 mesh.faces{
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))?,
];
let mut normal_agreement_checker=MeshDataNormalChecker::new();
let face=face.into_iter().map(|vertex|{
let MeshDataNormalId2(normal_id)=vertex.normal_id;
normal_agreement_checker.check(normal_id);
let pos=glam::Vec3::from_array(vertex.pos)/graphics_size;
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 tex_coord=glam::Vec2::from_array(vertex.tex);
let maybe_face_description=&cube_face_description[normal_id as usize-1];
let (tex,color)=match maybe_face_description{
Some(face_description)=>{
// transform texture coordinates and set decal color
let tex=mb.acquire_tex_id(face_description.transform.transform_point2(tex_coord));
let color=mb.acquire_color_id(face_description.color);
(tex,color)
},
None=>{
// texture coordinates don't matter and pass through mesh vertex color
let tex=mb.acquire_tex_id(tex_coord);
let color=mb.acquire_color_id(glam::Vec4::from_array(vertex.color.map(|f|f as f32/255.0f32)));
(tex,color)
},
};
Ok(mb.acquire_vertex_id(IndexedVertex{pos,tex,normal,color}))
}).collect::<Result<Vec<_>,_>>().map_err(Error::Planar64Vec3)?;
if let Some(normal_id)=normal_agreement_checker.into_agreed_normal(){
polygon_groups_normal_id[normal_id as usize-1].push(face);
}else{
panic!("Empty face!");
}
}
Ok(())
}
fn build_mesh5(
mb:&mut MeshBuilder,
polygon_groups_normal_id:&mut [Vec<Vec<VertexId>>;NORMAL_FACES],
cube_face_description:&[Option<FaceDescription>;NORMAL_FACES],
mesh:rbx_mesh::mesh_data::CSGMDL5,
)->Result<(),Error>{
//autoscale to size, idk what roblox is doing with the graphics mesh size
let mut pos_min=glam::Vec3::MAX;
let mut pos_max=glam::Vec3::MIN;
for &pos in &mesh.positions{
let p=pos.into();
pos_min=pos_min.min(p);
pos_max=pos_max.max(p);
}
let graphics_size=pos_max-pos_min;
for face in mesh.faces.indices.chunks_exact(3){
let mut normal_agreement_checker=MeshDataNormalChecker::new();
let face=face.into_iter().map(|&vertex_id|{
let vertex_index=vertex_id as usize;
let &pos=mesh.positions.get(vertex_index).ok_or(Error::MissingVertexId(vertex_id))?;
let &MeshDataNormalId5(normal_id)=mesh.normal_ids.get(vertex_index).ok_or(Error::MissingVertexId(vertex_id))?;
let &norm=mesh.normals.get(vertex_index).ok_or(Error::MissingVertexId(vertex_id))?;
let &tex=mesh.tex.get(vertex_index).ok_or(Error::MissingVertexId(vertex_id))?;
let &color=mesh.colors.get(vertex_index).ok_or(Error::MissingVertexId(vertex_id))?;
normal_agreement_checker.check(normal_id);
let pos=glam::Vec3::from_array(pos)/graphics_size;
let pos=mb.acquire_pos_id(vec3::try_from_f32_array(pos.to_array()).map_err(Error::Planar64Vec3)?);
let normal=mb.acquire_normal_id(vec3::try_from_f32_array(norm).map_err(Error::Planar64Vec3)?);
let tex_coord=glam::Vec2::from_array(tex);
let maybe_face_description=&cube_face_description[normal_id as usize-1];
let (tex,color)=match maybe_face_description{
Some(face_description)=>{
// transform texture coordinates and set decal color
let tex=mb.acquire_tex_id(face_description.transform.transform_point2(tex_coord));
let color=mb.acquire_color_id(face_description.color);
(tex,color)
},
None=>{
// texture coordinates don't matter and pass through mesh vertex color
let tex=mb.acquire_tex_id(tex_coord);
let color=mb.acquire_color_id(glam::Vec4::from_array(color.map(|f|f as f32/255.0f32)));
(tex,color)
},
};
Ok(mb.acquire_vertex_id(IndexedVertex{pos,tex,normal,color}))
}).collect::<Result<Vec<_>,_>>()?;
if let Some(normal_id)=normal_agreement_checker.into_agreed_normal(){
polygon_groups_normal_id[normal_id as usize-1].push(face);
}else{
panic!("Empty face!");
}
}
Ok(())
}
const NORMAL_FACES:usize=6;
impl std::error::Error for Error{}
pub fn convert(
roblox_physics_data:&[u8],
roblox_mesh_data:&[u8],
size:glam::Vec3,
crate::rbx::RobloxPartDescription(part_texture_description):crate::rbx::RobloxPartDescription,
)->Result<model::Mesh,Error>{
const NORMAL_FACES:usize=6;
let mut polygon_groups_normal_id=vec![Vec::new();NORMAL_FACES];
RobloxPartDescription(part_texture_description):RobloxPartDescription,
)->Result<MeshWithSize,Error>{
let mut polygon_groups_normal_id:[_;NORMAL_FACES]=[vec![],vec![],vec![],vec![],vec![],vec![]];
// build graphics and physics meshes
let mut mb=strafesnet_common::model::MeshBuilder::new();
let mut mb=MeshBuilder::new();
// graphics
let graphics_groups=if !roblox_mesh_data.is_empty(){
// create per-face texture coordinate affine transforms
@@ -75,46 +187,12 @@ pub fn convert(
let mesh_data=rbx_mesh::read_mesh_data_versioned(
std::io::Cursor::new(roblox_mesh_data)
).map_err(Error::RobloxMeshData)?;
let graphics_mesh=match mesh_data{
match mesh_data{
rbx_mesh::mesh_data::MeshData::CSGK(_)=>return Err(Error::Block),
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::CSGMDL4(mesh_data4))=>mesh_data4.mesh,
rbx_mesh::mesh_data::MeshData::CSGMDL(rbx_mesh::mesh_data::CSGMDL::V2(mesh_data2))=>build_mesh2(&mut mb,&mut polygon_groups_normal_id,&cube_face_description,mesh_data2.mesh)?,
rbx_mesh::mesh_data::MeshData::CSGMDL(rbx_mesh::mesh_data::CSGMDL::V4(mesh_data4))=>build_mesh2(&mut mb,&mut polygon_groups_normal_id,&cube_face_description,mesh_data4.mesh)?,
rbx_mesh::mesh_data::MeshData::CSGMDL(rbx_mesh::mesh_data::CSGMDL::V5(mesh_data4))=>build_mesh5(&mut mb,&mut polygon_groups_normal_id,&cube_face_description,mesh_data4)?,
};
for [MeshDataVertexId(vertex_id0),MeshDataVertexId(vertex_id1),MeshDataVertexId(vertex_id2)] in graphics_mesh.faces{
let face=[
graphics_mesh.vertices.get(vertex_id0 as usize).ok_or(Error::MissingVertexId(vertex_id0))?,
graphics_mesh.vertices.get(vertex_id1 as usize).ok_or(Error::MissingVertexId(vertex_id1))?,
graphics_mesh.vertices.get(vertex_id2 as usize).ok_or(Error::MissingVertexId(vertex_id2))?,
];
let mut normal_agreement_checker=MeshDataNormalChecker::new();
let face=face.into_iter().map(|vertex|{
normal_agreement_checker.check(vertex.normal_id);
let pos=mb.acquire_pos_id(vec3::try_from_f32_array(vertex.pos)?);
let normal=mb.acquire_normal_id(vec3::try_from_f32_array(vertex.norm)?);
let tex_coord=glam::Vec2::from_array(vertex.tex);
let maybe_face_description=&cube_face_description[vertex.normal_id as usize-1];
let (tex,color)=match maybe_face_description{
Some(face_description)=>{
// transform texture coordinates and set decal color
let tex=mb.acquire_tex_id(face_description.transform.transform_point2(tex_coord));
let color=mb.acquire_color_id(face_description.color);
(tex,color)
},
None=>{
// texture coordinates don't matter and pass through mesh vertex color
let tex=mb.acquire_tex_id(tex_coord);
let color=mb.acquire_color_id(glam::Vec4::from_array(vertex.color.map(|f|f as f32/255.0f32)));
(tex,color)
},
};
Ok(mb.acquire_vertex_id(IndexedVertex{pos,tex,normal,color}))
}).collect::<Result<Vec<_>,_>>().map_err(Error::Planar64Vec3)?;
if let Some(normal_id)=normal_agreement_checker.into_agreed_normal(){
polygon_groups_normal_id[normal_id as usize-1].push(face);
}else{
panic!("Empty face!");
}
}
(0..NORMAL_FACES).map(|polygon_group_id|{
model::IndexedGraphicsGroup{
render:cube_face_description[polygon_group_id].as_ref().map_or(RenderConfigId::new(0),|face_description|face_description.render),
@@ -126,7 +204,11 @@ pub fn convert(
};
//physics
let physics_convex_meshes=if !roblox_physics_data.is_empty(){
let polygon_groups_normal_it=polygon_groups_normal_id.into_iter().map(|faces|
// 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(
std::io::Cursor::new(roblox_physics_data)
).map_err(Error::RobloxPhysicsData)?;
@@ -135,44 +217,56 @@ pub fn convert(
// have not seen this format in practice
|rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::Block)
=>return Err(Error::Block),
rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::Meshes(meshes))
rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::V3(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,
rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::PhysicsInfoMesh(pim))
=>vec![pim.mesh],
};
physics_convex_meshes
let physics_convex_meshes_it=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(|[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{
Vec::new()
};
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);
// generate a unit cube as default physics
let pos_list=CUBE_DEFAULT_VERTICES.map(|pos|mb.acquire_pos_id(pos>>1));
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<_,_>>()?)))
})).collect::<Result<_,_>>()?;
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 physics_groups=(NORMAL_FACES..polygon_groups.len()).map(|id|model::IndexedPhysicsGroup{
groups:vec![PolygonGroupId::new(id as u32)]
}).collect();
Ok(mb.build(
let mesh=mb.build(
polygon_groups,
graphics_groups,
physics_groups,
))
);
Ok(MeshWithSize{
mesh,
size:vec3::ONE,
})
}

View File

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

View File

@@ -1,93 +1,121 @@
use rbx_dom_weak::{types::Ref,ustr,InstanceBuilder,WeakDom};
use crate::util::static_ustr;
use rbx_dom_weak::{types::Ref,InstanceBuilder,WeakDom};
pub fn class_is_a(class:&str,superclass:&str)->bool{
class==superclass
||rbx_reflection_database::get().classes.get(class)
.is_some_and(|descriptor|
descriptor.superclass.as_ref().is_some_and(|class_super|
class_is_a(class_super,superclass)
)
)
#[derive(Debug)]
pub enum ServicesError{
WorkspaceNotFound,
}
impl std::fmt::Display for ServicesError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for ServicesError{}
pub struct Services{
pub(crate) game:Ref,
pub(crate) workspace:Ref,
}
#[repr(transparent)]
impl Services{
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(crate)dom:WeakDom,
pub(crate)services:Services,
}
impl Context{
pub const fn new(dom:WeakDom)->Self{
Self{dom}
pub fn from_place(dom:WeakDom)->Result<Context,ServicesError>{
let services=Services::find_services(&dom)?;
Ok(Self{dom,services})
}
pub fn script_singleton(source:String)->(Context,crate::runner::instance::Instance,Services){
pub fn script_singleton(source:String)->(Context,crate::runner::instance::Instance){
let script=InstanceBuilder::new("Script")
.with_property("Source",rbx_types::Variant::String(source));
let script_ref=script.referent();
let mut context=Self::new(WeakDom::new(
let dom=WeakDom::new(
InstanceBuilder::new("DataModel")
.with_child(script)
));
let services=context.convert_into_place();
(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)}
);
let context=Self::from_model(dom);
(context,crate::runner::instance::Instance::new_unchecked(script_ref))
}
/// 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{
self.dom.descendants().filter(|&instance|
class_is_a(instance.class.as_ref(),superclass)
).map(|instance|instance.referent())
}
pub fn scripts(&self)->Vec<crate::runner::instance::Instance>{
self.superclass_iter("LuaSourceContainer").map(crate::runner::instance::Instance::new).collect()
}
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(),
let db=rbx_reflection_database::get();
let Some(superclass)=db.classes.get(superclass)else{
panic!("Invalid class");
};
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 convert_into_place(&mut self)->Services{
pub fn scripts(&self)->Vec<crate::runner::instance::Instance>{
self.superclass_iter("Script")
.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{
//snapshot root instances
let children=self.dom.root().children().to_owned();
let children=dom.root().children().to_owned();
//insert services
let game=self.dom.root_ref();
let terrain_bldr=InstanceBuilder::new("Terrain");
let workspace=self.dom.insert(game,
let game=dom.root_ref();
let terrain_bldr=InstanceBuilder::new("Terrain")
.with_properties([
("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")
//Set Workspace.Terrain property equal to Terrain
.with_property("Terrain",terrain_bldr.referent())
.with_child(terrain_bldr)
);
{
//Lowercase and upper case workspace property!
let game=self.dom.root_mut();
game.properties.insert(ustr("workspace"),rbx_types::Variant::Ref(workspace));
game.properties.insert(ustr("Workspace"),rbx_types::Variant::Ref(workspace));
}
self.dom.insert(game,InstanceBuilder::new("Lighting"));
//transfer original root instances into workspace
for instance in children{
self.dom.transfer_within(instance,workspace);
dom.transfer_within(instance,workspace);
}
Services{
game,
workspace,
{
//Lowercase and upper case workspace property!
let game=dom.root_mut();
// 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}
}
}
pub struct Services{
pub game:Ref,
pub workspace:Ref,
impl AsRef<WeakDom> for Context{
fn as_ref(&self)->&WeakDom{
&self.dom
}
}

View File

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

View File

@@ -0,0 +1,59 @@
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);

View File

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

View File

@@ -1,3 +1,5 @@
use super::number::Number;
#[derive(Clone,Copy)]
pub struct Color3{
r:f32,
@@ -8,28 +10,37 @@ impl Color3{
pub const fn new(r:f32,g:f32,b:f32)->Self{
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 Into<rbx_types::Color3> for Color3{
fn into(self)->rbx_types::Color3{
rbx_types::Color3::new(self.r,self.g,self.b)
impl From<rbx_types::Color3> for Color3{
fn from(value:rbx_types::Color3)->Color3{
Color3::new(value.r,value.g,value.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>{
let color3_table=lua.create_table()?;
let table=lua.create_table()?;
color3_table.raw_set("new",
lua.create_function(|_,(r,g,b):(f32,f32,f32)|
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))
table.raw_set("new",
lua.create_function(|_,(r,g,b):(Number,Number,Number)|
Ok(Color3::new(r.into(),g.into(),b.into()))
)?
)?;
globals.set("Color3",color3_table)?;
let from_rgb=lua.create_function(|_,(r,g,b):(u8,u8,u8)|
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(())
}

View File

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

View File

@@ -1,63 +1,136 @@
use mlua::IntoLua;
#[derive(Clone,Copy)]
pub struct Enum(u32);
#[derive(Clone,Copy)]
pub struct EnumItems;
#[derive(Clone,Copy)]
pub struct EnumItem<'a>{
ed:&'a rbx_reflection::EnumDescriptor<'a>,
name:Option<&'a str>,
value:u32,
}
impl Into<rbx_types::Enum> for Enum{
fn into(self)->rbx_types::Enum{
rbx_types::Enum::from_u32(self.0)
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,
}
}
}
}
impl<'a> EnumItem<'a>{
const fn new(ed:&'a rbx_reflection::EnumDescriptor)->Self{
Self{ed}
#[derive(Clone,Copy)]
pub struct Enums;
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)]
pub struct EnumItems<'a>{
ed:&'a rbx_reflection::EnumDescriptor<'a>,
}
impl<'a> EnumItems<'a>{
pub fn from_value(&self,value:u32)->Option<EnumItem<'a>>{
self.ed.items.iter().find(|&(_,&v)|v==value).map(EnumItem::known_name)
}
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>{
Integer(i32),
String(mlua::String),
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>{
globals.set("Enum",EnumItems)
globals.set("Enum",Enums)
}
impl mlua::UserData for EnumItem<'_>{
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_meta_function(mlua::MetaMethod::Index,|lua,(this,val):(EnumItem<'_>,mlua::String)|{
match this.ed.items.get(&*val.to_str()?){
Some(&id)=>Enum(id).into_lua(lua),
None=>mlua::Value::Nil.into_lua(lua),
}
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<'_>{
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){
methods.add_meta_function(mlua::MetaMethod::Eq,|_,(lhs,rhs):(EnumItem<'_>,EnumItem<'_>)|{
Ok(lhs==rhs)
});
}
}
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);

View File

@@ -2,48 +2,47 @@ use std::collections::{hash_map::Entry,HashMap};
use mlua::{FromLua,FromLuaMulti,IntoLua,IntoLuaMulti};
use rbx_types::Ref;
use rbx_dom_weak::{ustr,Ustr,InstanceBuilder,WeakDom};
use rbx_dom_weak::{Ustr,InstanceBuilder,WeakDom};
use crate::util::static_ustr;
use crate::runner::vector3::Vector3;
use crate::runner::number::Number;
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
//class functions store
lua.set_app_data(ClassMethodsStore::default());
lua.set_app_data(InstanceValueStore::default());
let instance_table=lua.create_table()?;
let table=lua.create_table()?;
//Instance.new
instance_table.raw_set("new",
table.raw_set("new",
lua.create_function(|lua,(class_name,parent):(mlua::String,Option<Instance>)|{
let class_name_str=&*class_name.to_str()?;
let parent=parent.ok_or_else(||mlua::Error::runtime("Nil Parent not yet supported"))?;
let parent_ref=parent.map_or(Ref::none(),|instance|instance.referent);
dom_mut(lua,|dom|{
//TODO: Nil instances
Ok(Instance::new(dom.insert(parent.referent,InstanceBuilder::new(class_name_str))))
Ok(Instance::new_unchecked(dom.insert(parent_ref,InstanceBuilder::new(class_name_str))))
})
})?
)?;
globals.set("Instance",instance_table)?;
globals.set("Instance",table)?;
Ok(())
}
// 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>{
let mut dom=lua.app_data_mut::<&'static mut WeakDom>().ok_or_else(||mlua::Error::runtime("DataModel missing"))?;
let mut dom=lua.app_data_mut::<crate::context::LuaAppData>().ok_or_else(||mlua::Error::runtime("DataModel missing"))?;
f(*dom)
}
fn coerce_float32(value:&mlua::Value)->Option<f32>{
match value{
&mlua::Value::Integer(i)=>Some(i as f32),
&mlua::Value::Number(f)=>Some(f as f32),
_=>None,
}
pub fn class_is_a(class:&str,superclass:&str)->bool{
let db=rbx_reflection_database::get();
let (Some(class),Some(superclass))=(db.classes.get(class),db.classes.get(superclass))else{
return false;
};
db.has_superclass(class,superclass)
}
fn get_full_name(dom:&rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance)->String{
let mut full_name=instance.name.clone();
let mut pref=instance.parent();
@@ -58,7 +57,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>{
dom_mut(lua,|dom|{
let instance=script.get(dom)?;
let source=match instance.properties.get(&ustr("Source")){
let source=match instance.properties.get(&static_ustr("Source")){
Some(rbx_dom_weak::types::Variant::String(s))=>s.clone(),
_=>Err(mlua::Error::external("Missing script.Source"))?,
};
@@ -80,14 +79,32 @@ 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)
}
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)]
pub struct Instance{
referent:Ref,
}
impl Instance{
pub const fn new(referent:Ref)->Self{
pub const fn new_unchecked(referent:Ref)->Self{
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>{
dom.get_by_ref(self.referent).ok_or_else(||mlua::Error::runtime("Instance missing"))
}
@@ -99,19 +116,23 @@ type_from_lua_userdata!(Instance);
impl mlua::UserData for Instance{
fn add_fields<F:mlua::UserDataFields<Self>>(fields:&mut F){
fields.add_field_method_get("Parent",|lua,this|{
fn get_parent(lua:&mlua::Lua,this:&Instance)->mlua::Result<Option<Instance>>{
dom_mut(lua,|dom|{
let instance=this.get(dom)?;
Ok(Instance::new(instance.parent()))
})
});
fields.add_field_method_set("Parent",|lua,this,val:Option<Instance>|{
let parent=val.ok_or_else(||mlua::Error::runtime("Nil Parent not yet supported"))?;
}
fields.add_field_method_get("parent",get_parent);
fields.add_field_method_get("Parent",get_parent);
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.transfer_within(this.referent,parent.referent);
dom.transfer_within(this.referent,parent_ref);
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|{
dom_mut(lua,|dom|{
let instance=this.get(dom)?;
@@ -134,17 +155,33 @@ impl mlua::UserData for Instance{
});
}
fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){
methods.add_method("GetChildren",|lua,this,_:()|
fn clone(lua:&mlua::Lua,this:&Instance,_:())->mlua::Result<Instance>{
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|{
let instance=this.get(dom)?;
let children:Vec<_>=instance
.children()
.iter()
.copied()
.map(Instance::new)
.map(Instance::new_unchecked)
.collect();
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>>{
let name_str=&*name.to_str()?;
@@ -156,7 +193,7 @@ impl mlua::UserData for Instance{
false=>find_first_child(dom,instance,name_str),
}
.map(|instance|
Instance::new(instance.referent())
Instance::new_unchecked(instance.referent())
)
)
})
@@ -166,13 +203,29 @@ impl mlua::UserData for Instance{
methods.add_method("FindFirstChildOfClass",|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_of_class(dom,this.get(dom)?,class_str),
false=>find_first_child_of_class(dom,this.get(dom)?,class_str),
true=>find_first_descendant_of_class(dom,inst,class_str),
false=>find_first_child_of_class(dom,inst,class_str),
}
.map(|instance|
Instance::new(instance.referent())
Instance::new_unchecked(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())
)
)
})
@@ -182,24 +235,42 @@ impl mlua::UserData for Instance{
let children:Vec<_>=dom
.descendants_of(this.referent)
.map(|instance|
Instance::new(instance.referent())
Instance::new_unchecked(instance.referent())
)
.collect();
Ok(children)
})
);
methods.add_method("IsA",|lua,this,classname:mlua::String|
methods.add_method("IsAncestorOf",|lua,this,descendant:Instance|
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|{
let instance=this.get(dom)?;
Ok(crate::context::class_is_a(instance.class.as_str(),&*classname.to_str()?))
Ok(std::iter::successors(Some(instance),|inst|dom.get_by_ref(inst.parent())).any(|inst|inst.referent()==ancestor.referent))
})
);
methods.add_method("Destroy",|lua,this,()|
fn is_a(lua:&mlua::Lua,this:&Instance,classname:mlua::String)->mlua::Result<bool>{
dom_mut(lua,|dom|{
dom.destroy(this.referent);
let instance=this.get(dom)?;
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(())
})
);
}
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|{
dom_mut(lua,|dom|{
let instance=this.get(dom)?;
@@ -208,29 +279,37 @@ impl mlua::UserData for Instance{
});
methods.add_meta_function(mlua::MetaMethod::Index,|lua,(this,index):(Instance,mlua::String)|{
let index_str=&*index.to_str()?;
let index_ustr=ustr(index_str);
dom_mut(lua,|dom|{
let instance=this.get(dom)?;
//println!("__index t={} i={index:?}",instance.name);
let db=rbx_reflection_database::get();
let class=db.classes.get(instance.class.as_str()).ok_or_else(||mlua::Error::runtime("Class missing"))?;
//Find existing property
match instance.properties.get(&index_ustr)
.cloned()
// Find existing property
// Interestingly, ustr can know ahead of time if
// a property does not exist in any runtime instance
match Ustr::from_existing(index_str)
.and_then(|index_ustr|
instance.properties.get(&index_ustr).cloned()
)
//Find default value
.or_else(||db.find_default_property(class,index_str).cloned())
//Find virtual property
.or_else(||db.superclasses_iter(class).find_map(|class|
find_virtual_property(&instance.properties,class,&index_ustr)
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::Int64(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::Ref(val))=>return Instance::new(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::Vector3(v))=>return Into::<crate::runner::vector3::Vector3>::into(v).into_lua(lua),
Some(rbx_types::Variant::String(val))=>return val.into_lua(lua),
Some(rbx_types::Variant::Ref(val))=>return Instance::new_unchecked(val).into_lua(lua),
Some(rbx_types::Variant::Enum(e))=>return crate::runner::r#enum::EnumItem::from(e).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=>(),
other=>return Err(mlua::Error::runtime(format!("Instance.__index Unsupported property type instance={} index={index_str} value={other:?}",instance.name))),
}
@@ -246,27 +325,22 @@ impl mlua::UserData for Instance{
}
//find or create an associated userdata object
if let Some(value)=instance_value_store_mut(lua,|ivs|{
//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)
}
})?{
let instance=this.get_mut(dom)?;
if let Some(value)=get_or_create_userdata(instance,lua,index_str)?{
return value.into_lua(lua);
}
// drop mutable borrow
//find a child with a matching name
let instance=this.get(dom)?;
find_first_child(dom,instance,index_str)
.map(|instance|Instance::new(instance.referent()))
.map(|instance|Instance::new_unchecked(instance.referent()))
.into_lua(lua)
})
});
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|{
let instance=this.get_mut(dom)?;
//println!("__newindex t={} i={index:?} v={value:?}",instance.name);
let index_str=&*index.to_str()?;
let index_ustr=ustr(index_str);
let db=rbx_reflection_database::get();
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|
@@ -274,53 +348,91 @@ impl mlua::UserData for Instance{
).ok_or_else(||
mlua::Error::runtime(format!("Property '{index_str}' missing on class '{}'",class.name))
)?;
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_ustr,rbx_types::Variant::Vector3(typed_value.into()));
},
let value=match &property.data_type{
rbx_reflection::DataType::Value(rbx_types::VariantType::Float32)=>{
let typed_value:f32=coerce_float32(&value).ok_or_else(||mlua::Error::runtime("Expected f32"))?;
instance.properties.insert(index_ustr,rbx_types::Variant::Float32(typed_value));
let typed_value=Number::from_lua(value.clone(),lua)?.to_f32();
rbx_types::Variant::Float32(typed_value)
},
rbx_reflection::DataType::Enum(enum_name)=>{
let typed_value=match &value{
&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::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"))?))
},
mlua::Value::UserData(any_user_data)=>{
let e:crate::runner::r#enum::Enum=*any_user_data.borrow()?;
let e:crate::runner::r#enum::EnumItem=*any_user_data.borrow()?;
Ok(e.into())
},
_=>Err(mlua::Error::runtime("Expected Enum")),
}?;
instance.properties.insert(index_ustr,rbx_types::Variant::Enum(typed_value));
rbx_types::Variant::Enum(typed_value)
},
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()?;
instance.properties.insert(index_ustr,rbx_types::Variant::Color3(typed_value.into()));
rbx_types::Variant::Color3(typed_value.into())
},
rbx_reflection::DataType::Value(rbx_types::VariantType::Bool)=>{
let typed_value=value.as_boolean().ok_or_else(||mlua::Error::runtime("Expected boolean"))?;
instance.properties.insert(index_ustr,rbx_types::Variant::Bool(typed_value));
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)=>{
let typed_value=value.as_str().ok_or_else(||mlua::Error::runtime("Expected boolean"))?;
instance.properties.insert(index_ustr,rbx_types::Variant::String(typed_value.to_owned()));
let typed_value=match &value{
mlua::Value::Integer(i)=>i.to_string(),
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)=>{
let typed_value:crate::runner::number_sequence::NumberSequence=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected NumberSequence"))?.borrow()?;
instance.properties.insert(index_ustr,rbx_types::Variant::NumberSequence(typed_value.into()));
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())
},
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()?;
instance.properties.insert(index_ustr,rbx_types::Variant::ColorSequence(typed_value.into()));
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())
},
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:?}"))),
}
};
// 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(())
})
});
@@ -345,28 +457,53 @@ type CFD=phf::Map<&'static str,// Class name
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!{
"DataModel"=>phf::phf_map!{
"GetService"=>cf!(|lua,_this,service:mlua::String|{
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"))),
}
})
}),
"service"=>GET_SERVICE,
"GetService"=>GET_SERVICE,
},
"Terrain"=>phf::phf_map!{
"FillBlock"=>cf!(|_lua,_,_:(crate::runner::cframe::CFrame,Vector3,crate::runner::r#enum::Enum)|mlua::Result::Ok(()))
"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::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,
))
}),
},
};
@@ -382,7 +519,7 @@ struct ClassMethodsStore{
}
impl ClassMethodsStore{
/// return self.classes[class] or create the ClassMethods and then return it
fn get_or_create_class_methods(&mut self,class:&str)->Option<ClassMethods>{
fn get_or_create_class_methods(&mut self,class:&str)->Option<ClassMethods<'_>>{
// Use get_entry to get the &'static str keys of the database
// and use it as a key for the classes hashmap
CLASS_FUNCTION_DATABASE.get_entry(class)
@@ -455,76 +592,68 @@ static VIRTUAL_PROPERTY_DATABASE:VPD=phf::phf_map!{
fn find_virtual_property(
properties:&rbx_dom_weak::UstrMap<rbx_types::Variant>,
class:&rbx_reflection::ClassDescriptor,
index:&Ustr,
index:&str,
)->Option<rbx_types::Variant>{
//Find virtual property
let class_virtual_properties=VIRTUAL_PROPERTY_DATABASE.get(&class.name)?;
let virtual_property=class_virtual_properties.get(index)?;
//Get source property
let variant=properties.get(&ustr(virtual_property.property))?;
let variant=properties.get(&static_ustr(virtual_property.property))?;
//Transform Source property with provided function
(virtual_property.pointer)(variant)
}
// 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 LUD=phf::Map<&'static str,// Class name
phf::Map<&'static str,// Value name
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!{
"RunService"=>phf::phf_map!{
"RenderStepped"=>|lua|{
lua.create_any_userdata(crate::runner::script_signal::ScriptSignal::new())
},
"Stepped"=>create_script_signal,
"Heartbeat"=>create_script_signal,
"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,
},
};
#[derive(Default)]
pub struct InstanceValueStore{
values:HashMap<Ref,
HashMap<&'static str,
mlua::AnyUserData
>
>,
}
pub struct InstanceValues<'a>{
named_values:&'static phf::Map<&'static str,CreateUserData>,
values:&'a mut HashMap<&'static str,mlua::AnyUserData>,
}
impl InstanceValueStore{
pub fn get_or_create_instance_values(&mut self,instance:&rbx_dom_weak::Instance)->Option<InstanceValues>{
LAZY_USER_DATA.get(instance.class.as_str())
.map(|named_values|
InstanceValues{
named_values,
values:self.values.entry(instance.referent())
.or_insert_with(||HashMap::new()),
}
)
fn get_or_create_userdata(instance:&mut rbx_dom_weak::Instance,lua:&mlua::Lua,index:&str)->mlua::Result<Option<mlua::AnyUserData>>{
use std::collections::hash_map::Entry;
let db=rbx_reflection_database::get();
let Some(class)=db.classes.get(instance.class.as_str())else{
return Ok(None)
};
if let Some((&static_str,create_userdata))=db.superclasses_iter(class).find_map(|superclass|
// find pair (class,index)
LAZY_USER_DATA.get(&superclass.name)
.and_then(|map|map.get_entry(index))
){
let index_ustr=static_ustr(static_str);
return Ok(Some(match instance.userdata.entry(index_ustr){
Entry::Occupied(entry)=>entry.get().clone(),
Entry::Vacant(entry)=>entry.insert(create_userdata(lua)?).clone(),
}));
}
}
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)
Ok(None)
}

View File

@@ -10,6 +10,18 @@ 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{
($ty:ident)=>{
impl mlua::FromLua for $ty<'static>{

View File

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

View File

@@ -0,0 +1,43 @@
// 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))),
}
}
}

View File

@@ -0,0 +1,34 @@
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);

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,42 @@
#[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(())
}

View File

@@ -0,0 +1,35 @@
use super::instance::Instance;
use super::tween_info::TweenInfo;
#[expect(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);

View File

@@ -0,0 +1,45 @@
use super::number::Number;
use super::r#enum::{CoerceEnum,Enums};
#[expect(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);

View File

@@ -0,0 +1,36 @@
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);

View File

@@ -0,0 +1,40 @@
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);

View File

@@ -0,0 +1,93 @@
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);

View File

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

View File

@@ -51,7 +51,7 @@ pub fn scheduler_mut<T>(lua:&mlua::Lua,mut f:impl FnMut(&mut crate::scheduler::S
f(&mut *scheduler)
}
fn schedule_thread(lua:&mlua::Lua,dt:mlua::Value)->Result<(),mlua::Error>{
pub fn schedule_thread(lua:&mlua::Lua,dt:mlua::Value)->Result<(),mlua::Error>{
let delay=match dt{
mlua::Value::Integer(i)=>i.max(0) as u64*60,
mlua::Value::Number(f)=>{
@@ -75,32 +75,3 @@ fn schedule_thread(lua:&mlua::Lua,dt:mlua::Value)->Result<(),mlua::Error>{
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(())
}

View File

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

View File

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

View File

@@ -85,6 +85,7 @@ pub struct Segment{
#[derive(Clone,Copy,Debug)]
pub struct SegmentInfo{
/// time of the first instruction in this segment.
#[expect(dead_code)]
time:Time,
instruction_count:u32,
/// How many total instructions in segments up to and including this segment
@@ -116,6 +117,7 @@ impl<R:BinReaderExt> StreamableBot<R>{
segment_map,
})
}
#[expect(dead_code)]
fn get_segment_info(&self,segment_id:SegmentId)->Result<SegmentInfo,Error>{
Ok(*self.segment_map.get(segment_id.get() as usize).ok_or(Error::InvalidSegmentId(segment_id))?)
}

View File

@@ -53,8 +53,6 @@ pub(crate) enum FourCC{
Map,
#[brw(magic=b"SNFB")]
Bot,
#[brw(magic=b"SNFD")]
Demo,
}
#[binrw]
#[brw(little)]

View File

@@ -5,7 +5,6 @@ mod newtypes;
mod file;
pub mod map;
pub mod bot;
pub mod demo;
#[derive(Debug)]
pub enum Error{
@@ -13,7 +12,6 @@ pub enum Error{
Header(file::Error),
Map(map::Error),
Bot(bot::Error),
Demo(demo::Error),
}
impl std::fmt::Display for Error{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
@@ -25,7 +23,6 @@ impl std::error::Error for Error{}
pub enum SNF<R:BinReaderExt>{
Map(map::StreamableMap<R>),
Bot(bot::StreamableBot<R>),
Demo(demo::StreamableDemo<R>),
}
pub fn read_snf<R:BinReaderExt>(input:R)->Result<SNF<R>,Error>{
@@ -33,7 +30,6 @@ pub fn read_snf<R:BinReaderExt>(input:R)->Result<SNF<R>,Error>{
Ok(match file.fourcc(){
file::FourCC::Map=>SNF::Map(map::StreamableMap::new(file).map_err(Error::Map)?),
file::FourCC::Bot=>SNF::Bot(bot::StreamableBot::new(file).map_err(Error::Bot)?),
file::FourCC::Demo=>SNF::Demo(demo::StreamableDemo::new(file).map_err(Error::Demo)?),
})
}
pub fn read_map<R:BinReaderExt>(input:R)->Result<map::StreamableMap<R>,Error>{
@@ -50,13 +46,6 @@ pub fn read_bot<R:BinReaderExt>(input:R)->Result<bot::StreamableBot<R>,Error>{
_=>Err(Error::UnexpectedFourCC)
}
}
pub fn read_demo<R:BinReaderExt>(input:R)->Result<demo::StreamableDemo<R>,Error>{
let file=file::File::new(input).map_err(Error::Header)?;
match file.fourcc(){
file::FourCC::Demo=>Ok(demo::StreamableDemo::new(file).map_err(Error::Demo)?),
_=>Err(Error::UnexpectedFourCC)
}
}
#[cfg(test)]
mod tests {

View File

@@ -104,6 +104,7 @@ impl From<strafesnet_common::gameplay_style::StyleModifiers> for StyleModifiers{
#[binrw::binrw]
#[brw(little,repr=u8)]
#[expect(dead_code)]
pub enum JumpCalculation{
Max,
BoostThenJump,
@@ -128,6 +129,7 @@ impl From<strafesnet_common::gameplay_style::JumpCalculation> for JumpCalculatio
}
}
#[expect(dead_code)]
pub enum JumpImpulse{
Time(Time),
Height(Planar64),

View File

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

View File

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

View File

@@ -3,11 +3,16 @@ use std::io::{Cursor,Read,Seek};
use std::collections::HashSet;
use clap::{Args,Subcommand};
use anyhow::Result as AResult;
use rbx_dom_weak::{ustr,Instance};
use rbx_dom_weak::Instance;
use strafesnet_deferred_loader::deferred_loader::LoadFailureMode;
use rbxassetid::RobloxAssetId;
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;
#[derive(Subcommand)]
@@ -27,8 +32,12 @@ pub struct RobloxToSNFSubcommand {
pub struct DownloadAssetsSubcommand{
#[arg(required=true)]
roblox_files:Vec<PathBuf>,
// #[arg(long)]
// cookie_file:Option<String>,
#[arg(long,group="cookie",required=true)]
cookie_literal:Option<String>,
#[arg(long,group="cookie",required=true)]
cookie_envvar:Option<String>,
#[arg(long,group="cookie",required=true)]
cookie_file:Option<PathBuf>,
}
impl Commands{
@@ -37,13 +46,27 @@ impl Commands{
Commands::RobloxToSNF(subcommand)=>roblox_to_snf(subcommand.input_files,subcommand.output_folder).await,
Commands::DownloadAssets(subcommand)=>download_assets(
subcommand.roblox_files,
rbx_asset::cookie::Cookie::new("".to_string()),
cookie_from_args(
subcommand.cookie_literal,
subcommand.cookie_envvar,
subcommand.cookie_file,
).await?,
).await,
}
}
}
#[allow(unused)]
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))
}
#[expect(dead_code)]
#[derive(Debug)]
enum LoadDomError{
IO(std::io::Error),
@@ -102,8 +125,8 @@ SurfaceAppearance.RoughnessMapContent
WrapLayer.ReferenceMeshContent
*/
fn accumulate_content(content_list:&mut HashSet<RobloxAssetId>,object:&Instance,property:&str){
let Some(rbx_dom_weak::types::Variant::Content(content))=object.properties.get(&ustr(property))else{
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());
return;
};
@@ -117,8 +140,8 @@ fn accumulate_content(content_list:&mut HashSet<RobloxAssetId>,object:&Instance,
};
content_list.insert(asset_id);
}
fn accumulate_content_id(content_list:&mut HashSet<RobloxAssetId>,object:&Instance,property:&str){
let Some(rbx_dom_weak::types::Variant::ContentId(content))=object.properties.get(&ustr(property))else{
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;
};
@@ -144,8 +167,8 @@ impl UniqueAssets{
fn collect(&mut self,object:&Instance){
match object.class.as_str(){
"Beam"=>accumulate_content_id(&mut self.textures,object,"Texture"),
"Decal"=>accumulate_content_id(&mut self.textures,object,"Texture"),
"Texture"=>accumulate_content_id(&mut self.textures,object,"Texture"),
"Decal"=>accumulate_content(&mut self.textures,object,"TextureContent"),
"Texture"=>accumulate_content(&mut self.textures,object,"TextureContent"),
"FileMesh"=>accumulate_content_id(&mut self.textures,object,"TextureId"),
"MeshPart"=>{
accumulate_content(&mut self.textures,object,"TextureContent");
@@ -169,7 +192,7 @@ impl UniqueAssets{
}
}
#[allow(unused)]
#[expect(dead_code)]
#[derive(Debug)]
enum UniqueAssetError{
IO(std::io::Error),
@@ -244,8 +267,8 @@ async fn download_retry(stats:&mut Stats,context:&rbx_asset::cookie::Context,dow
tokio::fs::write(path,&data).await?;
break Ok(DownloadResult::Data(data));
},
Err(rbx_asset::cookie::GetError::Response(rbx_asset::types::ResponseError::StatusCodeWithUrlAndBody(scwuab)))=>{
if scwuab.status_code.as_u16()==429{
Err(rbx_asset::cookie::GetError::Response(rbx_asset::types::ResponseError::Details{status_code,url_and_body}))=>{
if status_code.as_u16()==429{
if retry==12{
println!("Giving up asset download {asset_id}");
stats.timed_out_downloads+=1;
@@ -257,7 +280,7 @@ async fn download_retry(stats:&mut Stats,context:&rbx_asset::cookie::Context,dow
retry+=1;
}else{
stats.failed_downloads+=1;
println!("weird scuwab error: {scwuab:?}");
println!("weird status_code error: status_code={status_code} url={} body={}",url_and_body.url,url_and_body.body);
break Ok(DownloadResult::Failed);
}
},
@@ -398,7 +421,7 @@ async fn download_assets(paths:Vec<PathBuf>,cookie:rbx_asset::cookie::Cookie)->A
}
#[derive(Debug)]
#[allow(dead_code)]
#[expect(dead_code)]
enum ConvertError{
IO(std::io::Error),
SNFMap(strafesnet_snf::map::Error),
@@ -411,17 +434,23 @@ impl std::fmt::Display for ConvertError{
}
}
impl std::error::Error for ConvertError{}
async fn convert_to_snf(path:&Path,output_folder:PathBuf)->AResult<()>{
let entire_file=tokio::fs::read(path).await?;
struct Errors{
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(
std::io::Cursor::new(entire_file)
entire_file.as_slice()
).map_err(ConvertError::RobloxRead)?;
let mut place=model.into_place();
place.run_scripts();
let mut place=strafesnet_rbx_loader::Place::from(model);
let script_errors=place.run_scripts().unwrap_or_else(|e|vec![e]);
let map=place.to_snf(LoadFailureMode::DefaultToNone).map_err(ConvertError::RobloxLoad)?;
let (map,convert_errors)=place.to_snf(LoadFailureMode::DefaultToNone).map_err(ConvertError::RobloxLoad)?;
let mut dest=output_folder;
dest.push(path.file_stem().unwrap());
@@ -430,7 +459,10 @@ async fn convert_to_snf(path:&Path,output_folder:PathBuf)->AResult<()>{
strafesnet_snf::map::write_map(file,map).map_err(ConvertError::SNFMap)?;
Ok(())
Ok(Errors{
script_errors,
convert_errors,
})
}
async fn roblox_to_snf(paths:Vec<std::path::PathBuf>,output_folder:PathBuf)->AResult<()>{
@@ -439,15 +471,25 @@ async fn roblox_to_snf(paths:Vec<std::path::PathBuf>,output_folder:PathBuf)->ARe
let thread_limit=std::thread::available_parallelism()?.get();
let mut it=paths.into_iter();
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);
while let (Ok(permit),Some(path))=(SEM.acquire().await,it.next()){
let output_folder=output_folder.clone();
tokio::spawn(async move{
let result=convert_to_snf(path.as_path(),output_folder).await;
tokio::task::spawn_blocking(move||{
let result=convert_to_snf(path.as_path(),output_folder);
drop(permit);
match result{
Ok(())=>(),
Ok(errors)=>{
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:?}"),
}
});

View File

@@ -452,7 +452,7 @@ fn bsp_contents(path:PathBuf)->AResult<()>{
}
#[derive(Debug)]
#[allow(dead_code)]
#[expect(dead_code)]
enum ConvertError{
IO(std::io::Error),
SNFMap(strafesnet_snf::map::Error),

View File

@@ -28,5 +28,9 @@ strafesnet_rbx_loader = { path = "../lib/rbx_loader", registry = "strafesnet", o
strafesnet_session = { path = "../engine/session", registry = "strafesnet" }
strafesnet_settings = { path = "../engine/settings", registry = "strafesnet" }
strafesnet_snf = { path = "../lib/snf", registry = "strafesnet", optional = true }
wgpu = "25.0.0"
wgpu = "26.0.1"
winit = "0.30.7"
[profile.dev]
strip = false
opt-level = 3

View File

@@ -3,7 +3,7 @@ use std::io::Read;
#[cfg(any(feature="roblox",feature="source"))]
use strafesnet_deferred_loader::deferred_loader::LoadFailureMode;
#[allow(dead_code)]
#[expect(dead_code)]
#[derive(Debug)]
pub enum ReadError{
#[cfg(feature="roblox")]
@@ -63,7 +63,7 @@ pub fn read<R:Read+std::io::Seek>(input:R)->Result<ReadFormat,ReadError>{
}
}
#[allow(dead_code)]
#[expect(dead_code)]
#[derive(Debug)]
pub enum LoadError{
ReadError(ReadError),
@@ -97,11 +97,16 @@ pub fn load<P:AsRef<std::path::Path>>(path:P)->Result<LoadFormat,LoadError>{
ReadFormat::SNFM(map)=>Ok(LoadFormat::Map(map)),
#[cfg(feature="roblox")]
ReadFormat::Roblox(model)=>{
let mut place=model.into_place();
place.run_scripts();
Ok(LoadFormat::Map(
place.to_snf(LoadFailureMode::DefaultToNone).map_err(LoadError::LoadRoblox)?
))
let mut place=strafesnet_rbx_loader::Place::from(model);
let script_errors=place.run_scripts().unwrap();
for error in script_errors{
println!("Script error: {error}");
}
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")]
ReadFormat::Source(bsp)=>Ok(LoadFormat::Map(

View File

@@ -21,7 +21,7 @@ WorkerDescription{
pub fn new(
mut graphics:graphics::GraphicsState,
mut config:wgpu::SurfaceConfiguration,
surface:wgpu::Surface,
surface:wgpu::Surface<'_>,
device:wgpu::Device,
queue:wgpu::Queue,
)->crate::compat_worker::INWorker<'_,Instruction>{

1
tools/clarion Executable file
View File

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