Compare commits

..

2 Commits

Author SHA1 Message Date
74cc6bc4a5 not best 2025-01-16 20:06:06 -08:00
78cfca8d09 cow mesh can work 2025-01-16 20:05:24 -08:00
118 changed files with 2019 additions and 7452 deletions

@ -1,2 +1,6 @@
[registries.strafesnet] [registries.strafesnet]
index = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/" index = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/"
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=/usr/bin/mold"]

2465
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -1,10 +1,5 @@
[workspace] [workspace]
members = [ members = [
"engine/graphics",
"engine/physics",
"engine/session",
"engine/settings",
"integration-testing",
"lib/bsp_loader", "lib/bsp_loader",
"lib/common", "lib/common",
"lib/deferred_loader", "lib/deferred_loader",
@ -12,10 +7,8 @@ members = [
"lib/linear_ops", "lib/linear_ops",
"lib/ratio_ops", "lib/ratio_ops",
"lib/rbx_loader", "lib/rbx_loader",
"lib/rbxassetid",
"lib/roblox_emulator", "lib/roblox_emulator",
"lib/snf", "lib/snf",
"map-tool",
"strafe-client", "strafe-client",
] ]
resolver = "2" resolver = "2"

@ -3,9 +3,6 @@
# Strafe Project # Strafe Project
Monorepo for working on projects related to strafe client. Monorepo for working on projects related to strafe client.
## Try it out
See [releases](https://git.itzana.me/StrafesNET/strafe-project/releases) for downloads.
## How to build and run ## How to build and run
1. Have rust and git installed 1. Have rust and git installed
2. `git clone https://git.itzana.me/StrafesNET/strafe-project` 2. `git clone https://git.itzana.me/StrafesNET/strafe-project`
@ -13,4 +10,4 @@ See [releases](https://git.itzana.me/StrafesNET/strafe-project/releases) for dow
4. `cargo run --release --bin strafe-client` 4. `cargo run --release --bin strafe-client`
## Licenses ## Licenses
Each project has its own license. Most crates are MIT/Apache but notably the Strafe Client and engine crates have a sole proprietor license. Each project has its own license. Most crates are MIT/Apache but notably the Strafe Client has a sole proprietor license.

@ -1,14 +0,0 @@
[package]
name = "strafesnet_graphics"
version = "0.1.0"
edition = "2024"
[dependencies]
bytemuck = { version = "1.13.1", features = ["derive"] }
ddsfile = "0.5.1"
glam = "0.30.0"
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 = "24.0.0"

@ -1,8 +0,0 @@
/*******************************************************
* Copyright (C) 2023-2024 Rhys Lloyd <krakow20@gmail.com>
*
* This file is part of the StrafesNET bhop/surf client.
*
* StrafesNET can not be copied and/or distributed
* without the express permission of Rhys Lloyd
*******************************************************/

@ -1,2 +0,0 @@
pub mod model;
pub mod graphics;

@ -1,10 +0,0 @@
[package]
name = "strafesnet_physics"
version = "0.1.0"
edition = "2024"
[dependencies]
arrayvec = "0.7.6"
glam = "0.30.0"
id = { version = "0.1.0", registry = "strafesnet" }
strafesnet_common = { path = "../../lib/common", registry = "strafesnet" }

@ -1,8 +0,0 @@
/*******************************************************
* Copyright (C) 2023-2024 Rhys Lloyd <krakow20@gmail.com>
*
* This file is part of the StrafesNET bhop/surf client.
*
* StrafesNET can not be copied and/or distributed
* without the express permission of Rhys Lloyd
*******************************************************/

@ -1,45 +0,0 @@
mod body;
mod push_solve;
mod face_crawler;
mod model;
pub mod physics;
// Physics bug fixes can easily desync all bots.
//
// When replaying a bot, use the exact physics version which it was recorded with.
//
// When validating a new bot, ignore the version and use the latest version,
// and overwrite the version in the file.
//
// Compatible physics versions should be determined
// empirically at development time via leaderboard resimulation.
//
// Compatible physics versions should result in an identical leaderboard state,
// or the only bots which fail are ones exploiting a surgically patched bug.
#[derive(Clone,Copy,Hash,Debug,id::Id,Eq,PartialEq,Ord,PartialOrd)]
pub struct PhysicsVersion(u32);
pub const VERSION:PhysicsVersion=PhysicsVersion(0);
const LATEST_COMPATIBLE_VERSION:[u32;1+VERSION.0 as usize]=const{
let compat=[0];
let mut input_version=0;
while input_version<compat.len(){
// compatible version must be greater than or equal to the input version
assert!(input_version as u32<=compat[input_version]);
// compatible version must be a version that exists
assert!(compat[input_version]<=VERSION.0);
input_version+=1;
}
compat
};
pub enum PhysicsVersionError{
UnknownPhysicsVersion,
}
pub const fn get_latest_compatible_version(PhysicsVersion(version):PhysicsVersion)->Result<PhysicsVersion,PhysicsVersionError>{
if (version as usize)<LATEST_COMPATIBLE_VERSION.len(){
Ok(PhysicsVersion(LATEST_COMPATIBLE_VERSION[version as usize]))
}else{
Err(PhysicsVersionError::UnknownPhysicsVersion)
}
}

@ -1,12 +0,0 @@
[package]
name = "strafesnet_session"
version = "0.1.0"
edition = "2024"
[dependencies]
glam = "0.30.0"
replace_with = "0.1.7"
strafesnet_common = { path = "../../lib/common", registry = "strafesnet" }
strafesnet_physics = { path = "../physics", registry = "strafesnet" }
strafesnet_settings = { path = "../settings", registry = "strafesnet" }
strafesnet_snf = { path = "../../lib/snf", registry = "strafesnet" }

@ -1,8 +0,0 @@
/*******************************************************
* Copyright (C) 2023-2024 Rhys Lloyd <krakow20@gmail.com>
*
* This file is part of the StrafesNET bhop/surf client.
*
* StrafesNET can not be copied and/or distributed
* without the express permission of Rhys Lloyd
*******************************************************/

@ -1,2 +0,0 @@
mod mouse_interpolator;
pub mod session;

@ -1,10 +0,0 @@
[package]
name = "strafesnet_settings"
version = "0.1.0"
edition = "2024"
[dependencies]
configparser = "3.0.2"
directories = "6.0.0"
glam = "0.30.0"
strafesnet_common = { path = "../../lib/common", registry = "strafesnet" }

@ -1,8 +0,0 @@
/*******************************************************
* Copyright (C) 2023-2024 Rhys Lloyd <krakow20@gmail.com>
*
* This file is part of the StrafesNET bhop/surf client.
*
* StrafesNET can not be copied and/or distributed
* without the express permission of Rhys Lloyd
*******************************************************/

@ -1,32 +0,0 @@
use std::path::PathBuf;
use crate::settings::{UserSettings,load_user_settings};
pub struct Directories{
pub settings:PathBuf,
pub maps:PathBuf,
pub replays:PathBuf,
}
impl Directories{
pub fn settings(&self)->UserSettings{
load_user_settings(&self.settings)
}
pub fn user()->Option<Self>{
let dirs=directories::ProjectDirs::from("net.strafes","StrafesNET","Strafe Client")?;
Some(Self{
settings:dirs.config_dir().join("settings.conf"),
maps:dirs.cache_dir().join("maps"),
// separate directory for remote downloaded replays (cache)
// bots:dirs.cache_dir().join("bots"),
replays:dirs.data_local_dir().join("replays"),
})
}
pub fn portable()->Result<Self,std::io::Error>{
let current_dir=std::env::current_dir()?;
Ok(Self{
settings:current_dir.join("settings.conf"),
maps:current_dir.join("maps"),
replays:current_dir.join("replays"),
})
}
}

@ -1,2 +0,0 @@
pub mod settings;
pub mod directories;

@ -1,9 +0,0 @@
[package]
name = "integration-testing"
version = "0.1.0"
edition = "2024"
[dependencies]
strafesnet_common = { path = "../lib/common", registry = "strafesnet" }
strafesnet_physics = { path = "../engine/physics", registry = "strafesnet" }
strafesnet_snf = { path = "../lib/snf", registry = "strafesnet" }

@ -1,221 +0,0 @@
use std::{io::{Cursor,Read},path::Path};
use strafesnet_physics::physics::{PhysicsData,PhysicsState,PhysicsContext};
fn main(){
test_determinism().unwrap();
}
#[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 mut file=std::fs::File::open(path)?;
let mut data=Vec::new();
file.read_to_end(&mut data)?;
Ok(Cursor::new(data))
}
fn run_replay()->Result<(),ReplayError>{
println!("loading map file..");
let data=read_entire_file("../tools/bhop_maps/5692113331.snfm")?;
let map=strafesnet_snf::read_map(data)?.into_complete_map()?;
println!("loading bot file..");
let data=read_entire_file("../tools/replays/535s+159764769ns.snfb")?;
let bot=strafesnet_snf::read_bot(data)?.read_all()?;
// create recording
let mut physics_data=PhysicsData::default();
println!("generating models..");
physics_data.generate_models(&map);
println!("simulating...");
let mut physics=PhysicsState::default();
for ins in bot.instructions{
PhysicsContext::run_input_instruction(&mut physics,&physics_data,ins);
}
match physics.get_finish_time(){
Some(time)=>println!("finish time:{}",time),
None=>println!("simulation did not end in finished state"),
}
Ok(())
}
enum DeterminismResult{
Deterministic,
NonDeterministic,
}
fn segment_determinism(bot:strafesnet_snf::bot::Segment,physics_data:&PhysicsData)->DeterminismResult{
// create default physics state
let mut physics_deterministic=PhysicsState::default();
// create a second physics state
let mut physics_filtered=PhysicsState::default();
// invent a new bot id and insert the replay
println!("simulating...");
let mut non_idle_count=0;
for (i,ins) in bot.instructions.into_iter().enumerate(){
let state_deterministic=physics_deterministic.clone();
let state_filtered=physics_filtered.clone();
PhysicsContext::run_input_instruction(&mut physics_deterministic,&physics_data,ins.clone());
match ins{
strafesnet_common::instruction::TimedInstruction{instruction:strafesnet_common::physics::Instruction::Idle,..}=>(),
other=>{
non_idle_count+=1;
// run
PhysicsContext::run_input_instruction(&mut physics_filtered,&physics_data,other.clone());
// check if position matches
let b0=physics_deterministic.camera_body();
let b1=physics_filtered.camera_body();
if b0.position!=b1.position{
println!("desync at instruction #{}",i);
println!("non idle instructions completed={non_idle_count}");
println!("instruction #{i}={:?}",other);
println!("deterministic state0:\n{state_deterministic:?}");
println!("filtered state0:\n{state_filtered:?}");
println!("deterministic state1:\n{:?}",physics_deterministic);
println!("filtered state1:\n{:?}",physics_filtered);
return DeterminismResult::NonDeterministic;
}
},
}
}
match physics_deterministic.get_finish_time(){
Some(time)=>println!("[with idle] finish time:{}",time),
None=>println!("[with idle] simulation did not end in finished state"),
}
match physics_filtered.get_finish_time(){
Some(time)=>println!("[filtered] finish time:{}",time),
None=>println!("[filtered] simulation did not end in finished state"),
}
DeterminismResult::Deterministic
}
type ThreadResult=Result<Option<DeterminismResult>,ReplayError>;
fn read_and_run(file_path:std::path::PathBuf,physics_data:&PhysicsData)->ThreadResult{
let data=read_entire_file(file_path.as_path())?;
let bot=strafesnet_snf::read_bot(data)?.read_all()?;
println!("Running {:?}",file_path.file_stem());
Ok(Some(segment_determinism(bot,physics_data)))
}
fn do_thread<'a>(s:&'a std::thread::Scope<'a,'_>,file_path:std::path::PathBuf,send:std::sync::mpsc::Sender<ThreadResult>,physics_data:&'a PhysicsData){
s.spawn(move ||{
let result=read_and_run(file_path,physics_data);
// send when thread is complete
send.send(result).unwrap();
});
}
fn get_file_path(dir_entry:std::fs::DirEntry)->Result<Option<std::path::PathBuf>,std::io::Error>{
Ok(dir_entry.file_type()?.is_file().then_some(
dir_entry.path()
))
}
fn test_determinism()->Result<(),ReplayError>{
let thread_limit=std::thread::available_parallelism()?.get();
println!("loading map file..");
let data=read_entire_file("../tools/bhop_maps/5692113331.snfm")?;
let map=strafesnet_snf::read_map(data)?.into_complete_map()?;
let mut physics_data=PhysicsData::default();
println!("generating models..");
physics_data.generate_models(&map);
let (send,recv)=std::sync::mpsc::channel();
let mut read_dir=std::fs::read_dir("../tools/replays")?;
// promise that &physics_data will outlive the spawned threads
let thread_results=std::thread::scope(|s|{
let mut thread_results=Vec::new();
// spawn threads
println!("spawning up to {thread_limit} threads...");
let mut active_thread_count=0;
while active_thread_count<thread_limit{
if let Some(dir_entry_result)=read_dir.next(){
if let Some(file_path)=get_file_path(dir_entry_result?)?{
active_thread_count+=1;
do_thread(s,file_path,send.clone(),&physics_data);
}
}else{
break;
}
}
// spawn another thread every time a message is received from the channel
println!("riding parallelism wave...");
while let Some(dir_entry_result)=read_dir.next(){
if let Some(file_path)=get_file_path(dir_entry_result?)?{
// wait for a thread to complete
thread_results.push(recv.recv().unwrap());
do_thread(s,file_path,send.clone(),&physics_data);
}
}
// wait for remaining threads to complete
println!("waiting for all threads to complete...");
for _ in 0..active_thread_count{
thread_results.push(recv.recv().unwrap());
}
println!("done.");
Ok::<_,ReplayError>(thread_results)
})?;
// tally results
#[derive(Default)]
struct Totals{
deterministic:u32,
nondeterministic:u32,
invalid:u32,
error:u32,
}
let Totals{deterministic,nondeterministic,invalid,error}=thread_results.into_iter().fold(Totals::default(),|mut totals,result|{
match result{
Ok(Some(DeterminismResult::Deterministic))=>totals.deterministic+=1,
Ok(Some(DeterminismResult::NonDeterministic))=>totals.nondeterministic+=1,
Ok(None)=>totals.invalid+=1,
Err(_)=>totals.error+=1,
}
totals
});
println!("deterministic={deterministic}");
println!("nondeterministic={nondeterministic}");
println!("invalid={invalid}");
println!("error={error}");
assert!(nondeterministic==0);
assert!(invalid==0);
assert!(error==0);
Ok(())
}

@ -1,7 +1,7 @@
[package] [package]
name = "strafesnet_bsp_loader" name = "strafesnet_bsp_loader"
version = "0.3.0" version = "0.2.2"
edition = "2024" edition = "2021"
repository = "https://git.itzana.me/StrafesNET/strafe-project" repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
description = "Convert Valve BSP files to StrafesNET data structures." description = "Convert Valve BSP files to StrafesNET data structures."
@ -10,10 +10,7 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
glam = "0.30.0" glam = "0.29.0"
strafesnet_common = { version = "0.6.0", path = "../common", registry = "strafesnet" } strafesnet_common = { path = "../common", registry = "strafesnet" }
strafesnet_deferred_loader = { version = "0.5.0", path = "../deferred_loader", registry = "strafesnet" } vbsp = "0.6.0"
vbsp = { version = "0.7.0-codegen5", registry = "strafesnet", default-features = false }
vbsp-entities = { version = "0.1.0", registry = "strafesnet", default-features = false, features = ["css"]}
vmdl = "0.2.0" vmdl = "0.2.0"
vpk = "0.2.0"

@ -1,342 +0,0 @@
use strafesnet_common::integer::Planar64;
use strafesnet_common::{model,integer};
use strafesnet_common::integer::{vec3::Vector3,Fixed,Ratio};
use crate::{valve_transform_normal,valve_transform_dist};
#[derive(Hash,Eq,PartialEq)]
struct Face{
normal:integer::Planar64Vec3,
dot:integer::Planar64,
}
#[derive(Debug)]
struct Faces{
faces:Vec<Vec<integer::Planar64Vec3>>,
}
fn solve3(c0:&Face,c1:&Face,c2:&Face)->Option<Ratio<Vector3<Fixed<3,96>>,Fixed<3,96>>>{
let n0_n1=c0.normal.cross(c1.normal);
let det=c2.normal.dot(n0_n1);
if det.abs().is_zero(){
return None;
}
Some((
c1.normal.cross(c2.normal)*c0.dot
+c2.normal.cross(c0.normal)*c1.dot
+c0.normal.cross(c1.normal)*c2.dot
)/det)
}
#[derive(Debug)]
pub enum PlanesToFacesError{
InitFace1,
InitFace2,
InitIntersection,
FindNewIntersection,
EmptyFaces,
InfiniteLoop1,
InfiniteLoop2,
}
impl std::fmt::Display for PlanesToFacesError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl core::error::Error for PlanesToFacesError{}
fn planes_to_faces(face_list:std::collections::HashSet<Face>)->Result<Faces,PlanesToFacesError>{
let mut faces=Vec::new();
// for each face, determine one edge at a time until you complete the face
'face: for face0 in &face_list{
// 1. find first edge
// 2. follow edges around face
// === finding first edge ===
// 1. pick the most perpendicular set of 3 faces
// 2. check if any faces occlude the intersection
// 3. use this test to replace left and right alternating until they are not occluded
// find the most perpendicular face to face0
let mut face1=face_list.iter().min_by_key(|&p|{
face0.normal.dot(p.normal).abs()
}).ok_or(PlanesToFacesError::InitFace1)?;
// direction of edge formed by face0 x face1
let edge_dir=face0.normal.cross(face1.normal);
// find the most perpendicular face to both face0 and face1
let mut face2=face_list.iter().max_by_key(|&p|{
// find the best *oriented* face (no .abs())
edge_dir.dot(p.normal)
}).ok_or(PlanesToFacesError::InitFace2)?;
let mut detect_loop=200u8;
let mut intersection=solve3(face0,face1,face2).ok_or(PlanesToFacesError::InitIntersection)?;
// repeatedly update face1, face2 until all faces form part of the convex solid
'find: loop{
detect_loop=detect_loop.checked_sub(1).ok_or(PlanesToFacesError::InfiniteLoop1)?;
// test if any *other* faces occlude the intersection
for new_face in &face_list{
// new face occludes intersection point
if (new_face.dot.fix_2()/Planar64::ONE).lt_ratio(new_face.normal.dot(intersection.num)/intersection.den){
// replace one of the faces with the new face
// dont' try to replace face0 because we are exploring that face in particular
if let Some(new_intersection)=solve3(face0,new_face,face2){
// face1 does not occlude (or intersect) the new intersection
if (face1.dot.fix_2()/Planar64::ONE).gt_ratio(face1.normal.dot(new_intersection.num)/new_intersection.den){
face1=new_face;
intersection=new_intersection;
continue 'find;
}
}
if let Some(new_intersection)=solve3(face0,face1,new_face){
// face2 does not occlude (or intersect) the new intersection
if (face2.dot.fix_2()/Planar64::ONE).gt_ratio(face2.normal.dot(new_intersection.num)/new_intersection.den){
face2=new_face;
intersection=new_intersection;
continue 'find;
}
}
}
}
// we have found a set of faces for which the intersection is on the convex solid
break 'find;
}
// check if face0 must go, meaning it is a degenerate face and does not contribute anything to the convex solid
for new_face in &face_list{
if core::ptr::eq(face0,new_face){
continue;
}
if core::ptr::eq(face1,new_face){
continue;
}
if core::ptr::eq(face2,new_face){
continue;
}
// new_face occludes intersection meaning intersection is not on convex solid and face0 is degenrate
if (new_face.dot.fix_2()/Planar64::ONE).lt_ratio(new_face.normal.dot(intersection.num)/intersection.den){
// abort! reject face0 entirely
continue 'face;
}
}
// === follow edges around face ===
// Note that we chose face2 such that the 3 faces create a particular winding order.
// If we choose a consistent face to follow (face1, face2) it will always wind with a consistent chirality
let mut detect_loop=200u8;
// keep looping until we meet this face again
let face1=face1;
let mut face=Vec::new();
loop{
// push point onto vertices
// problem: this may push a vertex that does not fit in the fixed point range and is thus meaningless
face.push(intersection.divide().fix_1());
// we looped back around to face1, we're done!
if core::ptr::eq(face1,face2){
break;
}
// the measure
let edge_dir=face0.normal.cross(face2.normal);
// the dot product to beat
let d_intersection=edge_dir.dot(intersection.num)/intersection.den;
// find the next face moving clockwise around face0
let (new_face,new_intersection,_)=face_list.iter().filter_map(|new_face|{
// ignore faces that are part of the current edge
if core::ptr::eq(face0,new_face)
|core::ptr::eq(face2,new_face){
return None;
}
let new_intersection=solve3(face0,face2,new_face)?;
// the d value must be larger
let d_new_intersection=edge_dir.dot(new_intersection.num)/new_intersection.den;
if d_new_intersection.le_ratio(d_intersection){
return None;
}
Some((new_face,new_intersection,d_new_intersection))
}).min_by_key(|&(_,_,d)|d).ok_or(PlanesToFacesError::FindNewIntersection)?;
face2=new_face;
intersection=new_intersection;
detect_loop=detect_loop.checked_sub(1).ok_or(PlanesToFacesError::InfiniteLoop2)?;
}
faces.push(face);
}
if faces.is_empty(){
Err(PlanesToFacesError::EmptyFaces)
}else{
Ok(Faces{
faces,
})
}
}
#[allow(dead_code)]
#[derive(Debug)]
pub enum BrushToMeshError{
SliceBrushSides,
MissingPlane,
InvalidFaceCount{
count:usize,
},
InvalidPlanes(PlanesToFacesError),
}
impl std::fmt::Display for BrushToMeshError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl core::error::Error for BrushToMeshError{}
pub fn faces_to_mesh(faces:Vec<Vec<integer::Planar64Vec3>>)->model::Mesh{
// generate the mesh
let mut mb=model::MeshBuilder::new();
let color=mb.acquire_color_id(glam::Vec4::ONE);
let tex=mb.acquire_tex_id(glam::Vec2::ZERO);
// normals are ignored by physics
let normal=mb.acquire_normal_id(integer::vec3::ZERO);
let polygon_list=faces.into_iter().map(|face|{
face.into_iter().map(|pos|{
let pos=mb.acquire_pos_id(pos);
mb.acquire_vertex_id(model::IndexedVertex{
pos,
tex,
normal,
color,
})
}).collect()
}).collect();
let polygon_groups=vec![model::PolygonGroup::PolygonList(model::PolygonList::new(polygon_list))];
let physics_groups=vec![model::IndexedPhysicsGroup{
groups:vec![model::PolygonGroupId::new(0)],
}];
let graphics_groups=vec![];
mb.build(polygon_groups,graphics_groups,physics_groups)
}
pub fn brush_to_mesh(bsp:&vbsp::Bsp,brush:&vbsp::Brush)->Result<model::Mesh,BrushToMeshError>{
let brush_start_idx=brush.brush_side as usize;
let sides_range=brush_start_idx..brush_start_idx+brush.num_brush_sides as usize;
let sides=bsp.brush_sides.get(sides_range).ok_or(BrushToMeshError::SliceBrushSides)?;
let face_list=sides.iter().map(|side|{
// The so-called tumor brushes have TRIGGER bit set
// but also ignore visleaf hint and skip sides
const TUMOR:vbsp::TextureFlags=vbsp::TextureFlags::HINT.union(vbsp::TextureFlags::SKIP).union(vbsp::TextureFlags::TRIGGER);
if let Some(texture_info)=bsp.textures_info.get(side.texture_info as usize){
if texture_info.flags.intersects(TUMOR){
return None;
}
}
let plane=bsp.plane(side.plane as usize)?;
Some(Face{
normal:valve_transform_normal(plane.normal.into()),
dot:valve_transform_dist(plane.dist.into()),
})
}).collect::<Option<std::collections::HashSet<_>>>().ok_or(BrushToMeshError::MissingPlane)?;
if face_list.len()<4{
return Err(BrushToMeshError::InvalidFaceCount{count:face_list.len()});
}
let faces=planes_to_faces(face_list).map_err(BrushToMeshError::InvalidPlanes)?;
let mesh=faces_to_mesh(faces.faces);
Ok(mesh)
}
pub fn unit_cube()->model::Mesh{
let face_list=[
Face{normal:integer::vec3::X,dot:Planar64::ONE},
Face{normal:integer::vec3::Y,dot:Planar64::ONE},
Face{normal:integer::vec3::Z,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_X,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_Y,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_Z,dot:Planar64::ONE},
].into_iter().collect();
let faces=planes_to_faces(face_list).unwrap();
let mesh=faces_to_mesh(faces.faces);
mesh
}
#[cfg(test)]
mod test{
use super::*;
#[test]
fn test_cube(){
let face_list=[
Face{normal:integer::vec3::X,dot:Planar64::ONE},
Face{normal:integer::vec3::Y,dot:Planar64::ONE},
Face{normal:integer::vec3::Z,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_X,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_Y,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_Z,dot:Planar64::ONE},
].into_iter().collect();
let faces=planes_to_faces(face_list).unwrap();
assert_eq!(faces.faces.len(),6);
dbg!(faces);
}
#[test]
fn test_cube_with_degernate_face(){
let face_list=[
Face{normal:integer::vec3::X,dot:Planar64::ONE},
Face{normal:integer::vec3::Y,dot:Planar64::ONE},
Face{normal:integer::vec3::Z,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_X,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_Y,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_Z,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_Z,dot:Planar64::EPSILON},
].into_iter().collect();
let faces=planes_to_faces(face_list).unwrap();
assert_eq!(faces.faces.len(),6);
dbg!(faces);
}
#[test]
fn test_cube_with_degernate_face2(){
let face_list=[
Face{normal:integer::vec3::X,dot:Planar64::ONE},
Face{normal:integer::vec3::Y,dot:Planar64::ONE},
Face{normal:integer::vec3::Z,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_X,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_Y,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_Z,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_X+integer::vec3::NEG_Z,dot:-Planar64::EPSILON},
].into_iter().collect();
let faces=planes_to_faces(face_list).unwrap();
assert_eq!(faces.faces.len(),5);
dbg!(faces);
}
#[test]
fn test_cube_with_degernate_face3(){
let face_list=[
Face{normal:integer::vec3::X,dot:Planar64::ONE},
Face{normal:integer::vec3::Y,dot:Planar64::ONE},
Face{normal:integer::vec3::Z,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_X,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_Y,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_Z,dot:Planar64::ONE},
Face{normal:integer::vec3::NEG_X+integer::vec3::NEG_Z,dot:Planar64::EPSILON},
].into_iter().collect();
let faces=planes_to_faces(face_list).unwrap();
assert_eq!(faces.faces.len(),7);
dbg!(faces);
}
}

@ -1,103 +1,36 @@
use std::borrow::Cow;
use vbsp_entities::css::Entity;
use strafesnet_common::{map,model,integer,gameplay_attributes}; use strafesnet_common::{map,model,integer,gameplay_attributes};
use strafesnet_deferred_loader::deferred_loader::{MeshDeferredLoader,RenderConfigDeferredLoader};
use strafesnet_deferred_loader::mesh::Meshes;
use strafesnet_deferred_loader::texture::{RenderConfigs,Texture};
use crate::valve_transform; const VALVE_SCALE:f32=1.0/16.0;
fn valve_transform([x,y,z]:[f32;3])->integer::Planar64Vec3{
fn ingest_vertex( integer::vec3::try_from_f32_array([x*VALVE_SCALE,z*VALVE_SCALE,-y*VALVE_SCALE]).unwrap()
mb:&mut model::MeshBuilder,
world_position:vbsp::Vector,
texture_transform_u:glam::Vec4,
texture_transform_v:glam::Vec4,
normal:model::NormalId,
color:model::ColorId,
)->model::VertexId{
//world_model.origin seems to always be 0,0,0
let vertex_xyz=world_position.into();
let pos=mb.acquire_pos_id(valve_transform(vertex_xyz));
//calculate texture coordinates
let pos_4d=glam::Vec3::from_array(vertex_xyz).extend(1.0);
let tex=glam::vec2(texture_transform_u.dot(pos_4d),texture_transform_v.dot(pos_4d));
let tex=mb.acquire_tex_id(tex);
mb.acquire_vertex_id(model::IndexedVertex{
pos,
tex,
normal,
color,
})
} }
pub fn convert_bsp<AcquireRenderConfigId,AcquireMeshId>(
fn add_brush<'a>( bsp:&vbsp::Bsp,
mesh_deferred_loader:&mut MeshDeferredLoader<&'a str>, mut acquire_render_config_id:AcquireRenderConfigId,
world_models:&mut Vec<model::Model>, mut acquire_mesh_id:AcquireMeshId
prop_models:&mut Vec<model::Model>, )->PartialMap1
model:&'a str, where
origin:vbsp::Vector, AcquireRenderConfigId:FnMut(Option<&str>)->model::RenderConfigId,
rendercolor:vbsp::Color, AcquireMeshId:FnMut(&str)->model::MeshId,
attributes:gameplay_attributes::CollisionAttributesId, {
){
let transform=integer::Planar64Affine3::from_translation(
valve_transform(origin.into())
);
let color=(glam::Vec3::from_array([
rendercolor.r as f32,
rendercolor.g as f32,
rendercolor.b as f32
])/255.0).extend(1.0);
match model.split_at(1){
// The first character of brush.model is '*'
("*",id_str)=>match id_str.parse(){
Ok(mesh_id)=>{
let mesh=model::MeshId::new(mesh_id);
world_models.push(
model::Model{mesh,attributes,transform,color}
);
},
Err(e)=>{
println!("Brush model int parse error: {e} model={model}");
return;
},
},
_=>{
let mesh=mesh_deferred_loader.acquire_mesh_id(model);
prop_models.push(
model::Model{mesh,attributes,transform,color}
);
}
}
}
pub fn convert<'a>(
bsp:&'a crate::Bsp,
render_config_deferred_loader:&mut RenderConfigDeferredLoader<Cow<'a,str>>,
mesh_deferred_loader:&mut MeshDeferredLoader<&'a str>,
)->PartialMap1{
let bsp=bsp.as_ref();
//figure out real attributes later //figure out real attributes later
let mut unique_attributes=Vec::new(); let mut unique_attributes=Vec::new();
unique_attributes.push(gameplay_attributes::CollisionAttributes::Decoration); unique_attributes.push(gameplay_attributes::CollisionAttributes::Decoration);
unique_attributes.push(gameplay_attributes::CollisionAttributes::contact_default()); const TEMP_TOUCH_ME_ATTRIBUTE:gameplay_attributes::CollisionAttributesId=gameplay_attributes::CollisionAttributesId::new(0);
unique_attributes.push(gameplay_attributes::CollisionAttributes::intersect_default());
const ATTRIBUTE_DECORATION:gameplay_attributes::CollisionAttributesId=gameplay_attributes::CollisionAttributesId::new(0);
const ATTRIBUTE_CONTACT_DEFAULT:gameplay_attributes::CollisionAttributesId=gameplay_attributes::CollisionAttributesId::new(1);
const ATTRIBUTE_INTERSECT_DEFAULT:gameplay_attributes::CollisionAttributesId=gameplay_attributes::CollisionAttributesId::new(2);
let mut prop_mesh_count=0;
//declare all prop models to Loader //declare all prop models to Loader
let mut prop_models=bsp.static_props().map(|prop|{ let prop_models=bsp.static_props().map(|prop|{
//get or create mesh_id //get or create mesh_id
let mesh_id=mesh_deferred_loader.acquire_mesh_id(prop.model()); let mesh_id=acquire_mesh_id(prop.model());
//not the most failsafe code but this is just for the map tool lmao
if prop_mesh_count==mesh_id.get(){
prop_mesh_count+=1;
};
let placement=prop.as_prop_placement(); let placement=prop.as_prop_placement();
model::Model{ model::Model{
mesh:mesh_id, mesh:mesh_id,
attributes:ATTRIBUTE_DECORATION, attributes:TEMP_TOUCH_ME_ATTRIBUTE,
transform:integer::Planar64Affine3::new( transform:integer::Planar64Affine3::new(
integer::mat3::try_from_f32_array_2d(( integer::mat3::try_from_f32_array_2d((
glam::Mat3A::from_diagonal(glam::Vec3::splat(placement.scale)) glam::Mat3A::from_diagonal(glam::Vec3::splat(placement.scale))
@ -114,12 +47,14 @@ pub fn convert<'a>(
//the generated MeshIds in here will collide with the Loader Mesh Ids //the generated MeshIds in here will collide with the Loader Mesh Ids
//but I can't think of a good workaround other than just remapping one later. //but I can't think of a good workaround other than just remapping one later.
let mut world_meshes:Vec<model::Mesh>=bsp.models().map(|world_model|{ let world_meshes:Vec<model::Mesh>=bsp.models().map(|world_model|{
let mut mb=model::MeshBuilder::new(); //non-deduplicated
let mut spam_pos=Vec::new();
let color=mb.acquire_color_id(glam::Vec4::ONE); let mut spam_tex=Vec::new();
let mut spam_normal=Vec::new();
let mut spam_vertices=Vec::new();
let mut graphics_groups=Vec::new(); let mut graphics_groups=Vec::new();
let mut render_id_to_graphics_group_id=std::collections::HashMap::new(); let mut physics_group=model::IndexedPhysicsGroup::default();
let polygon_groups=world_model.faces().enumerate().map(|(polygon_group_id,face)|{ let polygon_groups=world_model.faces().enumerate().map(|(polygon_group_id,face)|{
let polygon_group_id=model::PolygonGroupId::new(polygon_group_id as u32); let polygon_group_id=model::PolygonGroupId::new(polygon_group_id as u32);
let face_texture=face.texture(); let face_texture=face.texture();
@ -128,163 +63,107 @@ pub fn convert<'a>(
let texture_transform_u=glam::Vec4::from_array(face_texture.texture_transforms_u)/(face_texture_data.width as f32); let texture_transform_u=glam::Vec4::from_array(face_texture.texture_transforms_u)/(face_texture_data.width as f32);
let texture_transform_v=glam::Vec4::from_array(face_texture.texture_transforms_v)/(face_texture_data.height as f32); let texture_transform_v=glam::Vec4::from_array(face_texture.texture_transforms_v)/(face_texture_data.height as f32);
//this automatically figures out what the texture is trying to do and creates
//a render config for it, and then returns the id to that render config
let render_id=acquire_render_config_id(Some(face_texture_data.name()));
//normal //normal
let normal=mb.acquire_normal_id(valve_transform(face.normal().into())); let normal=face.normal();
let mut polygon_iter=face.vertex_positions().map(|vertex_position| let normal_idx=spam_normal.len() as u32;
world_model.origin+vertex_position spam_normal.push(valve_transform(normal.into()));
); let mut polygon_iter=face.vertex_positions().map(|vertex_position|{
//world_model.origin seems to always be 0,0,0
let vertex_xyz=(world_model.origin+vertex_position).into();
let pos_idx=spam_pos.len();
spam_pos.push(valve_transform(vertex_xyz));
//calculate texture coordinates
let pos=glam::Vec3::from_array(vertex_xyz).extend(1.0);
let tex=glam::vec2(texture_transform_u.dot(pos),texture_transform_v.dot(pos));
let tex_idx=spam_tex.len() as u32;
spam_tex.push(tex);
let vertex_id=model::VertexId::new(spam_vertices.len() as u32);
spam_vertices.push(model::IndexedVertex{
pos:model::PositionId::new(pos_idx as u32),
tex:model::TextureCoordinateId::new(tex_idx as u32),
normal:model::NormalId::new(normal_idx),
color:model::ColorId::new(0),
});
vertex_id
});
let polygon_list=std::iter::from_fn(move||{ let polygon_list=std::iter::from_fn(move||{
match (polygon_iter.next(),polygon_iter.next(),polygon_iter.next()){ match (polygon_iter.next(),polygon_iter.next(),polygon_iter.next()){
(Some(v1),Some(v2),Some(v3))=>Some([v1,v2,v3]), (Some(v1),Some(v2),Some(v3))=>Some(vec![v1,v2,v3]),
//ignore extra vertices, not sure what to do in this case, failing the whole conversion could be appropriate //ignore extra vertices, not sure what to do in this case, failing the whole conversion could be appropriate
_=>None, _=>None,
} }
}).map(|triplet|{
triplet.map(|world_position|
ingest_vertex(&mut mb,world_position,texture_transform_u,texture_transform_v,normal,color)
).to_vec()
}).collect(); }).collect();
if face.is_visible(){ if face.is_visible(){
//this automatically figures out what the texture is trying to do and creates //TODO: deduplicate graphics groups by render id
//a render config for it, and then returns the id to that render config graphics_groups.push(model::IndexedGraphicsGroup{
let render_id=render_config_deferred_loader.acquire_render_config_id(Some(Cow::Borrowed(face_texture_data.name()))); render:render_id,
//deduplicate graphics groups by render id groups:vec![polygon_group_id],
let graphics_group_id=*render_id_to_graphics_group_id.entry(render_id).or_insert_with(||{ })
let graphics_group_id=graphics_groups.len();
graphics_groups.push(model::IndexedGraphicsGroup{
render:render_id,
groups:vec![],
});
graphics_group_id
});
graphics_groups[graphics_group_id].groups.push(polygon_group_id);
} }
physics_group.groups.push(polygon_group_id);
model::PolygonGroup::PolygonList(model::PolygonList::new(polygon_list)) model::PolygonGroup::PolygonList(model::PolygonList::new(polygon_list))
}).collect(); }).collect();
model::Mesh{
mb.build(polygon_groups,graphics_groups,vec![]) unique_pos:spam_pos,
unique_tex:spam_tex,
unique_normal:spam_normal,
unique_color:vec![glam::Vec4::ONE],
unique_vertices:spam_vertices,
polygon_groups,
graphics_groups,
physics_groups:vec![physics_group],
}
}).collect(); }).collect();
let mut found_spawn=None; let world_models:Vec<model::Model>=
//one instance of the main world mesh
let mut world_models=Vec::new(); std::iter::once((
//world_model
// the one and only world model 0 model::MeshId::new(0),
world_models.push(model::Model{ //model_origin
mesh:model::MeshId::new(0), vbsp::Vector::from([0.0,0.0,0.0]),
attributes:ATTRIBUTE_DECORATION, //model_color
transform:integer::Planar64Affine3::IDENTITY, vbsp::Color{r:255,g:255,b:255},
color:glam::Vec4::W, )).chain(
}); //entities sprinkle instances of the other meshes around
bsp.entities.iter()
const WHITE:vbsp::Color=vbsp::Color{r:255,g:255,b:255}; .flat_map(|ent|ent.parse())//ignore entity parsing errors
for raw_ent in &bsp.entities{ .filter_map(|ent|match ent{
match raw_ent.parse(){ vbsp::Entity::Brush(brush)=>Some(brush),
Ok(Entity::Cycler(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION), vbsp::Entity::BrushIllusionary(brush)=>Some(brush),
Ok(Entity::EnvSprite(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor.parse().unwrap_or(WHITE),ATTRIBUTE_DECORATION), vbsp::Entity::BrushWall(brush)=>Some(brush),
Ok(Entity::FuncBreakable(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION), vbsp::Entity::BrushWallToggle(brush)=>Some(brush),
Ok(Entity::FuncBrush(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION), _=>None,
Ok(Entity::FuncButton(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION), }).flat_map(|brush|
Ok(Entity::FuncDoor(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION), //The first character of brush.model is '*'
Ok(Entity::FuncDoorRotating(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION), brush.model[1..].parse().map(|mesh_id|//ignore parse int errors
Ok(Entity::FuncIllusionary(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION), (model::MeshId::new(mesh_id),brush.origin,brush.color)
Ok(Entity::FuncMonitor(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION), )
Ok(Entity::FuncMovelinear(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION), )
Ok(Entity::FuncPhysbox(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION), ).map(|(mesh_id,model_origin,vbsp::Color{r,g,b})|{
Ok(Entity::FuncPhysboxMultiplayer(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION), model::Model{
Ok(Entity::FuncRotButton(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::FuncRotating(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncTracktrain(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncTrain(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncWall(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin.unwrap_or_default(),brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncWallToggle(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin.unwrap_or_default(),brush.rendercolor,ATTRIBUTE_DECORATION),
Ok(Entity::FuncWaterAnalog(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor.unwrap_or(WHITE),ATTRIBUTE_DECORATION),
Ok(Entity::PropDoorRotating(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::PropDynamic(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::PropDynamicOverride(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::PropPhysics(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::PropPhysicsMultiplayer(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::PropPhysicsOverride(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::PropRagdoll(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::TriggerGravity(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::TriggerHurt(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::TriggerLook(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::TriggerMultiple(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model.unwrap_or_default(),brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::TriggerOnce(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::TriggerProximity(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::TriggerPush(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::TriggerSoundscape(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::TriggerTeleport(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model.unwrap_or_default(),brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::TriggerVphysicsMotion(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::TriggerWind(brush))=>add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,WHITE,ATTRIBUTE_DECORATION),
Ok(Entity::InfoPlayerCounterterrorist(spawn))=>{
found_spawn=Some(valve_transform(spawn.origin.into()));
},
Ok(Entity::InfoPlayerTerrorist(spawn))=>{
found_spawn=Some(valve_transform(spawn.origin.into()));
},
Err(e)=>{
println!("Bsp Entity parse error: {e}");
},
_=>(),
}
}
// physics models
for brush in &bsp.brushes{
if !brush.flags.contains(vbsp::BrushFlags::SOLID){
continue;
}
let mesh_result=crate::brush::brush_to_mesh(bsp,brush);
match mesh_result{
Ok(mesh)=>{
let mesh_id=model::MeshId::new(world_meshes.len() as u32);
world_meshes.push(mesh);
world_models.push(model::Model{
mesh:mesh_id,
attributes:ATTRIBUTE_CONTACT_DEFAULT,
transform:integer::Planar64Affine3::new(
integer::mat3::identity(),
integer::vec3::ZERO,
),
color:glam::Vec4::ONE,
});
},
Err(e)=>println!("Brush mesh error: {e}"),
}
}
let mut modes_list=Vec::new();
if let Some(spawn_point)=found_spawn{
// create a new mesh
let mesh_id=model::MeshId::new(world_meshes.len() as u32);
world_meshes.push(crate::brush::unit_cube());
// create a new model
let model_id=model::ModelId::new(world_models.len() as u32);
world_models.push(model::Model{
mesh:mesh_id, mesh:mesh_id,
attributes:ATTRIBUTE_INTERSECT_DEFAULT, attributes:TEMP_TOUCH_ME_ATTRIBUTE,
transform:integer::Planar64Affine3::from_translation(spawn_point), transform:integer::Planar64Affine3::new(
color:glam::Vec4::W, integer::mat3::identity(),
}); valve_transform(model_origin.into())
),
let first_stage=strafesnet_common::gameplay_modes::Stage::empty(model_id); color:(glam::Vec3::from_array([r as f32,g as f32,b as f32])/255.0).extend(1.0),
let main_mode=strafesnet_common::gameplay_modes::Mode::new( }
strafesnet_common::gameplay_style::StyleModifiers::source_bhop(), }).collect();
model_id,
std::collections::HashMap::new(),
vec![first_stage],
std::collections::HashMap::new(),
);
modes_list.push(main_mode);
}
PartialMap1{ PartialMap1{
attributes:unique_attributes, attributes:unique_attributes,
world_meshes, world_meshes,
prop_models, prop_models,
world_models, world_models,
modes:strafesnet_common::gameplay_modes::Modes::new(modes_list), modes:strafesnet_common::gameplay_modes::Modes::new(Vec::new()),
} }
} }
@ -297,13 +176,26 @@ pub struct PartialMap1{
modes:strafesnet_common::gameplay_modes::Modes, modes:strafesnet_common::gameplay_modes::Modes,
} }
impl PartialMap1{ impl PartialMap1{
pub fn add_prop_meshes<'a>( pub fn add_prop_meshes<AcquireRenderConfigId>(
self, self,
prop_meshes:Meshes, prop_meshes:impl IntoIterator<Item=(model::MeshId,crate::data::ModelData)>,
)->PartialMap2{ mut acquire_render_config_id:AcquireRenderConfigId,
)->PartialMap2
where
AcquireRenderConfigId:FnMut(Option<&str>)->model::RenderConfigId,
{
PartialMap2{ PartialMap2{
attributes:self.attributes, attributes:self.attributes,
prop_meshes:prop_meshes.consume().collect(), prop_meshes:prop_meshes.into_iter().filter_map(|(mesh_id,model_data)|
//this will generate new render ids and texture ids
match convert_mesh(model_data,&mut acquire_render_config_id){
Ok(mesh)=>Some((mesh_id,mesh)),
Err(e)=>{
println!("error converting mesh: {e}");
None
}
}
).collect(),
prop_models:self.prop_models, prop_models:self.prop_models,
world_meshes:self.world_meshes, world_meshes:self.world_meshes,
world_models:self.world_models, world_models:self.world_models,
@ -322,7 +214,8 @@ pub struct PartialMap2{
impl PartialMap2{ impl PartialMap2{
pub fn add_render_configs_and_textures( pub fn add_render_configs_and_textures(
mut self, mut self,
render_configs:RenderConfigs, render_configs:impl IntoIterator<Item=(model::RenderConfigId,model::RenderConfig)>,
textures:impl IntoIterator<Item=(model::TextureId,Vec<u8>)>,
)->map::CompleteMap{ )->map::CompleteMap{
//merge mesh and model lists, flatten and remap all ids //merge mesh and model lists, flatten and remap all ids
let mesh_id_offset=self.world_meshes.len(); let mesh_id_offset=self.world_meshes.len();
@ -341,14 +234,13 @@ impl PartialMap2{
}) })
)); ));
//let mut models=Vec::new(); //let mut models=Vec::new();
let (textures,render_configs)=render_configs.consume();
let (textures,texture_id_map):(Vec<Vec<u8>>,std::collections::HashMap<model::TextureId,model::TextureId>) let (textures,texture_id_map):(Vec<Vec<u8>>,std::collections::HashMap<model::TextureId,model::TextureId>)
=textures.into_iter() =textures.into_iter()
//.filter_map(f) cull unused textures //.filter_map(f) cull unused textures
.enumerate().map(|(new_texture_id,(old_texture_id,Texture::ImageDDS(texture)))|{ .enumerate().map(|(new_texture_id,(old_texture_id,texture))|{
(texture,(old_texture_id,model::TextureId::new(new_texture_id as u32))) (texture,(old_texture_id,model::TextureId::new(new_texture_id as u32)))
}).unzip(); }).unzip();
let render_configs=render_configs.into_iter().map(|(_render_config_id,mut render_config)|{ let render_configs=render_configs.into_iter().map(|(render_config_id,mut render_config)|{
//this may generate duplicate no-texture render configs but idc //this may generate duplicate no-texture render configs but idc
render_config.texture=render_config.texture.and_then(|texture_id| render_config.texture=render_config.texture.and_then(|texture_id|
texture_id_map.get(&texture_id).copied() texture_id_map.get(&texture_id).copied()
@ -365,3 +257,77 @@ impl PartialMap2{
} }
} }
} }
fn convert_mesh<AcquireRenderConfigId>(
model_data:crate::data::ModelData,
acquire_render_config_id:&mut AcquireRenderConfigId,
)->Result<model::Mesh,vmdl::ModelError>
where
AcquireRenderConfigId:FnMut(Option<&str>)->model::RenderConfigId,
{
let model=model_data.read_model()?;
let texture_paths=model.texture_directories();
if texture_paths.len()!=1{
println!("WARNING: multiple texture paths");
}
let skin=model.skin_tables().nth(0).unwrap();
let mut spam_pos=Vec::with_capacity(model.vertices().len());
let mut spam_normal=Vec::with_capacity(model.vertices().len());
let mut spam_tex=Vec::with_capacity(model.vertices().len());
let mut spam_vertices=Vec::with_capacity(model.vertices().len());
for (i,vertex) in model.vertices().iter().enumerate(){
spam_pos.push(valve_transform(vertex.position.into()));
spam_normal.push(valve_transform(vertex.normal.into()));
spam_tex.push(glam::Vec2::from_array(vertex.texture_coordinates));
spam_vertices.push(model::IndexedVertex{
pos:model::PositionId::new(i as u32),
tex:model::TextureCoordinateId::new(i as u32),
normal:model::NormalId::new(i as u32),
color:model::ColorId::new(0),
});
}
let mut graphics_groups=Vec::new();
let mut physics_groups=Vec::new();
let polygon_groups=model.meshes().enumerate().map(|(polygon_group_id,mesh)|{
let polygon_group_id=model::PolygonGroupId::new(polygon_group_id as u32);
let render_id=if let (Some(texture_path),Some(texture_name))=(texture_paths.get(0),skin.texture(mesh.material_index())){
let mut path=std::path::PathBuf::from(texture_path.as_str());
path.push(texture_name);
acquire_render_config_id(path.as_os_str().to_str())
}else{
acquire_render_config_id(None)
};
graphics_groups.push(model::IndexedGraphicsGroup{
render:render_id,
groups:vec![polygon_group_id],
});
physics_groups.push(model::IndexedPhysicsGroup{
groups:vec![polygon_group_id],
});
model::PolygonGroup::PolygonList(model::PolygonList::new(
//looking at the code, it would seem that the strips are pre-deindexed into triangle lists when calling this function
mesh.vertex_strip_indices().flat_map(|mut strip|
std::iter::from_fn(move||{
match (strip.next(),strip.next(),strip.next()){
(Some(v1),Some(v2),Some(v3))=>Some([v1,v2,v3].map(|vertex_id|model::VertexId::new(vertex_id as u32)).to_vec()),
//ignore extra vertices, not sure what to do in this case, failing the whole conversion could be appropriate
_=>None,
}
})
).collect()
))
}).collect();
Ok(model::Mesh{
unique_pos:spam_pos,
unique_normal:spam_normal,
unique_tex:spam_tex,
unique_color:vec![glam::Vec4::ONE],
unique_vertices:spam_vertices,
polygon_groups,
graphics_groups,
physics_groups,
})
}

@ -0,0 +1,60 @@
pub struct Bsp(vbsp::Bsp);
impl Bsp{
pub const fn new(value:vbsp::Bsp)->Self{
Self(value)
}
}
impl AsRef<vbsp::Bsp> for Bsp{
fn as_ref(&self)->&vbsp::Bsp{
&self.0
}
}
pub struct MdlData(Vec<u8>);
impl MdlData{
pub const fn new(value:Vec<u8>)->Self{
Self(value)
}
}
impl AsRef<[u8]> for MdlData{
fn as_ref(&self)->&[u8]{
self.0.as_ref()
}
}
pub struct VtxData(Vec<u8>);
impl VtxData{
pub const fn new(value:Vec<u8>)->Self{
Self(value)
}
}
impl AsRef<[u8]> for VtxData{
fn as_ref(&self)->&[u8]{
self.0.as_ref()
}
}
pub struct VvdData(Vec<u8>);
impl VvdData{
pub const fn new(value:Vec<u8>)->Self{
Self(value)
}
}
impl AsRef<[u8]> for VvdData{
fn as_ref(&self)->&[u8]{
self.0.as_ref()
}
}
pub struct ModelData{
pub mdl:MdlData,
pub vtx:VtxData,
pub vvd:VvdData,
}
impl ModelData{
pub fn read_model(&self)->Result<vmdl::Model,vmdl::ModelError>{
Ok(vmdl::Model::from_parts(
vmdl::mdl::Mdl::read(self.mdl.as_ref())?,
vmdl::vtx::Vtx::read(self.vtx.as_ref())?,
vmdl::vvd::Vvd::read(self.vvd.as_ref())?,
))
}
}

@ -1,20 +1,7 @@
use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLoader,RenderConfigDeferredLoader};
mod bsp; mod bsp;
mod mesh; pub mod data;
mod brush;
pub mod loader;
const VALVE_SCALE:f32=1.0/16.0; pub use data::Bsp;
pub(crate) fn valve_transform_dist(d:f32)->strafesnet_common::integer::Planar64{
(d*VALVE_SCALE).try_into().unwrap()
}
pub(crate) fn valve_transform_normal([x,y,z]:[f32;3])->strafesnet_common::integer::Planar64Vec3{
strafesnet_common::integer::vec3::try_from_f32_array([x,z,-y]).unwrap()
}
pub(crate) fn valve_transform([x,y,z]:[f32;3])->strafesnet_common::integer::Planar64Vec3{
strafesnet_common::integer::vec3::try_from_f32_array([x*VALVE_SCALE,z*VALVE_SCALE,-y*VALVE_SCALE]).unwrap()
}
#[derive(Debug)] #[derive(Debug)]
pub enum ReadError{ pub enum ReadError{
@ -28,38 +15,6 @@ impl std::fmt::Display for ReadError{
} }
impl std::error::Error for ReadError{} impl std::error::Error for ReadError{}
#[derive(Debug)]
pub enum LoadError{
Texture(loader::TextureError),
Mesh(loader::MeshError),
}
impl std::fmt::Display for LoadError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for LoadError{}
impl From<loader::TextureError> for LoadError{
fn from(value:loader::TextureError)->Self{
Self::Texture(value)
}
}
impl From<loader::MeshError> for LoadError{
fn from(value:loader::MeshError)->Self{
Self::Mesh(value)
}
}
pub struct Bsp{
bsp:vbsp::Bsp,
case_folded_file_names:std::collections::HashMap<String,String>,
}
impl AsRef<vbsp::Bsp> for Bsp{
fn as_ref(&self)->&vbsp::Bsp{
&self.bsp
}
}
pub fn read<R:std::io::Read>(mut input:R)->Result<Bsp,ReadError>{ pub fn read<R:std::io::Read>(mut input:R)->Result<Bsp,ReadError>{
let mut s=Vec::new(); let mut s=Vec::new();
@ -68,66 +23,15 @@ pub fn read<R:std::io::Read>(mut input:R)->Result<Bsp,ReadError>{
vbsp::Bsp::read(s.as_slice()).map(Bsp::new).map_err(ReadError::Bsp) vbsp::Bsp::read(s.as_slice()).map(Bsp::new).map_err(ReadError::Bsp)
} }
impl Bsp{
pub fn new(bsp:vbsp::Bsp)->Self{
let case_folded_file_names=bsp.pack.clone().into_zip().lock().unwrap().file_names().map(|s|{
(s.to_lowercase(),s.to_owned())
}).collect();
Self{
bsp,
case_folded_file_names,
}
}
pub fn pack_get(&self,name_lowercase:&str)->Result<Option<Vec<u8>>,vbsp::BspError>{
match self.case_folded_file_names.get(name_lowercase){
Some(name_folded)=>self.bsp.pack.get(name_folded),
None=>Ok(None),
}
}
pub fn to_snf(&self,failure_mode:LoadFailureMode,vpk_list:&[Vpk])->Result<strafesnet_common::map::CompleteMap,LoadError>{
let mut texture_deferred_loader=RenderConfigDeferredLoader::new();
let mut mesh_deferred_loader=MeshDeferredLoader::new();
let map_step1=bsp::convert( pub fn convert<AcquireRenderConfigId,AcquireMeshId>(
self, bsp:&Bsp,
&mut texture_deferred_loader, acquire_render_config_id:AcquireRenderConfigId,
&mut mesh_deferred_loader, acquire_mesh_id:AcquireMeshId
); )->bsp::PartialMap1
where
let mut mesh_loader=loader::MeshLoader::new(loader::BspFinder{bsp:self,vpks:vpk_list},&mut texture_deferred_loader); AcquireRenderConfigId:FnMut(Option<&str>)->strafesnet_common::model::RenderConfigId,
let prop_meshes=mesh_deferred_loader.into_meshes(&mut mesh_loader,failure_mode).map_err(LoadError::Mesh)?; AcquireMeshId:FnMut(&str)->strafesnet_common::model::MeshId,
{
let map_step2=map_step1.add_prop_meshes(prop_meshes); bsp::convert_bsp(bsp.as_ref(),acquire_render_config_id,acquire_mesh_id)
}
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)
}
}
pub struct Vpk{
vpk:vpk::VPK,
case_folded_file_names:std::collections::HashMap<String,String>,
}
impl AsRef<vpk::VPK> for Vpk{
fn as_ref(&self)->&vpk::VPK{
&self.vpk
}
}
impl Vpk{
pub fn new(vpk:vpk::VPK)->Vpk{
let case_folded_file_names=vpk.tree.keys().map(|s|{
(s.to_lowercase(),s.to_owned())
}).collect();
Vpk{
vpk,
case_folded_file_names,
}
}
pub fn tree_get(&self,name_lowercase:&str)->Option<&vpk::entry::VPKEntry>{
let name_folded=self.case_folded_file_names.get(name_lowercase)?;
self.vpk.tree.get(name_folded)
}
}

@ -1,175 +0,0 @@
use std::{borrow::Cow, io::Read};
use strafesnet_common::model::Mesh;
use strafesnet_deferred_loader::{loader::Loader,texture::Texture};
use crate::{Bsp,Vpk};
#[allow(dead_code)]
#[derive(Debug)]
pub enum TextureError{
Io(std::io::Error),
}
impl std::fmt::Display for TextureError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for TextureError{}
impl From<std::io::Error> for TextureError{
fn from(value:std::io::Error)->Self{
Self::Io(value)
}
}
pub struct TextureLoader<'a>(std::marker::PhantomData<&'a ()>);
impl TextureLoader<'_>{
pub fn new()->Self{
Self(std::marker::PhantomData)
}
}
impl<'a> Loader for TextureLoader<'a>{
type Error=TextureError;
type Index=Cow<'a,str>;
type Resource=Texture;
fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>{
let file_name=format!("textures/{}.dds",index);
let mut file=std::fs::File::open(file_name)?;
let mut data=Vec::new();
file.read_to_end(&mut data)?;
Ok(Texture::ImageDDS(data))
}
}
#[allow(dead_code)]
#[derive(Debug)]
pub enum MeshError{
Io(std::io::Error),
VMDL(vmdl::ModelError),
VBSP(vbsp::BspError),
MissingMdl(String),
MissingVtx,
MissingVvd,
}
impl std::fmt::Display for MeshError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for MeshError{}
impl From<std::io::Error> for MeshError{
fn from(value:std::io::Error)->Self{
Self::Io(value)
}
}
impl From<vmdl::ModelError> for MeshError{
fn from(value:vmdl::ModelError)->Self{
Self::VMDL(value)
}
}
impl From<vbsp::BspError> for MeshError{
fn from(value:vbsp::BspError)->Self{
Self::VBSP(value)
}
}
#[derive(Clone,Copy)]
pub struct BspFinder<'bsp,'vpk>{
pub bsp:&'bsp Bsp,
pub vpks:&'vpk [Vpk],
}
impl<'bsp,'vpk> BspFinder<'bsp,'vpk>{
pub fn find<'a>(&self,path:&str)->Result<Option<Cow<'a,[u8]>>,vbsp::BspError>
where
'bsp:'a,
'vpk:'a,
{
// search bsp
if let Some(data)=self.bsp.pack_get(path)?{
return Ok(Some(Cow::Owned(data)));
}
//search each vpk
for vpk in self.vpks{
if let Some(vpk_entry)=vpk.tree_get(path){
return Ok(Some(vpk_entry.get()?));
}
}
Ok(None)
}
}
pub struct ModelLoader<'bsp,'vpk,'a>{
finder:BspFinder<'bsp,'vpk>,
life:core::marker::PhantomData<&'a ()>,
}
impl ModelLoader<'_,'_,'_>{
#[inline]
pub const fn new<'bsp,'vpk,'a>(
finder:BspFinder<'bsp,'vpk>,
)->ModelLoader<'bsp,'vpk,'a>{
ModelLoader{
finder,
life:core::marker::PhantomData,
}
}
}
impl<'bsp,'vpk,'a> Loader for ModelLoader<'bsp,'vpk,'a>
where
'bsp:'a,
'vpk:'a,
{
type Error=MeshError;
type Index=&'a str;
type Resource=vmdl::Model;
fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>{
let mdl_path_lower=index.to_lowercase();
//.mdl, .vvd, .dx90.vtx
let path=std::path::PathBuf::from(mdl_path_lower.as_str());
let mut vvd_path=path.clone();
let mut vtx_path=path;
vvd_path.set_extension("vvd");
vtx_path.set_extension("dx90.vtx");
// TODO: search more packs, possibly using an index of multiple packs
let mdl=self.finder.find(mdl_path_lower.as_str())?.ok_or(MeshError::MissingMdl(mdl_path_lower))?;
let vtx=self.finder.find(vtx_path.as_os_str().to_str().unwrap())?.ok_or(MeshError::MissingVtx)?;
let vvd=self.finder.find(vvd_path.as_os_str().to_str().unwrap())?.ok_or(MeshError::MissingVvd)?;
Ok(vmdl::Model::from_parts(
vmdl::mdl::Mdl::read(mdl.as_ref())?,
vmdl::vtx::Vtx::read(vtx.as_ref())?,
vmdl::vvd::Vvd::read(vvd.as_ref())?,
))
}
}
pub struct MeshLoader<'bsp,'vpk,'load,'a>{
finder:BspFinder<'bsp,'vpk>,
deferred_loader:&'load mut strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader<Cow<'a,str>>,
}
impl MeshLoader<'_,'_,'_,'_>{
#[inline]
pub const fn new<'bsp,'vpk,'load,'a>(
finder:BspFinder<'bsp,'vpk>,
deferred_loader:&'load mut strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader<Cow<'a,str>>,
)->MeshLoader<'bsp,'vpk,'load,'a>{
MeshLoader{
finder,
deferred_loader
}
}
}
impl<'bsp,'vpk,'load,'a> Loader for MeshLoader<'bsp,'vpk,'load,'a>
where
'bsp:'a,
'vpk:'a,
{
type Error=MeshError;
type Index=&'a str;
type Resource=Mesh;
fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>{
let model=ModelLoader::new(self.finder).load(index)?;
let mesh=crate::mesh::convert_mesh(model,&mut self.deferred_loader);
Ok(mesh)
}
}

@ -1,78 +0,0 @@
use std::borrow::Cow;
use strafesnet_common::model;
use strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader;
use crate::valve_transform;
fn ingest_vertex(mb:&mut model::MeshBuilder,vertex:&vmdl::vvd::Vertex,color:model::ColorId)->model::VertexId{
let pos=mb.acquire_pos_id(valve_transform(vertex.position.into()));
let normal=mb.acquire_normal_id(valve_transform(vertex.normal.into()));
let tex=mb.acquire_tex_id(glam::Vec2::from_array(vertex.texture_coordinates));
mb.acquire_vertex_id(model::IndexedVertex{
pos,
tex,
normal,
color,
})
}
pub fn convert_mesh(model:vmdl::Model,deferred_loader:&mut RenderConfigDeferredLoader<Cow<str>>)->model::Mesh{
let texture_paths=model.texture_directories();
if texture_paths.len()!=1{
println!("WARNING: multiple texture paths");
}
let skin=model.skin_tables().nth(0).unwrap();
let mut mb=model::MeshBuilder::new();
let color=mb.acquire_color_id(glam::Vec4::ONE);
let model_vertices=model.vertices();
let mut graphics_groups=Vec::new();
let mut physics_groups=Vec::new();
let polygon_groups=model.meshes().enumerate().map(|(polygon_group_id,mesh)|{
let polygon_group_id=model::PolygonGroupId::new(polygon_group_id as u32);
let render_id=if let (Some(texture_path),Some(texture_name))=(texture_paths.get(0),skin.texture(mesh.material_index())){
let mut path=std::path::PathBuf::from(texture_path.as_str());
path.push(texture_name);
let index=path.as_os_str().to_str().map(|s|Cow::Owned(s.to_owned()));
deferred_loader.acquire_render_config_id(index)
}else{
deferred_loader.acquire_render_config_id(None)
};
graphics_groups.push(model::IndexedGraphicsGroup{
render:render_id,
groups:vec![polygon_group_id],
});
physics_groups.push(model::IndexedPhysicsGroup{
groups:vec![polygon_group_id],
});
model::PolygonGroup::PolygonList(model::PolygonList::new(
//looking at the code, it would seem that the strips are pre-deindexed into triangle lists when calling this function
mesh.vertex_strip_indices().flat_map(|mut strip|{
std::iter::from_fn(move ||{
match (strip.next(),strip.next(),strip.next()){
(Some(v1),Some(v2),Some(v3))=>Some([v1,v2,v3]),
//ignore extra vertices, not sure what to do in this case, failing the whole conversion could be appropriate
_=>None,
}
})
}).flat_map(|[v1,v2,v3]|{
// this should probably be a fatal error :D
let v1=model_vertices.get(v1)?;
let v2=model_vertices.get(v2)?;
let v3=model_vertices.get(v3)?;
Some(vec![
ingest_vertex(&mut mb,v1,color),
ingest_vertex(&mut mb,v2,color),
ingest_vertex(&mut mb,v3,color),
])
}).collect()
))
}).collect();
mb.build(polygon_groups,graphics_groups,physics_groups)
}

@ -1,7 +1,7 @@
[package] [package]
name = "strafesnet_common" name = "strafesnet_common"
version = "0.6.0" version = "0.5.2"
edition = "2024" edition = "2021"
repository = "https://git.itzana.me/StrafesNET/strafe-project" repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
description = "Common types and helpers for Strafe Client associated projects." description = "Common types and helpers for Strafe Client associated projects."
@ -12,8 +12,8 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
[dependencies] [dependencies]
arrayvec = "0.7.4" arrayvec = "0.7.4"
bitflags = "2.6.0" bitflags = "2.6.0"
fixed_wide = { version = "0.1.2", path = "../fixed_wide", registry = "strafesnet", features = ["deferred-division","zeroes","wide-mul"] } fixed_wide = { path = "../fixed_wide", registry = "strafesnet", features = ["deferred-division","zeroes","wide-mul"] }
linear_ops = { version = "0.1.0", path = "../linear_ops", registry = "strafesnet", features = ["deferred-division","named-fields"] } linear_ops = { path = "../linear_ops", registry = "strafesnet", features = ["deferred-division","named-fields"] }
ratio_ops = { version = "0.1.0", path = "../ratio_ops", registry = "strafesnet" } ratio_ops = { path = "../ratio_ops", registry = "strafesnet" }
glam = "0.30.0" glam = "0.29.0"
id = { version = "0.1.0", registry = "strafesnet" } id = { version = "0.1.0", registry = "strafesnet" }

@ -34,10 +34,6 @@ impl Aabb{
self.min-=hs; self.min-=hs;
self.max+=hs; self.max+=hs;
} }
pub fn contains(&self,point:Planar64Vec3)->bool{
let bvec=self.min.lt(point)&point.lt(self.max);
bvec.all()
}
pub fn intersects(&self,aabb:&Aabb)->bool{ pub fn intersects(&self,aabb:&Aabb)->bool{
let bvec=self.min.lt(aabb.max)&aabb.min.lt(self.max); let bvec=self.min.lt(aabb.max)&aabb.min.lt(self.max);
bvec.all() bvec.all()

@ -1,10 +1,4 @@
use std::cmp::Ordering;
use std::collections::BTreeMap;
use crate::aabb::Aabb; use crate::aabb::Aabb;
use crate::ray::Ray;
use crate::integer::{Ratio,Planar64};
use crate::instruction::{InstructionCollector,TimedInstruction};
//da algaritum //da algaritum
//lista boxens //lista boxens
@ -16,117 +10,35 @@ use crate::instruction::{InstructionCollector,TimedInstruction};
//sort the centerpoints on each axis (3 lists) //sort the centerpoints on each axis (3 lists)
//bv is put into octant based on whether it is upper or lower in each list //bv is put into octant based on whether it is upper or lower in each list
pub enum RecursiveContent<R,T>{
pub fn intersect_aabb(ray:&Ray,aabb:&Aabb)->Option<Ratio<Planar64,Planar64>>{ Branch(Vec<R>),
// n.(o+d*t)==n.p Leaf(T),
// n.o + n.d * t == n.p
// t == (n.p - n.o)/n.d
let mut hit=None;
match ray.direction.x.cmp(&Planar64::ZERO){
Ordering::Less=>{
let rel_min=aabb.min()-ray.origin;
let rel_max=aabb.max()-ray.origin;
let dy=rel_max.x*ray.direction.y;
let dz=rel_max.x*ray.direction.z;
// x is negative, so inequalities are flipped
if rel_min.y*ray.direction.x>dy&&dy>rel_max.y*ray.direction.x
&&rel_min.z*ray.direction.x>dz&&dz>rel_max.z*ray.direction.x{
let t=rel_max.x/ray.direction.x;
hit=Some(hit.map_or(t,|best_t|t.min(best_t)));
}
},
Ordering::Equal=>(),
Ordering::Greater=>{
let rel_min=aabb.min()-ray.origin;
let rel_max=aabb.max()-ray.origin;
let dy=rel_min.x*ray.direction.y;
let dz=rel_min.x*ray.direction.z;
// x is positive, so inequalities are normal
if rel_min.y*ray.direction.x<dy&&dy<rel_max.y*ray.direction.x
&&rel_min.z*ray.direction.x<dz&&dz<rel_max.z*ray.direction.x{
let t=rel_min.x/ray.direction.x;
hit=Some(hit.map_or(t,|best_t|t.min(best_t)));
}
},
}
match ray.direction.z.cmp(&Planar64::ZERO){
Ordering::Less=>{
let rel_min=aabb.min()-ray.origin;
let rel_max=aabb.max()-ray.origin;
let dx=rel_max.z*ray.direction.x;
let dy=rel_max.z*ray.direction.y;
// z is negative, so inequalities are flipped
if rel_min.x*ray.direction.z>dx&&dx>rel_max.x*ray.direction.z
&&rel_min.y*ray.direction.z>dy&&dy>rel_max.y*ray.direction.z{
let t=rel_max.z/ray.direction.z;
hit=Some(hit.map_or(t,|best_t|t.min(best_t)));
}
},
Ordering::Equal=>(),
Ordering::Greater=>{
let rel_min=aabb.min()-ray.origin;
let rel_max=aabb.max()-ray.origin;
let dx=rel_min.z*ray.direction.x;
let dy=rel_min.z*ray.direction.y;
// z is positive, so inequalities are normal
if rel_min.x*ray.direction.z<dx&&dx<rel_max.x*ray.direction.z
&&rel_min.y*ray.direction.z<dy&&dy<rel_max.y*ray.direction.z{
let t=rel_min.z/ray.direction.z;
hit=Some(hit.map_or(t,|best_t|t.min(best_t)));
}
},
}
match ray.direction.y.cmp(&Planar64::ZERO){
Ordering::Less=>{
let rel_min=aabb.min()-ray.origin;
let rel_max=aabb.max()-ray.origin;
let dz=rel_max.y*ray.direction.z;
let dx=rel_max.y*ray.direction.x;
// y is negative, so inequalities are flipped
if rel_min.z*ray.direction.y>dz&&dz>rel_max.z*ray.direction.y
&&rel_min.x*ray.direction.y>dx&&dx>rel_max.x*ray.direction.y{
let t=rel_max.y/ray.direction.y;
hit=Some(hit.map_or(t,|best_t|t.min(best_t)));
}
},
Ordering::Equal=>(),
Ordering::Greater=>{
let rel_min=aabb.min()-ray.origin;
let rel_max=aabb.max()-ray.origin;
let dz=rel_min.y*ray.direction.z;
let dx=rel_min.y*ray.direction.x;
// y is positive, so inequalities are normal
if rel_min.z*ray.direction.y<dz&&dz<rel_max.z*ray.direction.y
&&rel_min.x*ray.direction.y<dx&&dx<rel_max.x*ray.direction.y{
let t=rel_min.y/ray.direction.y;
hit=Some(hit.map_or(t,|best_t|t.min(best_t)));
}
},
}
hit
} }
impl<R,T> Default for RecursiveContent<R,T>{
pub enum RecursiveContent<N,L>{ fn default()->Self{
Branch(Vec<N>),
Leaf(L),
}
impl<N,L> RecursiveContent<N,L>{
pub fn empty()->Self{
Self::Branch(Vec::new()) Self::Branch(Vec::new())
} }
} }
pub struct BvhNode<L>{ pub struct BvhNode<T>{
content:RecursiveContent<BvhNode<L>,L>, content:RecursiveContent<BvhNode<T>,T>,
aabb:Aabb, aabb:Aabb,
} }
impl<L> BvhNode<L>{ impl<T> Default for BvhNode<T>{
pub fn empty()->Self{ fn default()->Self{
Self{ Self{
content:RecursiveContent::empty(), content:Default::default(),
aabb:Aabb::default(), aabb:Aabb::default(),
} }
} }
pub fn sample_aabb<F:FnMut(&L)>(&self,aabb:&Aabb,f:&mut F){ }
pub struct BvhWeightNode<W,T>{
content:RecursiveContent<BvhWeightNode<W,T>,T>,
weight:W,
aabb:Aabb,
}
impl<T> BvhNode<T>{
pub fn the_tester<F:FnMut(&T)>(&self,aabb:&Aabb,f:&mut F){
match &self.content{ match &self.content{
RecursiveContent::Leaf(model)=>f(model), RecursiveContent::Leaf(model)=>f(model),
RecursiveContent::Branch(children)=>for child in children{ RecursiveContent::Branch(children)=>for child in children{
@ -135,101 +47,51 @@ impl<L> BvhNode<L>{
//you're probably not going to spend a lot of time outside the map, //you're probably not going to spend a lot of time outside the map,
//so the test is extra work for nothing //so the test is extra work for nothing
if aabb.intersects(&child.aabb){ if aabb.intersects(&child.aabb){
child.sample_aabb(aabb,f); child.the_tester(aabb,f);
} }
}, },
} }
} }
fn populate_nodes<'a,T,F>( pub fn into_visitor<F:FnMut(T)>(self,f:&mut F){
&'a self, match self.content{
collector:&mut InstructionCollector<&'a L,Ratio<Planar64,Planar64>>, RecursiveContent::Leaf(model)=>f(model),
nodes:&mut BTreeMap<Ratio<Planar64,Planar64>,&'a BvhNode<L>>,
ray:&Ray,
start_time:Ratio<Planar64,Planar64>,
f:&F,
)
where
T:Ord+Copy,
Ratio<Planar64,Planar64>:From<T>,
F:Fn(&L,&Ray)->Option<T>,
{
match &self.content{
RecursiveContent::Leaf(leaf)=>if let Some(time)=f(leaf,ray){
let ins=TimedInstruction{time:time.into(),instruction:leaf};
if start_time.lt_ratio(ins.time)&&ins.time.lt_ratio(collector.time()){
collector.collect(Some(ins));
}
},
RecursiveContent::Branch(children)=>for child in children{ RecursiveContent::Branch(children)=>for child in children{
if child.aabb.contains(ray.origin){ child.into_visitor(f)
child.populate_nodes(collector,nodes,ray,start_time,f); },
}else{ }
// Am I an upcoming superstar? }
if let Some(t)=intersect_aabb(ray,&child.aabb){ pub fn weigh_contents<W:Copy+std::iter::Sum<W>,F:Fn(&T)->W>(self,f:&F)->BvhWeightNode<W,T>{
if start_time.lt_ratio(t)&&t.lt_ratio(collector.time()){ match self.content{
nodes.insert(t,child); RecursiveContent::Leaf(model)=>BvhWeightNode{
} weight:f(&model),
} content:RecursiveContent::Leaf(model),
aabb:self.aabb,
},
RecursiveContent::Branch(children)=>{
let branch:Vec<BvhWeightNode<W,T>>=children.into_iter().map(|child|
child.weigh_contents(f)
).collect();
BvhWeightNode{
weight:branch.iter().map(|node|node.weight).sum(),
content:RecursiveContent::Branch(branch),
aabb:self.aabb,
} }
}, },
} }
} }
pub fn sample_ray<T,F>( }
&self,
ray:&Ray,
start_time:T,
time_limit:T,
f:F,
)->Option<(T,&L)>
where
T:Ord+Copy,
T:From<Ratio<Planar64,Planar64>>,
Ratio<Planar64,Planar64>:From<T>,
F:Fn(&L,&Ray)->Option<T>,
{
// source of nondeterminism when Aabb boundaries are coplanar
let mut nodes=BTreeMap::new();
let start_time=start_time.into(); impl <W,T> BvhWeightNode<W,T>{
let time_limit=time_limit.into(); pub const fn weight(&self)->&W{
let mut collector=InstructionCollector::new(time_limit); &self.weight
// break open all nodes that contain ray.origin and populate nodes with future intersection times
self.populate_nodes(&mut collector,&mut nodes,ray,start_time,&f);
// swim through nodes one at a time
while let Some((t,node))=nodes.pop_first(){
if collector.time()<t{
break;
}
match &node.content{
RecursiveContent::Leaf(leaf)=>if let Some(time)=f(leaf,ray){
let ins=TimedInstruction{time:time.into(),instruction:leaf};
// this lower bound can also be omitted
// but it causes type inference errors lol
if start_time.lt_ratio(ins.time)&&ins.time.lt_ratio(collector.time()){
collector.collect(Some(ins));
}
},
// break open the node and predict collisions with the child nodes
RecursiveContent::Branch(children)=>for child in children{
// Am I an upcoming superstar?
if let Some(t)=intersect_aabb(ray,&child.aabb){
// we don't need to check the lower bound
// because child aabbs are guaranteed to be within the parent bounds.
if t<collector.time(){
nodes.insert(t,child);
}
}
},
}
}
collector.take().map(|TimedInstruction{time,instruction:leaf}|(time.into(),leaf))
} }
pub fn into_inner(self)->(RecursiveContent<BvhNode<L>,L>,Aabb){ pub const fn aabb(&self)->&Aabb{
(self.content,self.aabb) &self.aabb
} }
pub fn into_visitor<F:FnMut(L)>(self,f:&mut F){ pub fn into_content(self)->RecursiveContent<BvhWeightNode<W,T>,T>{
self.content
}
pub fn into_visitor<F:FnMut(T)>(self,f:&mut F){
match self.content{ match self.content{
RecursiveContent::Leaf(model)=>f(model), RecursiveContent::Leaf(model)=>f(model),
RecursiveContent::Branch(children)=>for child in children{ RecursiveContent::Branch(children)=>for child in children{
@ -268,9 +130,9 @@ fn generate_bvh_node<T>(boxen:Vec<(T,Aabb)>,force:bool)->BvhNode<T>{
sort_y.push((i,center.y)); sort_y.push((i,center.y));
sort_z.push((i,center.z)); sort_z.push((i,center.z));
} }
sort_x.sort_by_key(|&(_,c)|c); sort_x.sort_by(|tup0,tup1|tup0.1.cmp(&tup1.1));
sort_y.sort_by_key(|&(_,c)|c); sort_y.sort_by(|tup0,tup1|tup0.1.cmp(&tup1.1));
sort_z.sort_by_key(|&(_,c)|c); sort_z.sort_by(|tup0,tup1|tup0.1.cmp(&tup1.1));
let h=n/2; let h=n/2;
let median_x=sort_x[h].1; let median_x=sort_x[h].1;
let median_y=sort_y[h].1; let median_y=sort_y[h].1;

@ -171,7 +171,4 @@ impl CollisionAttributes{
pub fn contact_default()->Self{ pub fn contact_default()->Self{
Self::Contact(ContactAttributes::default()) Self::Contact(ContactAttributes::default())
} }
pub fn intersect_default()->Self{
Self::Intersect(IntersectAttributes::default())
}
} }

@ -1,11 +1,13 @@
use crate::integer::Time;
#[derive(Clone,Debug)] #[derive(Clone,Debug)]
pub struct TimedInstruction<I,T>{ pub struct TimedInstruction<I,T>{
pub time:T, pub time:Time<T>,
pub instruction:I, pub instruction:I,
} }
impl<I,T> TimedInstruction<I,T>{ impl<I,T> TimedInstruction<I,T>{
#[inline] #[inline]
pub fn set_time<T2>(self,new_time:T2)->TimedInstruction<I,T2>{ pub fn set_time<TimeInner>(self,new_time:Time<TimeInner>)->TimedInstruction<I,TimeInner>{
TimedInstruction{ TimedInstruction{
time:new_time, time:new_time,
instruction:self.instruction, instruction:self.instruction,
@ -15,21 +17,21 @@ impl<I,T> TimedInstruction<I,T>{
/// Ensure all emitted instructions are processed before consuming external instructions /// Ensure all emitted instructions are processed before consuming external instructions
pub trait InstructionEmitter<I>{ pub trait InstructionEmitter<I>{
type Time; type TimeInner;
fn next_instruction(&self,time_limit:Self::Time)->Option<TimedInstruction<I,Self::Time>>; fn next_instruction(&self,time_limit:Time<Self::TimeInner>)->Option<TimedInstruction<I,Self::TimeInner>>;
} }
/// Apply an atomic state update /// Apply an atomic state update
pub trait InstructionConsumer<I>{ pub trait InstructionConsumer<I>{
type Time; type TimeInner;
fn process_instruction(&mut self,instruction:TimedInstruction<I,Self::Time>); fn process_instruction(&mut self,instruction:TimedInstruction<I,Self::TimeInner>);
} }
/// If the object produces its own instructions, allow exhaustively feeding them back in /// If the object produces its own instructions, allow exhaustively feeding them back in
pub trait InstructionFeedback<I,T>:InstructionEmitter<I,Time=T>+InstructionConsumer<I,Time=T> pub trait InstructionFeedback<I,T>:InstructionEmitter<I,TimeInner=T>+InstructionConsumer<I,TimeInner=T>
where where
T:Copy, Time<T>:Copy,
{ {
#[inline] #[inline]
fn process_exhaustive(&mut self,time_limit:T){ fn process_exhaustive(&mut self,time_limit:Time<T>){
while let Some(instruction)=self.next_instruction(time_limit){ while let Some(instruction)=self.next_instruction(time_limit){
self.process_instruction(instruction); self.process_instruction(instruction);
} }
@ -37,40 +39,29 @@ pub trait InstructionFeedback<I,T>:InstructionEmitter<I,Time=T>+InstructionConsu
} }
impl<I,T,X> InstructionFeedback<I,T> for X impl<I,T,X> InstructionFeedback<I,T> for X
where where
T:Copy, Time<T>:Copy,
X:InstructionEmitter<I,Time=T>+InstructionConsumer<I,Time=T>, X:InstructionEmitter<I,TimeInner=T>+InstructionConsumer<I,TimeInner=T>,
{} {}
//PROPER PRIVATE FIELDS!!! //PROPER PRIVATE FIELDS!!!
pub struct InstructionCollector<I,T>{ pub struct InstructionCollector<I,T>{
time:T, time:Time<T>,
instruction:Option<I>, instruction:Option<I>,
} }
impl<I,T> InstructionCollector<I,T>{ impl<I,T> InstructionCollector<I,T>
where Time<T>:Copy+PartialOrd,
{
#[inline] #[inline]
pub const fn new(time:T)->Self{ pub const fn new(time:Time<T>)->Self{
Self{ Self{
time, time,
instruction:None instruction:None
} }
} }
#[inline] #[inline]
pub fn take(self)->Option<TimedInstruction<I,T>>{ pub const fn time(&self)->Time<T>{
//STEAL INSTRUCTION AND DESTROY INSTRUCTIONCOLLECTOR
self.instruction.map(|instruction|TimedInstruction{
time:self.time,
instruction
})
}
}
impl<I,T:Copy> InstructionCollector<I,T>{
#[inline]
pub const fn time(&self)->T{
self.time self.time
} }
}
impl<I,T:PartialOrd> InstructionCollector<I,T>{
#[inline]
pub fn collect(&mut self,instruction:Option<TimedInstruction<I,T>>){ pub fn collect(&mut self,instruction:Option<TimedInstruction<I,T>>){
if let Some(ins)=instruction{ if let Some(ins)=instruction{
if ins.time<self.time{ if ins.time<self.time{
@ -79,4 +70,11 @@ impl<I,T:PartialOrd> InstructionCollector<I,T>{
} }
} }
} }
pub fn instruction(self)->Option<TimedInstruction<I,T>>{
//STEAL INSTRUCTION AND DESTROY INSTRUCTIONCOLLECTOR
self.instruction.map(|instruction|TimedInstruction{
time:self.time,
instruction
})
}
} }

@ -4,17 +4,16 @@ pub use ratio_ops::ratio::{Ratio,Divide};
//integer units //integer units
/// specific example of a "default" time type /// specific example of a "default" time type
#[derive(Clone,Copy,Hash,Eq,PartialEq,Ord,PartialOrd,Debug)] #[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
pub enum TimeInner{} pub enum TimeInner{}
pub type AbsoluteTime=Time<TimeInner>; pub type AbsoluteTime=Time<TimeInner>;
#[derive(Clone,Copy,Hash,Eq,PartialEq,Ord,PartialOrd,Debug)] #[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
pub struct Time<T>(i64,core::marker::PhantomData<T>); pub struct Time<T>(i64,core::marker::PhantomData<T>);
impl<T> Time<T>{ impl<T> Time<T>{
pub const MIN:Self=Self::raw(i64::MIN); pub const MIN:Self=Self::raw(i64::MIN);
pub const MAX:Self=Self::raw(i64::MAX); pub const MAX:Self=Self::raw(i64::MAX);
pub const ZERO:Self=Self::raw(0); pub const ZERO:Self=Self::raw(0);
pub const EPSILON:Self=Self::raw(1);
pub const ONE_SECOND:Self=Self::raw(1_000_000_000); pub const ONE_SECOND:Self=Self::raw(1_000_000_000);
pub const ONE_MILLISECOND:Self=Self::raw(1_000_000); pub const ONE_MILLISECOND:Self=Self::raw(1_000_000);
pub const ONE_MICROSECOND:Self=Self::raw(1_000); pub const ONE_MICROSECOND:Self=Self::raw(1_000);
@ -63,12 +62,6 @@ impl<T> From<Planar64> for Time<T>{
Self::raw((value*Planar64::raw(1_000_000_000)).fix_1().to_raw()) Self::raw((value*Planar64::raw(1_000_000_000)).fix_1().to_raw())
} }
} }
impl<T> From<Time<T>> for Ratio<Planar64,Planar64>{
#[inline]
fn from(value:Time<T>)->Self{
value.to_ratio()
}
}
impl<T,Num,Den,N1,T1> From<Ratio<Num,Den>> for Time<T> impl<T,Num,Den,N1,T1> From<Ratio<Num,Den>> for Time<T>
where where
Num:core::ops::Mul<Planar64,Output=N1>, Num:core::ops::Mul<Planar64,Output=N1>,
@ -575,7 +568,7 @@ pub mod vec3{
Planar64Vec3::new(array.map(Planar64::raw)) Planar64Vec3::new(array.map(Planar64::raw))
} }
#[inline] #[inline]
pub fn raw_xyz(x:i64,y:i64,z:i64)->Planar64Vec3{ pub const fn raw_xyz(x:i64,y:i64,z:i64)->Planar64Vec3{
Planar64Vec3::new([Planar64::raw(x),Planar64::raw(y),Planar64::raw(z)]) Planar64Vec3::new([Planar64::raw(x),Planar64::raw(y),Planar64::raw(z)])
} }
#[inline] #[inline]
@ -654,19 +647,11 @@ pub struct Planar64Affine3{
pub translation:Planar64Vec3, pub translation:Planar64Vec3,
} }
impl Planar64Affine3{ impl Planar64Affine3{
pub const IDENTITY:Self=Self::new(mat3::identity(),vec3::ZERO);
#[inline] #[inline]
pub const fn new(matrix3:Planar64Mat3,translation:Planar64Vec3)->Self{ pub const fn new(matrix3:Planar64Mat3,translation:Planar64Vec3)->Self{
Self{matrix3,translation} Self{matrix3,translation}
} }
#[inline] #[inline]
pub const fn from_translation(translation:Planar64Vec3)->Self{
Self{
matrix3:mat3::identity(),
translation,
}
}
#[inline]
pub fn transform_point3(&self,point:Planar64Vec3)->vec3::Vector3<Fixed<2,64>>{ pub fn transform_point3(&self,point:Planar64Vec3)->vec3::Vector3<Fixed<2,64>>{
self.translation.fix_2()+self.matrix3*point self.translation.fix_2()+self.matrix3*point
} }

@ -1,6 +1,5 @@
pub mod bvh; pub mod bvh;
pub mod map; pub mod map;
pub mod ray;
pub mod run; pub mod run;
pub mod aabb; pub mod aabb;
pub mod model; pub mod model;

@ -1,5 +1,3 @@
use std::collections::HashMap;
use crate::integer::{Planar64Vec3,Planar64Affine3}; use crate::integer::{Planar64Vec3,Planar64Affine3};
use crate::gameplay_attributes; use crate::gameplay_attributes;
@ -125,87 +123,6 @@ pub struct Mesh{
pub physics_groups:Vec<IndexedPhysicsGroup>, pub physics_groups:Vec<IndexedPhysicsGroup>,
} }
#[derive(Default)]
pub struct MeshBuilder{
unique_pos:Vec<Planar64Vec3>,//Unit32Vec3
unique_normal:Vec<Planar64Vec3>,//Unit32Vec3
unique_tex:Vec<TextureCoordinate>,
unique_color:Vec<Color4>,
unique_vertices:Vec<IndexedVertex>,
pos_id_from:HashMap<Planar64Vec3,PositionId>,//Unit32Vec3
normal_id_from:HashMap<Planar64Vec3,NormalId>,//Unit32Vec3
tex_id_from:HashMap<[u32;2],TextureCoordinateId>,
color_id_from:HashMap<[u32;4],ColorId>,
vertex_id_from:HashMap<IndexedVertex,VertexId>,
}
impl MeshBuilder{
pub fn new()->Self{
Self::default()
}
pub fn build(
self,
polygon_groups:Vec<PolygonGroup>,
graphics_groups:Vec<IndexedGraphicsGroup>,
physics_groups:Vec<IndexedPhysicsGroup>,
)->Mesh{
let MeshBuilder{
unique_pos,
unique_normal,
unique_tex,
unique_color,
unique_vertices,
..
}=self;
Mesh{
unique_pos,
unique_normal,
unique_tex,
unique_color,
unique_vertices,
polygon_groups,
graphics_groups,
physics_groups,
}
}
pub fn acquire_pos_id(&mut self,pos:Planar64Vec3)->PositionId{
*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);
pos_id
})
}
pub fn acquire_normal_id(&mut self,normal:Planar64Vec3)->NormalId{
*self.normal_id_from.entry(normal).or_insert_with(||{
let normal_id=NormalId::new(self.unique_normal.len() as u32);
self.unique_normal.push(normal);
normal_id
})
}
pub fn acquire_tex_id(&mut self,tex:TextureCoordinate)->TextureCoordinateId{
let h=tex.to_array().map(f32::to_bits);
*self.tex_id_from.entry(h).or_insert_with(||{
let tex_id=TextureCoordinateId::new(self.unique_tex.len() as u32);
self.unique_tex.push(tex);
tex_id
})
}
pub fn acquire_color_id(&mut self,color:Color4)->ColorId{
let h=color.to_array().map(f32::to_bits);
*self.color_id_from.entry(h).or_insert_with(||{
let color_id=ColorId::new(self.unique_color.len() as u32);
self.unique_color.push(color);
color_id
})
}
pub fn acquire_vertex_id(&mut self,vertex:IndexedVertex)->VertexId{
*self.vertex_id_from.entry(vertex.clone()).or_insert_with(||{
let vertex_id=VertexId::new(self.unique_vertices.len() as u32);
self.unique_vertices.push(vertex);
vertex_id
})
}
}
#[derive(Debug,Clone,Copy,Hash,id::Id,Eq,PartialEq)] #[derive(Debug,Clone,Copy,Hash,id::Id,Eq,PartialEq)]
pub struct ModelId(u32); pub struct ModelId(u32);
pub struct Model{ pub struct Model{

@ -1,7 +1,6 @@
use crate::mouse::MouseState; use crate::mouse::MouseState;
use crate::gameplay_modes::{ModeId,StageId};
#[derive(Clone,Copy,Hash,Eq,PartialEq,Ord,PartialOrd,Debug)] #[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
pub enum TimeInner{} pub enum TimeInner{}
pub type Time=crate::integer::Time<TimeInner>; pub type Time=crate::integer::Time<TimeInner>;
@ -43,11 +42,10 @@ pub enum ModeInstruction{
/// This forgets all inputs and settings which need to be reapplied. /// This forgets all inputs and settings which need to be reapplied.
Reset, Reset,
/// Restart: Teleport to the start zone. /// Restart: Teleport to the start zone.
/// This runs when you press R or teleport to a bonus Restart,
Restart(ModeId),
/// Spawn: Teleport to a specific mode's spawn /// Spawn: Teleport to a specific mode's spawn
/// This runs when the map loads to put you at the map lobby /// Sets current mode & spawn
Spawn(ModeId,StageId), Spawn(crate::gameplay_modes::ModeId,crate::gameplay_modes::StageId),
} }
#[derive(Clone,Debug)] #[derive(Clone,Debug)]
pub enum MiscInstruction{ pub enum MiscInstruction{

@ -1,20 +0,0 @@
use ratio_ops::ratio::Ratio;
use crate::integer::{self,Planar64,Planar64Vec3};
pub struct Ray{
pub origin:Planar64Vec3,
pub direction:Planar64Vec3,
}
impl Ray{
pub fn extrapolate<Num,Den,N1,T1>(&self,t:Ratio<Num,Den>)->Planar64Vec3
where
Num:Copy,
Den:Copy,
Num:core::ops::Mul<Planar64,Output=N1>,
Planar64:core::ops::Mul<Den,Output=N1>,
N1:integer::Divide<Den,Output=T1>,
T1:integer::Fix<Planar64>,
{
self.origin+self.direction.map(|elem|(t*elem).divide().fix())
}
}

@ -2,7 +2,7 @@ use crate::timer::{TimerFixed,Realtime,Paused,Unpaused};
use crate::physics::{TimeInner as PhysicsTimeInner,Time as PhysicsTime}; use crate::physics::{TimeInner as PhysicsTimeInner,Time as PhysicsTime};
#[derive(Clone,Copy,Hash,Eq,PartialEq,Ord,PartialOrd,Debug)] #[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
pub enum TimeInner{} pub enum TimeInner{}
pub type Time=crate::integer::Time<TimeInner>; pub type Time=crate::integer::Time<TimeInner>;
@ -76,7 +76,7 @@ impl Run{
match &self.state{ match &self.state{
RunState::Created=>Time::ZERO, RunState::Created=>Time::ZERO,
RunState::Started{timer}=>timer.time(time), RunState::Started{timer}=>timer.time(time),
RunState::Finished{timer}=>timer.time(), RunState::Finished{timer}=>timer.time(time),
} }
} }
pub fn start(&mut self,time:PhysicsTime)->Result<(),Error>{ pub fn start(&mut self,time:PhysicsTime)->Result<(),Error>{
@ -110,10 +110,4 @@ impl Run{
self.flagged=Some(flag_reason); self.flagged=Some(flag_reason);
} }
} }
pub fn get_finish_time(&self)->Option<Time>{
match &self.state{
RunState::Finished{timer}=>Some(timer.time()),
_=>None,
}
}
} }

@ -1,3 +1,3 @@
#[derive(Clone,Copy,Hash,Eq,PartialEq,Ord,PartialOrd,Debug)] #[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
pub enum TimeInner{} pub enum TimeInner{}
pub type Time=crate::integer::Time<TimeInner>; pub type Time=crate::integer::Time<TimeInner>;

@ -23,7 +23,7 @@ impl PauseState for Unpaused{
} }
#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)] #[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
pub enum Inner{} enum Inner{}
type InnerTime=Time<Inner>; type InnerTime=Time<Inner>;
#[derive(Clone,Copy,Debug)] #[derive(Clone,Copy,Debug)]
@ -157,7 +157,7 @@ impl<T:TimerState> TimerFixed<T,Paused>
where Time<T::In>:Copy, where Time<T::In>:Copy,
{ {
pub fn into_unpaused(self,time:Time<T::In>)->TimerFixed<T,Unpaused>{ pub fn into_unpaused(self,time:Time<T::In>)->TimerFixed<T,Unpaused>{
let new_time=self.time(); let new_time=self.time(time);
let mut timer=TimerFixed{ let mut timer=TimerFixed{
state:self.state, state:self.state,
_paused:Unpaused, _paused:Unpaused,
@ -165,9 +165,6 @@ impl<T:TimerState> TimerFixed<T,Paused>
timer.set_time(time,new_time); timer.set_time(time,new_time);
timer timer
} }
pub fn time(&self)->Time<T::Out>{
self.state.get_offset().coerce()
}
} }
impl<T:TimerState> TimerFixed<T,Unpaused> impl<T:TimerState> TimerFixed<T,Unpaused>
where Time<T::In>:Copy, where Time<T::In>:Copy,
@ -181,9 +178,6 @@ impl<T:TimerState> TimerFixed<T,Unpaused>
timer.set_time(time,new_time); timer.set_time(time,new_time);
timer timer
} }
pub fn time(&self,time:Time<T::In>)->Time<T::Out>{
self.state.get_time(time)
}
} }
//the new constructor and time queries are generic across both //the new constructor and time queries are generic across both
@ -205,6 +199,12 @@ impl<T:TimerState,P:PauseState> TimerFixed<T,P>{
pub fn into_state(self)->T{ pub fn into_state(self)->T{
self.state self.state
} }
pub fn time(&self,time:Time<T::In>)->Time<T::Out>{
match P::IS_PAUSED{
true=>self.state.get_offset().coerce(),
false=>self.state.get_time(time),
}
}
pub fn set_time(&mut self,time:Time<T::In>,new_time:Time<T::Out>){ pub fn set_time(&mut self,time:Time<T::In>,new_time:Time<T::Out>){
match P::IS_PAUSED{ match P::IS_PAUSED{
true=>self.state.set_offset(new_time.coerce()), true=>self.state.set_offset(new_time.coerce()),
@ -256,7 +256,7 @@ impl<T:TimerState> Timer<T>
} }
pub fn time(&self,time:Time<T::In>)->Time<T::Out>{ pub fn time(&self,time:Time<T::In>)->Time<T::Out>{
match self{ match self{
Self::Paused(timer)=>timer.time(), Self::Paused(timer)=>timer.time(time),
Self::Unpaused(timer)=>timer.time(time), Self::Unpaused(timer)=>timer.time(time),
} }
} }
@ -329,7 +329,7 @@ mod test{
//create a paused timer that reads 0s //create a paused timer that reads 0s
let timer=TimerFixed::<Scaled<Parent,Calculated>,Paused>::from_state(Scaled::new(0.5f32.try_into().unwrap(),sec!(0))); let timer=TimerFixed::<Scaled<Parent,Calculated>,Paused>::from_state(Scaled::new(0.5f32.try_into().unwrap(),sec!(0)));
//the paused timer at 1 second should read 0s //the paused timer at 1 second should read 0s
assert_eq!(timer.time(),sec!(0)); assert_eq!(timer.time(sec!(1)),sec!(0));
//unpause it after one second //unpause it after one second
let timer=timer.into_unpaused(sec!(1)); let timer=timer.into_unpaused(sec!(1));
@ -339,7 +339,7 @@ mod test{
//pause the timer after 11 seconds //pause the timer after 11 seconds
let timer=timer.into_paused(sec!(11)); let timer=timer.into_paused(sec!(11));
//the paused timer at 20 seconds should read 5s //the paused timer at 20 seconds should read 5s
assert_eq!(timer.time(),sec!(5)); assert_eq!(timer.time(sec!(20)),sec!(5));
} }
#[test] #[test]
fn test_timer()->Result<(),Error>{ fn test_timer()->Result<(),Error>{

@ -1,5 +1,3 @@
// This whole thing should be a drive macro
pub trait Updatable<Updater>{ pub trait Updatable<Updater>{
fn update(&mut self,update:Updater); fn update(&mut self,update:Updater);
} }
@ -55,3 +53,4 @@ impl Updatable<OuterUpdate> for Outer{
} }
} }
} }
//*/

@ -1,7 +1,7 @@
[package] [package]
name = "strafesnet_deferred_loader" name = "strafesnet_deferred_loader"
version = "0.5.0" version = "0.4.1"
edition = "2024" edition = "2021"
repository = "https://git.itzana.me/StrafesNET/strafe-project" repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
description = "Acquire IDs for objects before loading them in bulk." description = "Acquire IDs for objects before loading them in bulk."
@ -9,5 +9,13 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = ["legacy"]
legacy = ["dep:url","dep:vbsp"]
#roblox = ["dep:lazy-regex"]
#source = ["dep:vbsp"]
[dependencies] [dependencies]
strafesnet_common = { version = "0.6.0", path = "../common", registry = "strafesnet" } strafesnet_common = { path = "../common", registry = "strafesnet" }
url = { version = "2.5.2", optional = true }
vbsp = { version = "0.6.0", optional = true }

@ -1,116 +0,0 @@
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};
#[derive(Clone,Copy,Debug)]
pub enum LoadFailureMode{
DefaultToNone,
Fatal,
}
pub struct RenderConfigDeferredLoader<H>{
texture_count:u32,
render_configs:Vec<RenderConfig>,
render_config_id_from_asset_id:HashMap<Option<H>,RenderConfigId>,
}
impl<H> RenderConfigDeferredLoader<H>{
pub fn new()->Self{
Self{
texture_count:0,
render_configs:Vec::new(),
render_config_id_from_asset_id:HashMap::new(),
}
}
}
impl<H:core::hash::Hash+Eq> RenderConfigDeferredLoader<H>{
pub fn acquire_render_config_id(&mut self,index:Option<H>)->RenderConfigId{
let some_texture=index.is_some();
*self.render_config_id_from_asset_id.entry(index).or_insert_with(||{
//create the render config.
let render_config=if some_texture{
let render_config=RenderConfig::texture(TextureId::new(self.texture_count));
self.texture_count+=1;
render_config
}else{
RenderConfig::default()
};
let render_id=RenderConfigId::new(self.render_configs.len() as u32);
self.render_configs.push(render_config);
render_id
})
}
pub fn into_indices(self)->impl Iterator<Item=H>{
self.render_config_id_from_asset_id.into_keys().flatten()
}
pub fn into_render_configs<L:Loader<Resource=Texture,Index=H>>(mut self,loader:&mut L,failure_mode:LoadFailureMode)->Result<RenderConfigs,L::Error>{
let mut sorted_textures=vec![None;self.texture_count as usize];
for (index_option,render_config_id) in self.render_config_id_from_asset_id{
let render_config=&mut self.render_configs[render_config_id.get() as usize];
if let (Some(index),Some(texture_id))=(index_option,render_config.texture){
let resource_result=loader.load(index);
let texture=match failure_mode{
// if texture fails to load, use no texture
LoadFailureMode::DefaultToNone=>match resource_result{
Ok(texture)=>Some(texture),
Err(e)=>{
render_config.texture=None;
println!("Error loading texture: {e}");
None
},
},
// loading failure is fatal
LoadFailureMode::Fatal=>Some(resource_result?)
};
sorted_textures[texture_id.get() as usize]=texture;
}
}
Ok(RenderConfigs::new(
sorted_textures,
self.render_configs,
))
}
}
pub struct MeshDeferredLoader<H>{
mesh_id_from_asset_id:HashMap<H,MeshId>,
}
impl<H> MeshDeferredLoader<H>{
pub fn new()->Self{
Self{
mesh_id_from_asset_id:HashMap::new(),
}
}
}
impl<H:core::hash::Hash+Eq> MeshDeferredLoader<H>{
pub fn acquire_mesh_id(&mut self,index:H)->MeshId{
let mesh_id=MeshId::new(self.mesh_id_from_asset_id.len() as u32);
*self.mesh_id_from_asset_id.entry(index).or_insert(mesh_id)
}
pub fn into_indices(self)->impl Iterator<Item=H>{
self.mesh_id_from_asset_id.into_keys()
}
pub fn into_meshes<L:Loader<Resource=Mesh,Index=H>>(self,loader:&mut L,failure_mode:LoadFailureMode)->Result<Meshes,L::Error>{
let mut mesh_list=vec![None;self.mesh_id_from_asset_id.len()];
for (index,mesh_id) in self.mesh_id_from_asset_id{
let resource_result=loader.load(index);
let mesh=match failure_mode{
// if mesh fails to load, use no mesh
LoadFailureMode::DefaultToNone=>match resource_result{
Ok(mesh)=>Some(mesh),
Err(e)=>{
println!("Error loading mesh: {e}");
None
},
},
// loading failure is fatal
LoadFailureMode::Fatal=>Some(resource_result?)
};
mesh_list[mesh_id.get() as usize]=mesh;
}
Ok(Meshes::new(mesh_list))
}
}

@ -1,5 +1,34 @@
#[cfg(feature="legacy")]
mod roblox_legacy;
#[cfg(feature="legacy")]
mod source_legacy;
#[cfg(feature="roblox")]
mod roblox;
#[cfg(feature="source")]
mod source;
#[cfg(any(feature="roblox",feature="legacy"))]
pub mod rbxassetid;
pub mod mesh;
pub mod loader;
pub mod texture; pub mod texture;
pub mod deferred_loader; #[cfg(any(feature="source",feature="legacy"))]
pub mod valve_mesh;
#[cfg(any(feature="roblox",feature="legacy"))]
pub mod roblox_mesh;
#[cfg(feature="legacy")]
pub fn roblox_legacy()->roblox_legacy::Loader{
roblox_legacy::Loader::new()
}
#[cfg(feature="legacy")]
pub fn source_legacy()->source_legacy::Loader{
source_legacy::Loader::new()
}
#[cfg(feature="roblox")]
pub fn roblox()->roblox::Loader{
roblox::Loader::new()
}
#[cfg(feature="source")]
pub fn source()->source::Loader{
source::Loader::new()
}

@ -1,8 +0,0 @@
use std::error::Error;
pub trait Loader{
type Error:Error;
type Index;
type Resource;
fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>;
}

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

@ -0,0 +1,48 @@
#[derive(Hash,Eq,PartialEq)]
pub struct RobloxAssetId(pub u64);
#[derive(Debug)]
#[allow(dead_code)]
pub struct StringWithError{
string:String,
error:RobloxAssetIdParseErr,
}
impl std::fmt::Display for StringWithError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for StringWithError{}
impl StringWithError{
const fn new(
string:String,
error:RobloxAssetIdParseErr,
)->Self{
Self{string,error}
}
}
#[derive(Debug)]
pub enum RobloxAssetIdParseErr{
Url(url::ParseError),
UnknownScheme,
ParseInt(std::num::ParseIntError),
MissingAssetId,
}
impl std::str::FromStr for RobloxAssetId{
type Err=StringWithError;
fn from_str(s:&str)->Result<Self,Self::Err>{
let url=url::Url::parse(s).map_err(|e|StringWithError::new(s.to_owned(),RobloxAssetIdParseErr::Url(e)))?;
let parsed_asset_id=match url.scheme(){
"rbxassetid"=>url.domain().ok_or_else(||StringWithError::new(s.to_owned(),RobloxAssetIdParseErr::MissingAssetId))?.parse(),
"http"|"https"=>{
let (_,asset_id)=url.query_pairs()
.find(|(id,_)|match id.as_ref(){
"ID"|"id"|"Id"|"iD"=>true,
_=>false,
}).ok_or_else(||StringWithError::new(s.to_owned(),RobloxAssetIdParseErr::MissingAssetId))?;
asset_id.parse()
},
_=>Err(StringWithError::new(s.to_owned(),RobloxAssetIdParseErr::UnknownScheme))?,
};
Ok(Self(parsed_asset_id.map_err(|e|StringWithError::new(s.to_owned(),RobloxAssetIdParseErr::ParseInt(e)))?))
}
}

@ -0,0 +1,112 @@
use std::io::Read;
use std::collections::HashMap;
use crate::roblox_mesh;
use crate::texture::{RenderConfigs,Texture};
use strafesnet_common::model::{MeshId,RenderConfig,RenderConfigId,TextureId};
use crate::rbxassetid::RobloxAssetId;
#[derive(Default)]
pub struct RenderConfigLoader{
texture_count:u32,
render_configs:Vec<RenderConfig>,
render_config_id_from_asset_id:HashMap<Option<RobloxAssetId>,RenderConfigId>,
}
impl RenderConfigLoader{
pub fn acquire_render_config_id(&mut self,name:Option<&str>)->RenderConfigId{
let render_id=RenderConfigId::new(self.render_config_id_from_asset_id.len() as u32);
let index=name.and_then(|name|{
match name.parse::<RobloxAssetId>(){
Ok(asset_id)=>Some(asset_id),
Err(e)=>{
println!("Failed to parse AssetId: {e}");
None
},
}
});
*self.render_config_id_from_asset_id.entry(index).or_insert_with(||{
//create the render config.
let render_config=if name.is_some(){
let render_config=RenderConfig::texture(TextureId::new(self.texture_count));
self.texture_count+=1;
render_config
}else{
RenderConfig::default()
};
self.render_configs.push(render_config);
render_id
})
}
}
#[derive(Default)]
pub struct MeshLoader{
mesh_id_from_asset_id:HashMap<Option<RobloxAssetId>,MeshId>,
}
impl MeshLoader{
pub fn acquire_mesh_id(&mut self,name:&str)->MeshId{
let mesh_id=MeshId::new(self.mesh_id_from_asset_id.len() as u32);
let index=match name.parse::<RobloxAssetId>(){
Ok(asset_id)=>Some(asset_id),
Err(e)=>{
println!("Failed to parse AssetId: {e}");
None
},
};
*self.mesh_id_from_asset_id.entry(index).or_insert(mesh_id)
}
pub fn load_meshes(&mut self)->Result<roblox_mesh::Meshes,std::io::Error>{
let mut mesh_data=vec![None;self.mesh_id_from_asset_id.len()];
for (asset_id_option,mesh_id) in &self.mesh_id_from_asset_id{
if let Some(asset_id)=asset_id_option{
if let Ok(mut file)=std::fs::File::open(format!("meshes/{}",asset_id.0)){
//TODO: parallel
let mut data=Vec::<u8>::new();
file.read_to_end(&mut data)?;
mesh_data[mesh_id.get() as usize]=Some(roblox_mesh::RobloxMeshData::new(data));
}else{
println!("[roblox_legacy] no mesh name={}",asset_id.0);
}
}
}
Ok(roblox_mesh::Meshes::new(mesh_data))
}
}
pub struct Loader{
render_config_loader:RenderConfigLoader,
mesh_loader:MeshLoader,
}
impl Loader{
pub fn new()->Self{
Self{
render_config_loader:RenderConfigLoader::default(),
mesh_loader:MeshLoader::default(),
}
}
pub fn get_inner_mut(&mut self)->(&mut RenderConfigLoader,&mut MeshLoader){
(&mut self.render_config_loader,&mut self.mesh_loader)
}
pub fn into_render_configs(mut self)->Result<RenderConfigs,std::io::Error>{
let mut sorted_textures=vec![None;self.render_config_loader.texture_count as usize];
for (asset_id_option,render_config_id) in self.render_config_loader.render_config_id_from_asset_id{
let render_config=self.render_config_loader.render_configs.get_mut(render_config_id.get() as usize).unwrap();
if let (Some(asset_id),Some(texture_id))=(asset_id_option,render_config.texture){
if let Ok(mut file)=std::fs::File::open(format!("textures/{}.dds",asset_id.0)){
//TODO: parallel
let mut data=Vec::<u8>::new();
file.read_to_end(&mut data)?;
sorted_textures[texture_id.get() as usize]=Some(Texture::ImageDDS(data));
}else{
//texture failed to load
render_config.texture=None;
}
}
}
Ok(RenderConfigs::new(
sorted_textures,
self.render_config_loader.render_configs,
))
}
}

@ -0,0 +1,30 @@
use strafesnet_common::model::MeshId;
#[derive(Clone)]
pub struct RobloxMeshData(Vec<u8>);
impl RobloxMeshData{
pub(crate) fn new(data:Vec<u8>)->Self{
Self(data)
}
pub fn get(self)->Vec<u8>{
self.0
}
}
pub struct Meshes{
meshes:Vec<Option<RobloxMeshData>>,
}
impl Meshes{
pub(crate) const fn new(meshes:Vec<Option<RobloxMeshData>>)->Self{
Self{
meshes,
}
}
pub fn get_texture(&self,texture_id:MeshId)->Option<&RobloxMeshData>{
self.meshes.get(texture_id.get() as usize)?.as_ref()
}
pub fn into_iter(self)->impl Iterator<Item=(MeshId,RobloxMeshData)>{
self.meshes.into_iter().enumerate().filter_map(|(mesh_id,maybe_mesh)|
maybe_mesh.map(|mesh|(MeshId::new(mesh_id as u32),mesh))
)
}
}

@ -0,0 +1,102 @@
use std::io::Read;
use std::collections::HashMap;
use crate::valve_mesh;
use crate::texture::{Texture,RenderConfigs};
use strafesnet_common::model::{MeshId,TextureId,RenderConfig,RenderConfigId};
pub struct RenderConfigLoader{
texture_count:u32,
render_configs:Vec<RenderConfig>,
texture_paths:HashMap<Option<Box<str>>,RenderConfigId>,
}
impl RenderConfigLoader{
pub fn acquire_render_config_id(&mut self,name:Option<&str>)->RenderConfigId{
let render_id=RenderConfigId::new(self.texture_paths.len() as u32);
*self.texture_paths.entry(name.map(Into::into)).or_insert_with(||{
//create the render config.
let render_config=if name.is_some(){
let render_config=RenderConfig::texture(TextureId::new(self.texture_count));
self.texture_count+=1;
render_config
}else{
RenderConfig::default()
};
self.render_configs.push(render_config);
render_id
})
}
}
pub struct MeshLoader{
mesh_paths:HashMap<Box<str>,MeshId>,
}
impl MeshLoader{
pub fn acquire_mesh_id(&mut self,name:&str)->MeshId{
let mesh_id=MeshId::new(self.mesh_paths.len() as u32);
*self.mesh_paths.entry(name.into()).or_insert(mesh_id)
}
//load_meshes should look like load_textures
pub fn load_meshes(&mut self,bsp:&vbsp::Bsp)->valve_mesh::Meshes{
let mut mesh_data=vec![None;self.mesh_paths.len()];
for (mesh_path,mesh_id) in &self.mesh_paths{
let mesh_path_lower=mesh_path.to_lowercase();
//.mdl, .vvd, .dx90.vtx
let path=std::path::PathBuf::from(mesh_path_lower.as_str());
let mut vvd_path=path.clone();
let mut vtx_path=path.clone();
vvd_path.set_extension("vvd");
vtx_path.set_extension("dx90.vtx");
match (bsp.pack.get(mesh_path_lower.as_str()),bsp.pack.get(vvd_path.as_os_str().to_str().unwrap()),bsp.pack.get(vtx_path.as_os_str().to_str().unwrap())){
(Ok(Some(mdl_file)),Ok(Some(vvd_file)),Ok(Some(vtx_file)))=>{
mesh_data[mesh_id.get() as usize]=Some(valve_mesh::ModelData{
mdl:valve_mesh::MdlData::new(mdl_file),
vtx:valve_mesh::VtxData::new(vtx_file),
vvd:valve_mesh::VvdData::new(vvd_file),
});
},
_=>println!("no model name={}",mesh_path),
}
}
valve_mesh::Meshes::new(mesh_data)
}
}
pub struct Loader{
render_config_loader:RenderConfigLoader,
mesh_loader:MeshLoader,
}
impl Loader{
pub fn new()->Self{
Self{
render_config_loader:RenderConfigLoader{
texture_count:0,
texture_paths:HashMap::new(),
render_configs:Vec::new(),
},
mesh_loader:MeshLoader{mesh_paths:HashMap::new()},
}
}
pub fn get_inner_mut(&mut self)->(&mut RenderConfigLoader,&mut MeshLoader){
(&mut self.render_config_loader,&mut self.mesh_loader)
}
pub fn into_render_configs(mut self)->Result<RenderConfigs,std::io::Error>{
let mut sorted_textures=vec![None;self.render_config_loader.texture_count as usize];
for (texture_path,render_config_id) in self.render_config_loader.texture_paths{
let render_config=self.render_config_loader.render_configs.get_mut(render_config_id.get() as usize).unwrap();
if let (Some(texture_path),Some(texture_id))=(texture_path,render_config.texture){
if let Ok(mut file)=std::fs::File::open(format!("textures/{}.dds",texture_path)){
//TODO: parallel
let mut data=Vec::<u8>::new();
file.read_to_end(&mut data)?;
sorted_textures[texture_id.get() as usize]=Some(Texture::ImageDDS(data));
}else{
//texture failed to load
render_config.texture=None;
}
}
}
Ok(RenderConfigs::new(
sorted_textures,
self.render_config_loader.render_configs,
))
}
}

@ -0,0 +1,60 @@
use strafesnet_common::model::MeshId;
//duplicate this code for now
#[derive(Clone)]
pub struct MdlData(Vec<u8>);
impl MdlData{
pub const fn new(value:Vec<u8>)->Self{
Self(value)
}
pub fn get(self)->Vec<u8>{
self.0
}
}
#[derive(Clone)]
pub struct VtxData(Vec<u8>);
impl VtxData{
pub const fn new(value:Vec<u8>)->Self{
Self(value)
}
pub fn get(self)->Vec<u8>{
self.0
}
}
#[derive(Clone)]
pub struct VvdData(Vec<u8>);
impl VvdData{
pub const fn new(value:Vec<u8>)->Self{
Self(value)
}
pub fn get(self)->Vec<u8>{
self.0
}
}
#[derive(Clone)]
pub struct ModelData{
pub mdl:MdlData,
pub vtx:VtxData,
pub vvd:VvdData,
}
//meshes is more prone to failure
pub struct Meshes{
meshes:Vec<Option<ModelData>>,
}
impl Meshes{
pub(crate) const fn new(meshes:Vec<Option<ModelData>>)->Self{
Self{
meshes,
}
}
pub fn get_texture(&self,texture_id:MeshId)->Option<&ModelData>{
self.meshes.get(texture_id.get() as usize)?.as_ref()
}
pub fn into_iter(self)->impl Iterator<Item=(MeshId,ModelData)>{
self.meshes.into_iter().enumerate().filter_map(|(mesh_id,maybe_mesh)|
maybe_mesh.map(|mesh|(MeshId::new(mesh_id as u32),mesh))
)
}
}

@ -1,7 +1,7 @@
[package] [package]
name = "fixed_wide" name = "fixed_wide"
version = "0.1.2" version = "0.1.1"
edition = "2024" edition = "2021"
repository = "https://git.itzana.me/StrafesNET/strafe-project" repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
description = "Fixed point numbers with optional widening Mul operator." description = "Fixed point numbers with optional widening Mul operator."
@ -17,4 +17,4 @@ zeroes=["dep:arrayvec"]
bnum = "0.12.0" bnum = "0.12.0"
arrayvec = { version = "0.7.6", optional = true } arrayvec = { version = "0.7.6", optional = true }
paste = "1.0.15" paste = "1.0.15"
ratio_ops = { version = "0.1.0", path = "../ratio_ops", registry = "strafesnet", optional = true } ratio_ops = { path = "../ratio_ops", registry = "strafesnet", optional = true }

@ -57,7 +57,7 @@ fn from_f32(){
assert_eq!(b,Ok(a)); assert_eq!(b,Ok(a));
//I32F32::MIN hits a special case since it's not representable as a positive signed integer //I32F32::MIN hits a special case since it's not representable as a positive signed integer
//TODO: don't return an overflow because this is technically possible //TODO: don't return an overflow because this is technically possible
let _a=I32F32::MIN; let a=I32F32::MIN;
let b:Result<I32F32,_>=Into::<f32>::into(I32F32::MIN).try_into(); let b:Result<I32F32,_>=Into::<f32>::into(I32F32::MIN).try_into();
assert_eq!(b,Err(crate::fixed::FixedFromFloatError::Overflow)); assert_eq!(b,Err(crate::fixed::FixedFromFloatError::Overflow));
//16 is within the 24 bits of float precision //16 is within the 24 bits of float precision

@ -1,7 +1,7 @@
[package] [package]
name = "linear_ops" name = "linear_ops"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2021"
repository = "https://git.itzana.me/StrafesNET/strafe-project" repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
description = "Vector/Matrix operations using trait bounds." description = "Vector/Matrix operations using trait bounds."
@ -14,8 +14,8 @@ fixed-wide=["dep:fixed_wide","dep:paste"]
deferred-division=["dep:ratio_ops"] deferred-division=["dep:ratio_ops"]
[dependencies] [dependencies]
ratio_ops = { version = "0.1.0", path = "../ratio_ops", registry = "strafesnet", optional = true } ratio_ops = { path = "../ratio_ops", registry = "strafesnet", optional = true }
fixed_wide = { version = "0.1.2", path = "../fixed_wide", registry = "strafesnet", optional = true } fixed_wide = { path = "../fixed_wide", registry = "strafesnet", optional = true }
paste = { version = "1.0.15", optional = true } paste = { version = "1.0.15", optional = true }
[dev-dependencies] [dev-dependencies]

@ -1,6 +1,5 @@
use crate::vector::Vector; use crate::vector::Vector;
#[repr(transparent)]
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq)] #[derive(Clone,Copy,Debug,Hash,Eq,PartialEq)]
pub struct Matrix<const X:usize,const Y:usize,T>{ pub struct Matrix<const X:usize,const Y:usize,T>{
pub(crate) array:[[T;Y];X], pub(crate) array:[[T;Y];X],

@ -3,7 +3,6 @@
/// v.x += v.z; /// v.x += v.z;
/// println!("v.x={}",v.x); /// println!("v.x={}",v.x);
#[repr(transparent)]
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq)] #[derive(Clone,Copy,Debug,Hash,Eq,PartialEq)]
pub struct Vector<const N:usize,T>{ pub struct Vector<const N:usize,T>{
pub(crate) array:[T;N], pub(crate) array:[T;N],

@ -1,7 +1,7 @@
[package] [package]
name = "ratio_ops" name = "ratio_ops"
version = "0.1.1" version = "0.1.0"
edition = "2024" edition = "2021"
repository = "https://git.itzana.me/StrafesNET/strafe-project" repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
description = "Ratio operations using trait bounds for avoiding division like the plague." description = "Ratio operations using trait bounds for avoiding division like the plague."

@ -268,35 +268,30 @@ impl<LhsNum,LhsDen,RhsNum,RhsDen,T,U> PartialEq<Ratio<RhsNum,RhsDen>> for Ratio<
} }
impl<Num,Den> Eq for Ratio<Num,Den> where Self:PartialEq{} impl<Num,Den> Eq for Ratio<Num,Den> where Self:PartialEq{}
// Wow! These were both completely wrong!
// Idea: use a 'signed' trait instead of parity and float the sign to the numerator.
impl<LhsNum,LhsDen,RhsNum,RhsDen,T,U> PartialOrd<Ratio<RhsNum,RhsDen>> for Ratio<LhsNum,LhsDen> impl<LhsNum,LhsDen,RhsNum,RhsDen,T,U> PartialOrd<Ratio<RhsNum,RhsDen>> for Ratio<LhsNum,LhsDen>
where where
LhsNum:Copy, LhsNum:Copy,
LhsDen:Copy+Parity, LhsDen:Copy,
RhsNum:Copy, RhsNum:Copy,
RhsDen:Copy+Parity, RhsDen:Copy,
LhsNum:core::ops::Mul<RhsDen,Output=T>, LhsNum:core::ops::Mul<RhsDen,Output=T>,
LhsDen:core::ops::Mul<RhsNum,Output=T>,
RhsNum:core::ops::Mul<LhsDen,Output=U>, RhsNum:core::ops::Mul<LhsDen,Output=U>,
RhsDen:core::ops::Mul<LhsNum,Output=U>, T:PartialOrd<U>,
T:PartialOrd<U>+Ord,
{ {
#[inline] #[inline]
fn partial_cmp(&self,&other:&Ratio<RhsNum,RhsDen>)->Option<core::cmp::Ordering>{ fn partial_cmp(&self,other:&Ratio<RhsNum,RhsDen>)->Option<core::cmp::Ordering>{
self.partial_cmp_ratio(other) (self.num*other.den).partial_cmp(&(other.num*self.den))
} }
} }
impl<Num,Den,T> Ord for Ratio<Num,Den> impl<Num,Den,T> Ord for Ratio<Num,Den>
where where
Num:Copy, Num:Copy,
Den:Copy+Parity, Den:Copy,
Num:core::ops::Mul<Den,Output=T>, Num:core::ops::Mul<Den,Output=T>,
Den:core::ops::Mul<Num,Output=T>,
T:Ord, T:Ord,
{ {
#[inline] #[inline]
fn cmp(&self,&other:&Self)->std::cmp::Ordering{ fn cmp(&self,other:&Self)->std::cmp::Ordering{
self.cmp_ratio(other) (self.num*other.den).cmp(&(other.num*self.den))
} }
} }

@ -1,7 +1,7 @@
[package] [package]
name = "strafesnet_rbx_loader" name = "strafesnet_rbx_loader"
version = "0.6.0" version = "0.5.2"
edition = "2024" edition = "2021"
repository = "https://git.itzana.me/StrafesNET/strafe-project" repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
description = "Convert Roblox place and model files to StrafesNET data structures." description = "Convert Roblox place and model files to StrafesNET data structures."
@ -11,14 +11,12 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
[dependencies] [dependencies]
bytemuck = "1.14.3" bytemuck = "1.14.3"
glam = "0.30.0" glam = "0.29.0"
lazy-regex = "3.1.0" lazy-regex = "3.1.0"
rbx_binary = { version = "0.7.4", registry = "strafesnet" } rbx_binary = { version = "0.7.4", registry = "strafesnet" }
rbx_dom_weak = { version = "2.7.0", registry = "strafesnet" } rbx_dom_weak = { version = "2.7.0", registry = "strafesnet" }
rbx_mesh = "0.3.1" rbx_mesh = "0.1.2"
rbx_reflection_database = { version = "0.2.10", registry = "strafesnet" } rbx_reflection_database = { version = "0.2.10", registry = "strafesnet" }
rbx_xml = { version = "0.13.3", registry = "strafesnet" } rbx_xml = { version = "0.13.3", registry = "strafesnet" }
rbxassetid = { version = "0.1.0", path = "../rbxassetid", registry = "strafesnet" } roblox_emulator = { path = "../roblox_emulator", registry = "strafesnet" }
roblox_emulator = { version = "0.4.7", path = "../roblox_emulator", registry = "strafesnet" } strafesnet_common = { path = "../common", registry = "strafesnet" }
strafesnet_common = { version = "0.6.0", path = "../common", registry = "strafesnet" }
strafesnet_deferred_loader = { version = "0.5.0", path = "../deferred_loader", registry = "strafesnet" }

@ -1,11 +1,8 @@
use std::io::Read; use std::io::Read;
use rbx_dom_weak::WeakDom; use rbx_dom_weak::WeakDom;
use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLoader,RenderConfigDeferredLoader};
mod rbx; mod rbx;
mod mesh; mod mesh;
mod union;
pub mod loader;
mod primitives; mod primitives;
pub mod data{ pub mod data{
@ -33,9 +30,6 @@ impl Model{
let services=context.convert_into_place(); let services=context.convert_into_place();
Place{dom,services} Place{dom,services}
} }
pub fn to_snf(&self,failure_mode:LoadFailureMode)->Result<strafesnet_common::map::CompleteMap,LoadError>{
to_snf(self,failure_mode)
}
} }
impl AsRef<WeakDom> for Model{ impl AsRef<WeakDom> for Model{
fn as_ref(&self)->&WeakDom{ fn as_ref(&self)->&WeakDom{
@ -48,7 +42,7 @@ pub struct Place{
services:roblox_emulator::context::Services, services:roblox_emulator::context::Services,
} }
impl Place{ impl Place{
pub fn new(dom:WeakDom)->Option<Self>{ fn new(dom:WeakDom)->Option<Self>{
let context=roblox_emulator::context::Context::from_ref(&dom); let context=roblox_emulator::context::Context::from_ref(&dom);
Some(Self{ Some(Self{
services:context.find_services()?, services:context.find_services()?,
@ -67,9 +61,6 @@ impl Place{
} }
} }
} }
pub fn to_snf(&self,failure_mode:LoadFailureMode)->Result<strafesnet_common::map::CompleteMap,LoadError>{
to_snf(self,failure_mode)
}
} }
impl AsRef<WeakDom> for Place{ impl AsRef<WeakDom> for Place{
fn as_ref(&self)->&WeakDom{ fn as_ref(&self)->&WeakDom{
@ -101,49 +92,16 @@ pub fn read<R:Read>(input:R)->Result<Model,ReadError>{
} }
} }
#[derive(Debug)] //ConvertError
pub enum LoadError{
Texture(loader::TextureError), pub fn convert<AcquireRenderConfigId,AcquireMeshId>(
Mesh(loader::MeshError), dom:impl AsRef<WeakDom>,
} acquire_render_config_id:AcquireRenderConfigId,
impl std::fmt::Display for LoadError{ acquire_mesh_id:AcquireMeshId
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ )->rbx::PartialMap1
write!(f,"{self:?}") where
} AcquireRenderConfigId:FnMut(Option<&str>)->strafesnet_common::model::RenderConfigId,
} AcquireMeshId:FnMut(&str)->strafesnet_common::model::MeshId,
impl std::error::Error for LoadError{} {
impl From<loader::TextureError> for LoadError{ rbx::convert(&dom.as_ref(),acquire_render_config_id,acquire_mesh_id)
fn from(value:loader::TextureError)->Self{
Self::Texture(value)
}
}
impl From<loader::MeshError> for LoadError{
fn from(value:loader::MeshError)->Self{
Self::Mesh(value)
}
}
fn to_snf(dom:impl AsRef<WeakDom>,failure_mode:LoadFailureMode)->Result<strafesnet_common::map::CompleteMap,LoadError>{
let dom=dom.as_ref();
let mut texture_deferred_loader=RenderConfigDeferredLoader::new();
let mut mesh_deferred_loader=MeshDeferredLoader::new();
let map_step1=rbx::convert(
dom,
&mut texture_deferred_loader,
&mut mesh_deferred_loader,
);
let mut mesh_loader=loader::MeshLoader::new();
let meshpart_meshes=mesh_deferred_loader.into_meshes(&mut mesh_loader,failure_mode).map_err(LoadError::Mesh)?;
let map_step2=map_step1.add_meshpart_meshes_and_calculate_attributes(meshpart_meshes);
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)
} }

@ -1,191 +0,0 @@
use std::io::Read;
use rbxassetid::{RobloxAssetId,RobloxAssetIdParseErr};
use strafesnet_common::model::Mesh;
use strafesnet_deferred_loader::{loader::Loader,texture::Texture};
use crate::data::RobloxMeshBytes;
use crate::rbx::RobloxFaceTextureDescription;
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();
file.read_to_end(&mut data)?;
Ok(data)
}
#[allow(dead_code)]
#[derive(Debug)]
pub enum TextureError{
Io(std::io::Error),
RobloxAssetIdParse(RobloxAssetIdParseErr),
}
impl std::fmt::Display for TextureError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for TextureError{}
impl From<std::io::Error> for TextureError{
fn from(value:std::io::Error)->Self{
Self::Io(value)
}
}
impl From<RobloxAssetIdParseErr> for TextureError{
fn from(value:RobloxAssetIdParseErr)->Self{
Self::RobloxAssetIdParse(value)
}
}
pub struct TextureLoader<'a>(std::marker::PhantomData<&'a ()>);
impl TextureLoader<'_>{
pub fn new()->Self{
Self(std::marker::PhantomData)
}
}
impl<'a> Loader for TextureLoader<'a>{
type Error=TextureError;
type Index=&'a str;
type Resource=Texture;
fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>{
let RobloxAssetId(asset_id)=index.parse()?;
let file_name=format!("textures/{}.dds",asset_id);
let data=read_entire_file(file_name)?;
Ok(Texture::ImageDDS(data))
}
}
#[allow(dead_code)]
#[derive(Debug)]
pub enum MeshError{
Io(std::io::Error),
RobloxAssetIdParse(RobloxAssetIdParseErr),
Mesh(crate::mesh::Error),
Union(crate::union::Error),
DecodeBinary(rbx_binary::DecodeError),
OneChildPolicy,
MissingInstance,
}
impl std::fmt::Display for MeshError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for MeshError{}
impl From<std::io::Error> for MeshError{
fn from(value:std::io::Error)->Self{
Self::Io(value)
}
}
impl From<RobloxAssetIdParseErr> for MeshError{
fn from(value:RobloxAssetIdParseErr)->Self{
Self::RobloxAssetIdParse(value)
}
}
impl From<crate::mesh::Error> for MeshError{
fn from(value:crate::mesh::Error)->Self{
Self::Mesh(value)
}
}
impl From<crate::union::Error> for MeshError{
fn from(value:crate::union::Error)->Self{
Self::Union(value)
}
}
impl From<rbx_binary::DecodeError> for MeshError{
fn from(value:rbx_binary::DecodeError)->Self{
Self::DecodeBinary(value)
}
}
#[derive(Hash,Eq,PartialEq)]
pub enum MeshType<'a>{
FileMesh,
Union{
mesh_data:&'a [u8],
physics_data:&'a [u8],
size_float_bits:[u32;3],
part_texture_description:[Option<RobloxFaceTextureDescription>;6],
},
}
#[derive(Hash,Eq,PartialEq)]
pub struct MeshIndex<'a>{
mesh_type:MeshType<'a>,
content:&'a str,
}
impl MeshIndex<'_>{
pub fn file_mesh(content:&str)->MeshIndex{
MeshIndex{
mesh_type:MeshType::FileMesh,
content,
}
}
pub fn union<'a>(
content:&'a str,
mesh_data:&'a [u8],
physics_data:&'a [u8],
size:&rbx_dom_weak::types::Vector3,
part_texture_description:crate::rbx::RobloxPartDescription,
)->MeshIndex<'a>{
MeshIndex{
mesh_type:MeshType::Union{
mesh_data,
physics_data,
size_float_bits:[size.x.to_bits(),size.y.to_bits(),size.z.to_bits()],
part_texture_description,
},
content,
}
}
}
pub struct MeshLoader<'a>(std::marker::PhantomData<&'a ()>);
impl MeshLoader<'_>{
pub fn new()->Self{
Self(std::marker::PhantomData)
}
}
impl<'a> Loader for MeshLoader<'a>{
type Error=MeshError;
type Index=MeshIndex<'a>;
type Resource=Mesh;
fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>{
let mesh=match index.mesh_type{
MeshType::FileMesh=>{
let RobloxAssetId(asset_id)=index.content.parse()?;
let file_name=format!("meshes/{}",asset_id);
let data=read_entire_file(file_name)?;
crate::mesh::convert(RobloxMeshBytes::new(data))?
},
MeshType::Union{mut physics_data,mut mesh_data,size_float_bits,part_texture_description}=>{
// decode asset
let size=glam::Vec3::from_array(size_float_bits.map(f32::from_bits));
if !index.content.is_empty()&&(physics_data.is_empty()||mesh_data.is_empty()){
let RobloxAssetId(asset_id)=index.content.parse()?;
let file_name=format!("unions/{}",asset_id);
let data=read_entire_file(file_name)?;
let dom=rbx_binary::from_reader(std::io::Cursor::new(data))?;
let &[referent]=dom.root().children()else{
return Err(MeshError::OneChildPolicy);
};
let Some(instance)=dom.get_by_ref(referent)else{
return Err(MeshError::MissingInstance);
};
if physics_data.is_empty(){
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get("PhysicsData"){
physics_data=data.as_ref();
}
}
if mesh_data.is_empty(){
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get("MeshData"){
mesh_data=data.as_ref();
}
}
crate::union::convert(physics_data,mesh_data,size,part_texture_description)?
}else{
crate::union::convert(physics_data,mesh_data,size,part_texture_description)?
}
},
};
Ok(mesh)
}
}

@ -1,9 +1,8 @@
use std::collections::HashMap; use std::collections::HashMap;
use rbx_mesh::mesh::{Vertex2,Vertex2Truncated}; 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::{integer::vec3,model::{self, ColorId, IndexedVertex, NormalId, PolygonGroup, PolygonList, PositionId, TextureCoordinateId, VertexId}};
#[allow(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub enum Error{ pub enum Error{
Planar64Vec3(strafesnet_common::integer::Planar64TryFromFloatError), Planar64Vec3(strafesnet_common::integer::Planar64TryFromFloatError),
@ -205,13 +204,7 @@ pub fn convert(roblox_mesh_bytes:crate::data::RobloxMeshBytes)->Result<model::Me
unique_vertices, unique_vertices,
polygon_groups, polygon_groups,
//these should probably be moved to the model... //these should probably be moved to the model...
//but what if models want to use the same texture graphics_groups:Vec::new(),
graphics_groups: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(), physics_groups:Vec::new(),
}) })
} }

@ -1,4 +1,4 @@
use strafesnet_common::model::{Color4,TextureCoordinate,Mesh,IndexedGraphicsGroup,IndexedPhysicsGroup,IndexedVertex,PolygonGroupId,PolygonGroup,PolygonList,PositionId,TextureCoordinateId,NormalId,ColorId,VertexId,RenderConfigId}; use strafesnet_common::model::{Color4,TextureCoordinate,Mesh,IndexedGraphicsGroup,IndexedPhysicsGroup,IndexedVertex,PolygonGroupId,PolygonGroup,PolygonList,IndexedVertexList,PositionId,TextureCoordinateId,NormalId,ColorId,VertexId,RenderConfigId};
use strafesnet_common::integer::{vec3,Planar64Vec3}; use strafesnet_common::integer::{vec3,Planar64Vec3};
#[derive(Debug)] #[derive(Debug)]
@ -42,6 +42,50 @@ const CUBE_DEFAULT_NORMALS:[Planar64Vec3;6]=[
vec3::int( 0,-1, 0),//CubeFace::Bottom vec3::int( 0,-1, 0),//CubeFace::Bottom
vec3::int( 0, 0,-1),//CubeFace::Front vec3::int( 0, 0,-1),//CubeFace::Front
]; ];
const CUBE_DEFAULT_POLYS:[[[u32;3];4];6]=[
// right (1, 0, 0)
[
[6,2,0],//[vertex,tex,norm]
[5,1,0],
[2,0,0],
[1,3,0],
],
// top (0, 1, 0)
[
[5,3,1],
[4,2,1],
[3,1,1],
[2,0,1],
],
// back (0, 0, 1)
[
[0,3,2],
[1,2,2],
[2,1,2],
[3,0,2],
],
// left (-1, 0, 0)
[
[0,2,3],
[3,1,3],
[4,0,3],
[7,3,3],
],
// bottom (0,-1, 0)
[
[1,1,4],
[0,0,4],
[7,3,4],
[6,2,4],
],
// front (0, 0,-1)
[
[4,1,5],
[5,0,5],
[6,3,5],
[7,2,5],
],
];
#[derive(Hash,PartialEq,Eq)] #[derive(Hash,PartialEq,Eq)]
pub enum WedgeFace{ pub enum WedgeFace{
@ -82,14 +126,17 @@ const CORNERWEDGE_DEFAULT_NORMALS:[Planar64Vec3;5]=[
vec3::int( 0,-1, 0),//CornerWedge::Bottom vec3::int( 0,-1, 0),//CornerWedge::Bottom
vec3::int( 0, 0,-1),//CornerWedge::Front vec3::int( 0, 0,-1),//CornerWedge::Front
]; ];
pub fn unit_sphere(render:RenderConfigId)->Mesh{
unit_cube(render)
}
#[derive(Default)] #[derive(Default)]
pub struct CubeFaceDescription([Option<FaceDescription>;6]); pub struct CubeFaceDescription([Option<FaceDescription>;6]);
impl CubeFaceDescription{ impl CubeFaceDescription{
pub fn insert(&mut self,index:CubeFace,value:FaceDescription){ pub fn insert(&mut self,index:CubeFace,value:FaceDescription){
self.0[index as usize]=Some(value); self.0[index as usize]=Some(value);
} }
pub fn pairs(self)->impl Iterator<Item=(usize,FaceDescription)>{ pub fn pairs(self)->std::iter::FilterMap<std::iter::Enumerate<std::array::IntoIter<Option<FaceDescription>,6>>,impl FnMut((usize,Option<FaceDescription>))->Option<(usize,FaceDescription)>>{
self.0.into_iter().enumerate().filter_map(|(i,v)|v.map(|u|(i,u))) self.0.into_iter().enumerate().filter_map(|v|v.1.map(|u|(v.0,u)))
} }
} }
pub fn unit_cube(render:RenderConfigId)->Mesh{ pub fn unit_cube(render:RenderConfigId)->Mesh{
@ -102,6 +149,10 @@ pub fn unit_cube(render:RenderConfigId)->Mesh{
t.insert(CubeFace::Front,FaceDescription::new_with_render_id(render)); t.insert(CubeFace::Front,FaceDescription::new_with_render_id(render));
generate_partial_unit_cube(t) generate_partial_unit_cube(t)
} }
pub fn unit_cylinder(render:RenderConfigId)->Mesh{
//lmao
unit_cube(render)
}
#[derive(Default)] #[derive(Default)]
pub struct WedgeFaceDescription([Option<FaceDescription>;5]); pub struct WedgeFaceDescription([Option<FaceDescription>;5]);
impl WedgeFaceDescription{ impl WedgeFaceDescription{
@ -112,15 +163,15 @@ impl WedgeFaceDescription{
self.0.into_iter().enumerate().filter_map(|v|v.1.map(|u|(v.0,u))) self.0.into_iter().enumerate().filter_map(|v|v.1.map(|u|(v.0,u)))
} }
} }
// pub fn unit_wedge(render:RenderConfigId)->Mesh{ pub fn unit_wedge(render:RenderConfigId)->Mesh{
// let mut t=WedgeFaceDescription::default(); let mut t=WedgeFaceDescription::default();
// t.insert(WedgeFace::Right,FaceDescription::new_with_render_id(render)); t.insert(WedgeFace::Right,FaceDescription::new_with_render_id(render));
// t.insert(WedgeFace::TopFront,FaceDescription::new_with_render_id(render)); t.insert(WedgeFace::TopFront,FaceDescription::new_with_render_id(render));
// t.insert(WedgeFace::Back,FaceDescription::new_with_render_id(render)); t.insert(WedgeFace::Back,FaceDescription::new_with_render_id(render));
// t.insert(WedgeFace::Left,FaceDescription::new_with_render_id(render)); t.insert(WedgeFace::Left,FaceDescription::new_with_render_id(render));
// t.insert(WedgeFace::Bottom,FaceDescription::new_with_render_id(render)); t.insert(WedgeFace::Bottom,FaceDescription::new_with_render_id(render));
// generate_partial_unit_wedge(t) generate_partial_unit_wedge(t)
// } }
#[derive(Default)] #[derive(Default)]
pub struct CornerWedgeFaceDescription([Option<FaceDescription>;5]); pub struct CornerWedgeFaceDescription([Option<FaceDescription>;5]);
impl CornerWedgeFaceDescription{ impl CornerWedgeFaceDescription{
@ -131,15 +182,15 @@ impl CornerWedgeFaceDescription{
self.0.into_iter().enumerate().filter_map(|v|v.1.map(|u|(v.0,u))) self.0.into_iter().enumerate().filter_map(|v|v.1.map(|u|(v.0,u)))
} }
} }
// pub fn unit_cornerwedge(render:RenderConfigId)->Mesh{ pub fn unit_cornerwedge(render:RenderConfigId)->Mesh{
// let mut t=CornerWedgeFaceDescription::default(); let mut t=CornerWedgeFaceDescription::default();
// t.insert(CornerWedgeFace::Right,FaceDescription::new_with_render_id(render)); t.insert(CornerWedgeFace::Right,FaceDescription::new_with_render_id(render));
// t.insert(CornerWedgeFace::TopBack,FaceDescription::new_with_render_id(render)); t.insert(CornerWedgeFace::TopBack,FaceDescription::new_with_render_id(render));
// t.insert(CornerWedgeFace::TopLeft,FaceDescription::new_with_render_id(render)); t.insert(CornerWedgeFace::TopLeft,FaceDescription::new_with_render_id(render));
// t.insert(CornerWedgeFace::Bottom,FaceDescription::new_with_render_id(render)); t.insert(CornerWedgeFace::Bottom,FaceDescription::new_with_render_id(render));
// t.insert(CornerWedgeFace::Front,FaceDescription::new_with_render_id(render)); t.insert(CornerWedgeFace::Front,FaceDescription::new_with_render_id(render));
// generate_partial_unit_cornerwedge(t) generate_partial_unit_cornerwedge(t)
// } }
#[derive(Clone)] #[derive(Clone)]
pub struct FaceDescription{ pub struct FaceDescription{
@ -157,50 +208,6 @@ impl FaceDescription{
} }
} }
pub fn generate_partial_unit_cube(face_descriptions:CubeFaceDescription)->Mesh{ pub fn generate_partial_unit_cube(face_descriptions:CubeFaceDescription)->Mesh{
const CUBE_DEFAULT_POLYS:[[[u32;3];4];6]=[
// right (1, 0, 0)
[
[6,2,0],//[vertex,tex,norm]
[5,1,0],
[2,0,0],
[1,3,0],
],
// top (0, 1, 0)
[
[5,3,1],
[4,2,1],
[3,1,1],
[2,0,1],
],
// back (0, 0, 1)
[
[0,3,2],
[1,2,2],
[2,1,2],
[3,0,2],
],
// left (-1, 0, 0)
[
[0,2,3],
[3,1,3],
[4,0,3],
[7,3,3],
],
// bottom (0,-1, 0)
[
[1,1,4],
[0,0,4],
[7,3,4],
[6,2,4],
],
// front (0, 0,-1)
[
[4,1,5],
[5,0,5],
[6,3,5],
[7,2,5],
],
];
let mut generated_pos=Vec::new(); let mut generated_pos=Vec::new();
let mut generated_tex=Vec::new(); let mut generated_tex=Vec::new();
let mut generated_normal=Vec::new(); let mut generated_normal=Vec::new();
@ -279,35 +286,35 @@ pub fn generate_partial_unit_cube(face_descriptions:CubeFaceDescription)->Mesh{
} }
//don't think too hard about the copy paste because this is all going into the map tool eventually... //don't think too hard about the copy paste because this is all going into the map tool eventually...
pub fn generate_partial_unit_wedge(face_descriptions:WedgeFaceDescription)->Mesh{ pub fn generate_partial_unit_wedge(face_descriptions:WedgeFaceDescription)->Mesh{
const WEDGE_DEFAULT_POLYS:[&[[u32;3]];5]=[ let wedge_default_polys=[
// right (1, 0, 0) // right (1, 0, 0)
&[ vec![
[6,2,0],//[vertex,tex,norm] [6,2,0],//[vertex,tex,norm]
[2,0,0], [2,0,0],
[1,3,0], [1,3,0],
], ],
// FrontTop (0, 1, -1) // FrontTop (0, 1, -1)
&[ vec![
[3,1,1], [3,1,1],
[2,0,1], [2,0,1],
[6,3,1], [6,3,1],
[7,2,1], [7,2,1],
], ],
// back (0, 0, 1) // back (0, 0, 1)
&[ vec![
[0,3,2], [0,3,2],
[1,2,2], [1,2,2],
[2,1,2], [2,1,2],
[3,0,2], [3,0,2],
], ],
// left (-1, 0, 0) // left (-1, 0, 0)
&[ vec![
[0,2,3], [0,2,3],
[3,1,3], [3,1,3],
[7,3,3], [7,3,3],
], ],
// bottom (0,-1, 0) // bottom (0,-1, 0)
&[ vec![
[1,1,4], [1,1,4],
[0,0,4], [0,0,4],
[7,3,4], [7,3,4],
@ -351,7 +358,7 @@ pub fn generate_partial_unit_wedge(face_descriptions:WedgeFaceDescription)->Mesh
//push vertices as they are needed //push vertices as they are needed
let group_id=PolygonGroupId::new(polygon_groups.len() as u32); let group_id=PolygonGroupId::new(polygon_groups.len() as u32);
polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(vec![ polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(vec![
WEDGE_DEFAULT_POLYS[face_id].iter().map(|tup|{ wedge_default_polys[face_id].iter().map(|tup|{
let pos=CUBE_DEFAULT_VERTICES[tup[0] as usize]; let pos=CUBE_DEFAULT_VERTICES[tup[0] as usize];
let pos_index=if let Some(pos_index)=generated_pos.iter().position(|&p|p==pos){ let pos_index=if let Some(pos_index)=generated_pos.iter().position(|&p|p==pos){
pos_index pos_index
@ -392,34 +399,34 @@ pub fn generate_partial_unit_wedge(face_descriptions:WedgeFaceDescription)->Mesh
} }
pub fn generate_partial_unit_cornerwedge(face_descriptions:CornerWedgeFaceDescription)->Mesh{ pub fn generate_partial_unit_cornerwedge(face_descriptions:CornerWedgeFaceDescription)->Mesh{
const CORNERWEDGE_DEFAULT_POLYS:[&[[u32;3]];5]=[ let cornerwedge_default_polys=[
// right (1, 0, 0) // right (1, 0, 0)
&[ vec![
[6,2,0],//[vertex,tex,norm] [6,2,0],//[vertex,tex,norm]
[5,1,0], [5,1,0],
[1,3,0], [1,3,0],
], ],
// BackTop (0, 1, 1) // BackTop (0, 1, 1)
&[ vec![
[5,3,1], [5,3,1],
[0,1,1], [0,1,1],
[1,0,1], [1,0,1],
], ],
// LeftTop (-1, 1, 0) // LeftTop (-1, 1, 0)
&[ vec![
[5,3,2], [5,3,2],
[7,2,2], [7,2,2],
[0,1,2], [0,1,2],
], ],
// bottom (0,-1, 0) // bottom (0,-1, 0)
&[ vec![
[1,1,3], [1,1,3],
[0,0,3], [0,0,3],
[7,3,3], [7,3,3],
[6,2,3], [6,2,3],
], ],
// front (0, 0,-1) // front (0, 0,-1)
&[ vec![
[5,0,4], [5,0,4],
[6,3,4], [6,3,4],
[7,2,4], [7,2,4],
@ -462,7 +469,7 @@ pub fn generate_partial_unit_cornerwedge(face_descriptions:CornerWedgeFaceDescri
//push vertices as they are needed //push vertices as they are needed
let group_id=PolygonGroupId::new(polygon_groups.len() as u32); let group_id=PolygonGroupId::new(polygon_groups.len() as u32);
polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(vec![ polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(vec![
CORNERWEDGE_DEFAULT_POLYS[face_id].iter().map(|tup|{ cornerwedge_default_polys[face_id].iter().map(|tup|{
let pos=CUBE_DEFAULT_VERTICES[tup[0] as usize]; let pos=CUBE_DEFAULT_VERTICES[tup[0] as usize];
let pos_index=if let Some(pos_index)=generated_pos.iter().position(|&p|p==pos){ let pos_index=if let Some(pos_index)=generated_pos.iter().position(|&p|p==pos){
pos_index pos_index

@ -1,7 +1,5 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::loader::MeshIndex;
use crate::primitives; use crate::primitives;
use strafesnet_common::aabb::Aabb;
use strafesnet_common::map; use strafesnet_common::map;
use strafesnet_common::model; use strafesnet_common::model;
use strafesnet_common::gameplay_modes; use strafesnet_common::gameplay_modes;
@ -10,9 +8,6 @@ use strafesnet_common::gameplay_attributes as attr;
use strafesnet_common::integer::{self,vec3,Planar64,Planar64Vec3,Planar64Mat3,Planar64Affine3}; use strafesnet_common::integer::{self,vec3,Planar64,Planar64Vec3,Planar64Mat3,Planar64Affine3};
use strafesnet_common::model::RenderConfigId; use strafesnet_common::model::RenderConfigId;
use strafesnet_common::updatable::Updatable; use strafesnet_common::updatable::Updatable;
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 { fn class_is_a(class: &str, superclass: &str) -> bool {
if class==superclass { if class==superclass {
@ -135,9 +130,9 @@ impl ModesBuilder{
fn push_mode_update(&mut self,mode_id:gameplay_modes::ModeId,mode_update:gameplay_modes::ModeUpdate){ fn push_mode_update(&mut self,mode_id:gameplay_modes::ModeId,mode_update:gameplay_modes::ModeUpdate){
self.mode_updates.push((mode_id,mode_update)); self.mode_updates.push((mode_id,mode_update));
} }
// fn push_stage_update(&mut self,mode_id:gameplay_modes::ModeId,stage_id:gameplay_modes::StageId,stage_update:gameplay_modes::StageUpdate){ fn push_stage_update(&mut self,mode_id:gameplay_modes::ModeId,stage_id:gameplay_modes::StageId,stage_update:gameplay_modes::StageUpdate){
// self.stage_updates.push((mode_id,stage_id,stage_update)); self.stage_updates.push((mode_id,stage_id,stage_update));
// } }
} }
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 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{
let mut general=attr::GeneralAttributes::default(); let mut general=attr::GeneralAttributes::default();
@ -346,103 +341,58 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
} }
} }
#[derive(Clone,Copy)] #[derive(Clone,Copy,PartialEq)]
pub struct RobloxTextureTransform{ struct RobloxTextureTransform{
offset_studs_u:f32, offset_u:f32,
offset_studs_v:f32, offset_v:f32,
studs_per_tile_u:f32, scale_u:f32,
studs_per_tile_v:f32, scale_v:f32,
size_u:f32,
size_v:f32,
} }
#[derive(Clone,Copy,Hash,Eq,PartialEq)] impl std::cmp::Eq for RobloxTextureTransform{}//????
pub struct RobloxTextureTransformBits{ impl std::default::Default for RobloxTextureTransform{
offset_studs_u:u32, fn default()->Self{
offset_studs_v:u32, Self{offset_u:0.0,offset_v:0.0,scale_u:1.0,scale_v:1.0}
studs_per_tile_u:u32, }
studs_per_tile_v:u32,
size_u:u32,
size_v:u32,
} }
impl RobloxTextureTransform{ impl std::hash::Hash for RobloxTextureTransform{
fn identity()->Self{ fn hash<H:std::hash::Hasher>(&self,state:&mut H) {
Self{ self.offset_u.to_ne_bytes().hash(state);
offset_studs_u:0.0, self.offset_v.to_ne_bytes().hash(state);
offset_studs_v:0.0, self.scale_u.to_ne_bytes().hash(state);
studs_per_tile_u:1.0, self.scale_v.to_ne_bytes().hash(state);
studs_per_tile_v:1.0,
size_u:1.0,
size_v:1.0,
}
}
pub fn to_bits(self)->RobloxTextureTransformBits{
RobloxTextureTransformBits{
offset_studs_u:self.offset_studs_u.to_bits(),
offset_studs_v:self.offset_studs_v.to_bits(),
studs_per_tile_u:self.studs_per_tile_u.to_bits(),
studs_per_tile_v:self.studs_per_tile_v.to_bits(),
size_u:self.size_u.to_bits(),
size_v:self.size_v.to_bits(),
}
}
pub fn affine(&self)->glam::Affine2{
glam::Affine2::from_translation(
glam::vec2(self.offset_studs_u/self.studs_per_tile_u,self.offset_studs_v/self.studs_per_tile_v)
)
*glam::Affine2::from_scale(
glam::vec2(self.size_u/self.studs_per_tile_u,self.size_v/self.studs_per_tile_v)
)
}
pub fn set_size(&mut self,size_u:f32,size_v:f32){
self.size_u=size_u;
self.size_v=size_v;
} }
} }
impl core::hash::Hash for RobloxTextureTransform{ #[derive(Clone,PartialEq)]
fn hash<H:core::hash::Hasher>(&self,state:&mut H){ struct RobloxFaceTextureDescription{
self.to_bits().hash(state);
}
}
#[derive(Clone,Copy,Hash,Eq,PartialEq)]
pub struct RobloxFaceTextureDescriptionBits{
render:RenderConfigId, render:RenderConfigId,
color:[u32;4], color:glam::Vec4,
transform:RobloxTextureTransformBits, transform:RobloxTextureTransform,
} }
#[derive(Clone,Copy)] impl std::cmp::Eq for RobloxFaceTextureDescription{}//????
pub struct RobloxFaceTextureDescription{ impl std::hash::Hash for RobloxFaceTextureDescription{
pub render:RenderConfigId, fn hash<H:std::hash::Hasher>(&self,state:&mut H){
pub color:glam::Vec4, self.render.hash(state);
pub transform:RobloxTextureTransform, self.transform.hash(state);
} for &el in self.color.as_ref().iter(){
impl core::cmp::PartialEq for RobloxFaceTextureDescription{ el.to_ne_bytes().hash(state);
fn eq(&self,other:&Self)->bool{ }
self.to_bits().eq(&other.to_bits()) }
}
}
impl core::cmp::Eq for RobloxFaceTextureDescription{}
impl core::hash::Hash for RobloxFaceTextureDescription{
fn hash<H:core::hash::Hasher>(&self,state:&mut H){
self.to_bits().hash(state);
}
} }
impl RobloxFaceTextureDescription{ impl RobloxFaceTextureDescription{
pub fn to_bits(self)->RobloxFaceTextureDescriptionBits{ fn to_face_description(&self)->primitives::FaceDescription{
RobloxFaceTextureDescriptionBits{
render:self.render,
color:self.color.to_array().map(f32::to_bits),
transform:self.transform.to_bits(),
}
}
pub fn to_face_description(&self)->primitives::FaceDescription{
primitives::FaceDescription{ primitives::FaceDescription{
render:self.render, render:self.render,
transform:self.transform.affine(), transform:glam::Affine2::from_translation(
glam::vec2(self.transform.offset_u,self.transform.offset_v)
)
*glam::Affine2::from_scale(
glam::vec2(self.transform.scale_u,self.transform.scale_v)
),
color:self.color, color:self.color,
} }
} }
} }
pub type RobloxPartDescription=[Option<RobloxFaceTextureDescription>;6]; type RobloxPartDescription=[Option<RobloxFaceTextureDescription>;6];
type RobloxWedgeDescription=[Option<RobloxFaceTextureDescription>;5]; type RobloxWedgeDescription=[Option<RobloxFaceTextureDescription>;5];
type RobloxCornerWedgeDescription=[Option<RobloxFaceTextureDescription>;5]; type RobloxCornerWedgeDescription=[Option<RobloxFaceTextureDescription>;5];
#[derive(Clone,Eq,Hash,PartialEq)] #[derive(Clone,Eq,Hash,PartialEq)]
@ -453,134 +403,52 @@ enum RobloxBasePartDescription{
Wedge(RobloxWedgeDescription), Wedge(RobloxWedgeDescription),
CornerWedge(RobloxCornerWedgeDescription), CornerWedge(RobloxCornerWedgeDescription),
} }
fn get_texture_description<'a>(
temp_objects:&mut Vec<rbx_dom_weak::types::Ref>,
render_config_deferred_loader:&mut RenderConfigDeferredLoader<&'a str>,
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=[None,None,None,None,None,None];
temp_objects.clear();
recursive_collect_superclass(temp_objects,&dom,object,"Decal");
for &mut decal_ref in temp_objects{
if let Some(decal)=dom.get_by_ref(decal_ref){
if let (
Some(rbx_dom_weak::types::Variant::Content(content)),
Some(rbx_dom_weak::types::Variant::Enum(normalid)),
Some(rbx_dom_weak::types::Variant::Color3(decal_color3)),
Some(rbx_dom_weak::types::Variant::Float32(decal_transparency)),
) = (
decal.properties.get("Texture"),
decal.properties.get("Face"),
decal.properties.get("Color3"),
decal.properties.get("Transparency"),
) {
let render_id=render_config_deferred_loader.acquire_render_config_id(Some(content.as_ref()));
let normal_id=normalid.to_u32();
if normal_id<6{
let (roblox_texture_color,roblox_texture_transform)=if decal.class=="Texture"{
//generate tranform
if let (
Some(&rbx_dom_weak::types::Variant::Float32(offset_studs_u)),
Some(&rbx_dom_weak::types::Variant::Float32(offset_studs_v)),
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("OffsetStudsU"),
decal.properties.get("OffsetStudsV"),
decal.properties.get("StudsPerTileU"),
decal.properties.get("StudsPerTileV"),
)
{
let (size_u,size_v)=match normal_id{
0=>(size.z,size.y),//right
1=>(size.x,size.z),//top
2=>(size.x,size.y),//back
3=>(size.z,size.y),//left
4=>(size.x,size.z),//bottom
5=>(size.x,size.y),//front
_=>unreachable!(),
};
(
glam::vec4(decal_color3.r,decal_color3.g,decal_color3.b,1.0-*decal_transparency),
RobloxTextureTransform{
offset_studs_u,
offset_studs_v,
studs_per_tile_u,
studs_per_tile_v,
size_u,
size_v,
}
)
}else{
(glam::Vec4::ONE,RobloxTextureTransform::identity())
}
}else{
(glam::Vec4::ONE,RobloxTextureTransform::identity())
};
part_texture_description[normal_id as usize]=Some(RobloxFaceTextureDescription{
render:render_id,
color:roblox_texture_color,
transform:roblox_texture_transform,
});
}else{
println!("NormalId={} is invalid",normal_id);
}
}
}
}
part_texture_description
}
enum Shape{ enum Shape{
Primitive(primitives::Primitives), Primitive(primitives::Primitives),
MeshPart, MeshPart,
PhysicsData,
} }
enum MeshAvailability{ enum MeshAvailability{
Immediate, Immediate,
DeferredMesh(RenderConfigId), Deferred(RenderConfigId),
DeferredUnion(RobloxPartDescription),
} }
struct DeferredModelDeferredAttributes<'a>{ struct DeferredModelDeferredAttributes{
render:RenderConfigId, render:RenderConfigId,
model:ModelDeferredAttributes<'a>, model:ModelDeferredAttributes,
} }
struct ModelDeferredAttributes<'a>{ struct ModelDeferredAttributes{
mesh:model::MeshId, mesh:model::MeshId,
deferred_attributes:GetAttributesArgs<'a>, deferred_attributes:GetAttributesArgs,
color:model::Color4,//transparency is in here color:model::Color4,//transparency is in here
transform:Planar64Affine3, transform:Planar64Affine3,
} }
struct DeferredUnionDeferredAttributes<'a>{
render:RobloxPartDescription,
model:ModelDeferredAttributes<'a>,
}
struct ModelOwnedAttributes{ struct ModelOwnedAttributes{
mesh:model::MeshId, mesh:model::MeshId,
attributes:attr::CollisionAttributes, attributes:attr::CollisionAttributes,
color:model::Color4,//transparency is in here color:model::Color4,//transparency is in here
transform:Planar64Affine3, transform:Planar64Affine3,
} }
struct GetAttributesArgs<'a>{ struct GetAttributesArgs{
name:&'a str, name:Box<str>,
can_collide:bool, can_collide:bool,
velocity:Planar64Vec3, velocity:Planar64Vec3,
} }
pub fn convert<'a>( pub fn convert<AcquireRenderConfigId,AcquireMeshId>(
dom:&'a rbx_dom_weak::WeakDom, dom:&rbx_dom_weak::WeakDom,
render_config_deferred_loader:&mut RenderConfigDeferredLoader<&'a str>, mut acquire_render_config_id:AcquireRenderConfigId,
mesh_deferred_loader:&mut MeshDeferredLoader<MeshIndex<'a>>, mut acquire_mesh_id:AcquireMeshId,
)->PartialMap1<'a>{ )->PartialMap1
where
AcquireRenderConfigId:FnMut(Option<&str>)->model::RenderConfigId,
AcquireMeshId:FnMut(&str)->model::MeshId,
{
let mut deferred_models_deferred_attributes=Vec::new(); let mut deferred_models_deferred_attributes=Vec::new();
let mut deferred_unions_deferred_attributes=Vec::new();
let mut primitive_models_deferred_attributes=Vec::new(); let mut primitive_models_deferred_attributes=Vec::new();
let mut primitive_meshes=Vec::new(); let mut primitive_meshes=Vec::new();
let mut mesh_id_from_description=HashMap::new(); let mut mesh_id_from_description=HashMap::new();
//just going to leave it like this for now instead of reworking the data structures for this whole thing //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 textureless_render_group=acquire_render_config_id(None);
let mut object_refs=Vec::new(); let mut object_refs=Vec::new();
let mut temp_objects=Vec::new(); let mut temp_objects=Vec::new();
@ -617,6 +485,9 @@ pub fn convert<'a>(
continue; continue;
} }
//at this point a new model is going to be generated for sure.
let model_id=model::ModelId::new(primitive_models_deferred_attributes.len() as u32);
//TODO: also detect "CylinderMesh" etc here //TODO: also detect "CylinderMesh" etc here
let shape=match object.class.as_str(){ let shape=match object.class.as_str(){
"Part"=>if let Some(rbx_dom_weak::types::Variant::Enum(shape))=object.properties.get("Shape"){ "Part"=>if let Some(rbx_dom_weak::types::Variant::Enum(shape))=object.properties.get("Shape"){
@ -635,7 +506,6 @@ pub fn convert<'a>(
"WedgePart"=>Shape::Primitive(primitives::Primitives::Wedge), "WedgePart"=>Shape::Primitive(primitives::Primitives::Wedge),
"CornerWedgePart"=>Shape::Primitive(primitives::Primitives::CornerWedge), "CornerWedgePart"=>Shape::Primitive(primitives::Primitives::CornerWedge),
"MeshPart"=>Shape::MeshPart, "MeshPart"=>Shape::MeshPart,
"UnionOperation"=>Shape::PhysicsData,
_=>{ _=>{
println!("Unsupported BasePart ClassName={}; defaulting to cube",object.class); println!("Unsupported BasePart ClassName={}; defaulting to cube",object.class);
Shape::Primitive(primitives::Primitives::Cube) Shape::Primitive(primitives::Primitives::Cube)
@ -644,8 +514,74 @@ pub fn convert<'a>(
let (availability,mesh_id)=match shape{ let (availability,mesh_id)=match shape{
Shape::Primitive(primitive_shape)=>{ Shape::Primitive(primitive_shape)=>{
//TODO: TAB TAB //TODO: TAB TAB
let part_texture_description=get_texture_description(&mut temp_objects,render_config_deferred_loader,dom,object,size); //use the biggest one and cut it down later...
let mut part_texture_description:RobloxPartDescription=[None,None,None,None,None,None];
temp_objects.clear();
recursive_collect_superclass(&mut temp_objects, &dom, object,"Decal");
for &decal_ref in &temp_objects{
if let Some(decal)=dom.get_by_ref(decal_ref){
if let (
Some(rbx_dom_weak::types::Variant::Content(content)),
Some(rbx_dom_weak::types::Variant::Enum(normalid)),
Some(rbx_dom_weak::types::Variant::Color3(decal_color3)),
Some(rbx_dom_weak::types::Variant::Float32(decal_transparency)),
) = (
decal.properties.get("Texture"),
decal.properties.get("Face"),
decal.properties.get("Color3"),
decal.properties.get("Transparency"),
) {
let render_id=acquire_render_config_id(Some(content.as_ref()));
let normal_id=normalid.to_u32();
if normal_id<6{
let (roblox_texture_color,roblox_texture_transform)=if decal.class=="Texture"{
//generate tranform
if let (
Some(rbx_dom_weak::types::Variant::Float32(ox)),
Some(rbx_dom_weak::types::Variant::Float32(oy)),
Some(rbx_dom_weak::types::Variant::Float32(sx)),
Some(rbx_dom_weak::types::Variant::Float32(sy)),
) = (
decal.properties.get("OffsetStudsU"),
decal.properties.get("OffsetStudsV"),
decal.properties.get("StudsPerTileU"),
decal.properties.get("StudsPerTileV"),
)
{
let (size_u,size_v)=match normal_id{
0=>(size.z,size.y),//right
1=>(size.x,size.z),//top
2=>(size.x,size.y),//back
3=>(size.z,size.y),//left
4=>(size.x,size.z),//bottom
5=>(size.x,size.y),//front
_=>unreachable!(),
};
(
glam::vec4(decal_color3.r,decal_color3.g,decal_color3.b,1.0-*decal_transparency),
RobloxTextureTransform{
offset_u:*ox/(*sx),offset_v:*oy/(*sy),
scale_u:size_u/(*sx),scale_v:size_v/(*sy),
}
)
}else{
(glam::Vec4::ONE,RobloxTextureTransform::default())
}
}else{
(glam::Vec4::ONE,RobloxTextureTransform::default())
};
part_texture_description[normal_id as usize]=Some(RobloxFaceTextureDescription{
render:render_id,
color:roblox_texture_color,
transform:roblox_texture_transform,
});
}else{
println!("NormalId={} unsupported for shape={:?}",normal_id,primitive_shape);
}
}
}
}
//obscure rust syntax "slice pattern" //obscure rust syntax "slice pattern"
let [ let [
f0,//Cube::Right f0,//Cube::Right
@ -662,7 +598,7 @@ pub fn convert<'a>(
//use front face texture first and use top face texture as a fallback //use front face texture first and use top face texture as a fallback
primitives::Primitives::Wedge=>RobloxBasePartDescription::Wedge([ primitives::Primitives::Wedge=>RobloxBasePartDescription::Wedge([
f0,//Cube::Right->Wedge::Right f0,//Cube::Right->Wedge::Right
f5.or(f1),//Cube::Front|Cube::Top->Wedge::TopFront if f5.is_some(){f5}else{f1},//Cube::Front|Cube::Top->Wedge::TopFront
f2,//Cube::Back->Wedge::Back f2,//Cube::Back->Wedge::Back
f3,//Cube::Left->Wedge::Left f3,//Cube::Left->Wedge::Left
f4,//Cube::Bottom->Wedge::Bottom f4,//Cube::Bottom->Wedge::Bottom
@ -670,8 +606,8 @@ pub fn convert<'a>(
//TODO: fix Left+Back texture coordinates to match roblox when not overwridden by Top //TODO: fix Left+Back texture coordinates to match roblox when not overwridden by Top
primitives::Primitives::CornerWedge=>RobloxBasePartDescription::CornerWedge([ primitives::Primitives::CornerWedge=>RobloxBasePartDescription::CornerWedge([
f0,//Cube::Right->CornerWedge::Right f0,//Cube::Right->CornerWedge::Right
f2.or(f1.clone()),//Cube::Back|Cube::Top->CornerWedge::TopBack if f2.is_some(){f2}else{f1.clone()},//Cube::Back|Cube::Top->CornerWedge::TopBack
f3.or(f1),//Cube::Left|Cube::Top->CornerWedge::TopLeft if f3.is_some(){f3}else{f1},//Cube::Left|Cube::Top->CornerWedge::TopLeft
f4,//Cube::Bottom->CornerWedge::Bottom f4,//Cube::Bottom->CornerWedge::Bottom
f5,//Cube::Front->CornerWedge::Front f5,//Cube::Front->CornerWedge::Front
]), ]),
@ -758,51 +694,29 @@ pub fn convert<'a>(
object.properties.get("TextureID"), object.properties.get("TextureID"),
){ ){
( (
MeshAvailability::DeferredMesh(render_config_deferred_loader.acquire_render_config_id(Some(texture_asset_id.as_ref()))), MeshAvailability::Deferred(acquire_render_config_id(Some(texture_asset_id.as_ref()))),
mesh_deferred_loader.acquire_mesh_id(MeshIndex::file_mesh(mesh_asset_id.as_ref())), acquire_mesh_id(mesh_asset_id.as_ref()),
) )
}else{ }else{
panic!("Mesh has no Mesh or Texture"); 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::Content(asset_id))=object.properties.get("AssetId"){
content=asset_id.as_ref();
}
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=object.properties.get("MeshData"){
mesh_data=data.as_ref();
}
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=object.properties.get("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)
},
}; };
let model_deferred_attributes=ModelDeferredAttributes{ let model_deferred_attributes=ModelDeferredAttributes{
mesh:mesh_id, mesh:mesh_id,
transform:model_transform, transform:model_transform,
color:glam::vec4(color3.r as f32/255f32, color3.g as f32/255f32, color3.b as f32/255f32, 1.0-*transparency), color:glam::vec4(color3.r as f32/255f32, color3.g as f32/255f32, color3.b as f32/255f32, 1.0-*transparency),
deferred_attributes:GetAttributesArgs{ deferred_attributes:GetAttributesArgs{
name:object.name.as_str(), name:object.name.as_str().into(),
can_collide:*can_collide, can_collide:*can_collide,
velocity:vec3::try_from_f32_array([velocity.x,velocity.y,velocity.z]).unwrap(), velocity:vec3::try_from_f32_array([velocity.x,velocity.y,velocity.z]).unwrap(),
}, },
}; };
match availability{ match availability{
MeshAvailability::Immediate=>primitive_models_deferred_attributes.push(model_deferred_attributes), MeshAvailability::Immediate=>primitive_models_deferred_attributes.push(model_deferred_attributes),
MeshAvailability::DeferredMesh(render)=>deferred_models_deferred_attributes.push(DeferredModelDeferredAttributes{ MeshAvailability::Deferred(render)=>deferred_models_deferred_attributes.push(DeferredModelDeferredAttributes{
render, render,
model:model_deferred_attributes model:model_deferred_attributes
}), }),
MeshAvailability::DeferredUnion(part_texture_description)=>deferred_unions_deferred_attributes.push(DeferredUnionDeferredAttributes{
render:part_texture_description,
model:model_deferred_attributes,
}),
} }
} }
} }
@ -811,71 +725,21 @@ pub fn convert<'a>(
primitive_meshes, primitive_meshes,
primitive_models_deferred_attributes, primitive_models_deferred_attributes,
deferred_models_deferred_attributes, deferred_models_deferred_attributes,
deferred_unions_deferred_attributes,
} }
} }
struct MeshWithAabb{ struct MeshWithAabb{
mesh:model::Mesh, mesh:model::Mesh,
aabb:Aabb, aabb:strafesnet_common::aabb::Aabb,
} }
fn acquire_mesh_id_from_render_config_id<'a>( pub struct PartialMap1{
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>,
old_mesh_id:model::MeshId,
render:RenderConfigId,
)->Option<(model::MeshId,&'a Aabb)>{
//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())
.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();
//set the render group lool
if let Some(graphics_group)=mesh_clone.graphics_groups.first_mut(){
graphics_group.render=render;
}
primitive_meshes.push(mesh_clone);
mesh_id
}),
&mesh_with_aabb.aabb,
))
}
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>,
old_union_id:model::MeshId,
part_texture_description:RobloxPartDescription,
)->Option<(model::MeshId,&'a Aabb)>{
//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())
.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();
//set the render groups
for (graphics_group,maybe_face_texture_description) in union_clone.graphics_groups.iter_mut().zip(part_texture_description){
if let Some(face_texture_description)=maybe_face_texture_description{
graphics_group.render=face_texture_description.render;
}
}
primitive_meshes.push(union_clone);
union_id
}),
&union_with_aabb.aabb,
))
}
pub struct PartialMap1<'a>{
primitive_meshes:Vec<model::Mesh>, primitive_meshes:Vec<model::Mesh>,
primitive_models_deferred_attributes:Vec<ModelDeferredAttributes<'a>>, primitive_models_deferred_attributes:Vec<ModelDeferredAttributes>,
deferred_models_deferred_attributes:Vec<DeferredModelDeferredAttributes<'a>>, deferred_models_deferred_attributes:Vec<DeferredModelDeferredAttributes>,
deferred_unions_deferred_attributes:Vec<DeferredUnionDeferredAttributes<'a>>,
} }
impl PartialMap1<'_>{ impl PartialMap1{
pub fn add_meshpart_meshes_and_calculate_attributes( pub fn add_meshpart_meshes_and_calculate_attributes(
mut self, mut self,
meshpart_meshes:Meshes, meshpart_meshes:impl IntoIterator<Item=(model::MeshId,crate::data::RobloxMeshBytes)>,
)->PartialMap2{ )->PartialMap2{
//calculate attributes //calculate attributes
let mut modes_builder=ModesBuilder::default(); let mut modes_builder=ModesBuilder::default();
@ -888,32 +752,51 @@ impl PartialMap1<'_>{
//decode roblox meshes //decode roblox meshes
//generate mesh_id_map based on meshes that failed to load //generate mesh_id_map based on meshes that failed to load
let loaded_meshes:HashMap<model::MeshId,MeshWithAabb>= let loaded_meshes:HashMap<model::MeshId,MeshWithAabb>=
meshpart_meshes.consume().map(|(old_mesh_id,mesh)|{ meshpart_meshes.into_iter().flat_map(|(old_mesh_id,roblox_mesh_bytes)|
let mut aabb=strafesnet_common::aabb::Aabb::default(); match crate::mesh::convert(roblox_mesh_bytes){
for &pos in &mesh.unique_pos{ Ok(mesh)=>{
aabb.grow(pos); let mut aabb=strafesnet_common::aabb::Aabb::default();
for &pos in &mesh.unique_pos{
aabb.grow(pos);
}
Some((old_mesh_id,MeshWithAabb{
mesh,
aabb,
}))
},
Err(e)=>{
println!("Error converting mesh: {e:?}");
None
},
} }
(old_mesh_id,MeshWithAabb{ ).collect();
mesh,
aabb,
})
}).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 mesh_id_from_render_config_id=HashMap::new(); let mut mesh_id_from_render_config_id=HashMap::new();
let mut union_id_from_render_config_id=HashMap::new(); //ignore meshes that fail to load completely for now
let mut acquire_mesh_id_from_render_config_id=|old_mesh_id,render|{
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())
.entry(render).or_insert_with(||{
let mesh_id=model::MeshId::new(self.primitive_meshes.len() as u32);
let mut mesh_clone=mesh_with_aabb.mesh.clone();
//add a render group lool
mesh_clone.graphics_groups.push(model::IndexedGraphicsGroup{
render,
//the lowest lod is highest quality
groups:vec![model::PolygonGroupId::new(0)]
});
self.primitive_meshes.push(mesh_clone);
mesh_id
}),
&mesh_with_aabb.aabb,
))
};
//now that the meshes are loaded, these models can be generated //now that the meshes are loaded, these models can be generated
let models_owned_attributes:Vec<ModelOwnedAttributes>= let models_owned_attributes:Vec<ModelOwnedAttributes>=
self.deferred_models_deferred_attributes.into_iter().flat_map(|deferred_model_deferred_attributes|{ 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 //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 //insert into primitive_meshes
let (mesh,aabb)=acquire_mesh_id_from_render_config_id( let (mesh,aabb)=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.model.mesh,
deferred_model_deferred_attributes.render deferred_model_deferred_attributes.render
)?; )?;
@ -931,32 +814,7 @@ impl PartialMap1<'_>{
deferred_model_deferred_attributes.model.transform.translation deferred_model_deferred_attributes.model.transform.translation
), ),
}) })
}).chain(self.deferred_unions_deferred_attributes.into_iter().flat_map(|deferred_union_deferred_attributes|{ }).chain(self.primitive_models_deferred_attributes.into_iter())
//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(
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,
color:deferred_union_deferred_attributes.model.color,
transform:Planar64Affine3::new(
Planar64Mat3::from_cols([
(deferred_union_deferred_attributes.model.transform.matrix3.x_axis*2/size.x).divide().fix_1(),
(deferred_union_deferred_attributes.model.transform.matrix3.y_axis*2/size.y).divide().fix_1(),
(deferred_union_deferred_attributes.model.transform.matrix3.z_axis*2/size.z).divide().fix_1()
]),
deferred_union_deferred_attributes.model.transform.translation
),
})
}))
.chain(self.primitive_models_deferred_attributes.into_iter())
.enumerate().map(|(model_id,model_deferred_attributes)|{ .enumerate().map(|(model_id,model_deferred_attributes)|{
let model_id=model::ModelId::new(model_id as u32); let model_id=model::ModelId::new(model_id as u32);
ModelOwnedAttributes{ ModelOwnedAttributes{
@ -1024,21 +882,15 @@ pub struct PartialMap2{
impl PartialMap2{ impl PartialMap2{
pub fn add_render_configs_and_textures( pub fn add_render_configs_and_textures(
self, self,
render_configs:RenderConfigs, render_configs:impl IntoIterator<Item=(model::RenderConfigId,model::RenderConfig)>,
textures:impl IntoIterator<Item=(model::TextureId,Vec<u8>)>,
)->map::CompleteMap{ )->map::CompleteMap{
let (textures,render_configs)=render_configs.consume();
let (textures,texture_id_map):(Vec<Vec<u8>>,HashMap<model::TextureId,model::TextureId>) 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)))|{ =textures.into_iter().enumerate().map(|(new_texture_id,(old_texture_id,texture))|{
(texture,(old_texture_id,model::TextureId::new(new_texture_id as u32))) (texture,(old_texture_id,model::TextureId::new(new_texture_id as u32)))
}).unzip(); }).unzip();
let render_configs=render_configs.into_iter().map(|(_render_config_id,mut render_config)|{ let render_configs=render_configs.into_iter().map(|(render_config_id,mut render_config)|{
// This may generate duplicate no-texture render configs but idc //this may generate duplicate no-texture render configs but idc
//
// This is because some textures may not exist, so the render config
// that it points to is unique but is texture.
//
// I don't think this needs to be fixed because missing textures
// should be a conversion error anyways.
render_config.texture=render_config.texture.and_then(|texture_id| render_config.texture=render_config.texture.and_then(|texture_id|
texture_id_map.get(&texture_id).copied() texture_id_map.get(&texture_id).copied()
); );

@ -1,177 +0,0 @@
use rbx_mesh::mesh_data::NormalId2 as MeshDataNormalId2;
use strafesnet_common::model::{self,IndexedVertex,PolygonGroup,PolygonGroupId,PolygonList,RenderConfigId};
use strafesnet_common::integer::vec3;
#[allow(dead_code)]
#[derive(Debug)]
pub enum Error{
Block,
MissingVertexId(u32),
Planar64Vec3(strafesnet_common::integer::Planar64TryFromFloatError),
RobloxPhysicsData(rbx_mesh::physics_data::Error),
RobloxMeshData(rbx_mesh::mesh_data::Error),
}
impl std::fmt::Display for Error{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
// 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),
Conflicting,
}
struct MeshDataNormalChecker{
status:Option<MeshDataNormalStatus>,
}
impl MeshDataNormalChecker{
fn new()->Self{
Self{status:None}
}
fn check(&mut self,normal:MeshDataNormalId2){
self.status=match self.status.take(){
None=>Some(MeshDataNormalStatus::Agree(normal)),
Some(MeshDataNormalStatus::Agree(old_normal))=>{
if old_normal==normal{
Some(MeshDataNormalStatus::Agree(old_normal))
}else{
Some(MeshDataNormalStatus::Conflicting)
}
},
Some(MeshDataNormalStatus::Conflicting)=>Some(MeshDataNormalStatus::Conflicting),
};
}
fn into_agreed_normal(self)->Option<MeshDataNormalId2>{
self.status.and_then(|status|match status{
MeshDataNormalStatus::Agree(normal)=>Some(normal),
MeshDataNormalStatus::Conflicting=>None,
})
}
}
impl std::error::Error for Error{}
pub fn convert(
roblox_physics_data:&[u8],
roblox_mesh_data:&[u8],
size:glam::Vec3,
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];
// build graphics and physics meshes
let mut mb=strafesnet_common::model::MeshBuilder::new();
// graphics
let graphics_groups=if !roblox_mesh_data.is_empty(){
// create per-face texture coordinate affine transforms
let cube_face_description=part_texture_description.map(|opt|opt.map(|mut t|{
t.transform.set_size(1.0,1.0);
t.to_face_description()
}));
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{
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,
};
for [vertex_id0,vertex_id1,vertex_id2] in graphics_mesh.faces{
let face=[
graphics_mesh.vertices.get(vertex_id0.0 as usize).ok_or(Error::MissingVertexId(vertex_id0.0))?,
graphics_mesh.vertices.get(vertex_id1.0 as usize).ok_or(Error::MissingVertexId(vertex_id1.0))?,
graphics_mesh.vertices.get(vertex_id2.0 as usize).ok_or(Error::MissingVertexId(vertex_id2.0))?,
];
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),
groups:vec![PolygonGroupId::new(polygon_group_id as u32)]
}
}).collect()
}else{
Vec::new()
};
//physics
let physics_convex_meshes=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)?;
let physics_convex_meshes=match physics_data{
rbx_mesh::physics_data::PhysicsData::CSGK(_)
// 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))
=>meshes.meshes,
rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::PhysicsInfoMesh(pim))
=>vec![pim.mesh],
};
physics_convex_meshes
}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);
let tex=mb.acquire_tex_id(glam::Vec2::ZERO);
// physics polygon groups (to do physics)
Ok(PolygonGroup::PolygonList(PolygonList::new(mesh.faces.into_iter().map(|[vertex_id0,vertex_id1,vertex_id2]|{
let face=[
mesh.vertices.get(vertex_id0.0 as usize).ok_or(Error::MissingVertexId(vertex_id0.0))?,
mesh.vertices.get(vertex_id1.0 as usize).ok_or(Error::MissingVertexId(vertex_id1.0))?,
mesh.vertices.get(vertex_id2.0 as usize).ok_or(Error::MissingVertexId(vertex_id2.0))?,
].map(|v|glam::Vec3::from_slice(v)/size);
let vertex_norm=(face[1]-face[0])
.cross(face[2]-face[0]);
let normal=mb.acquire_normal_id(vec3::try_from_f32_array(vertex_norm.to_array()).map_err(Error::Planar64Vec3)?);
face.into_iter().map(|vertex_pos|{
let pos=mb.acquire_pos_id(vec3::try_from_f32_array(vertex_pos.to_array()).map_err(Error::Planar64Vec3)?);
Ok(mb.acquire_vertex_id(IndexedVertex{pos,tex,normal,color}))
}).collect()
}).collect::<Result<_,_>>()?)))
})).collect::<Result<_,_>>()?;
let physics_groups=(NORMAL_FACES..polygon_groups.len()).map(|id|model::IndexedPhysicsGroup{
groups:vec![PolygonGroupId::new(id as u32)]
}).collect();
Ok(mb.build(
polygon_groups,
graphics_groups,
physics_groups,
))
}

@ -1,11 +0,0 @@
[package]
name = "rbxassetid"
version = "0.1.0"
edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0"
description = "Parse Roblox asset id from 'Content' urls."
authors = ["Rhys Lloyd <krakow20@gmail.com>"]
[dependencies]
url = "2.5.4"

@ -1,176 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

@ -1,23 +0,0 @@
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

@ -1,26 +0,0 @@
Roblox Asset Id
===============
## Example
```rust
use rbxassetid::RobloxAssetId;
let content="rbxassetid://255299419";
let RobloxAssetId(asset_id)=content.parse()?;
```
#### License
<sup>
Licensed under either of <a href="LICENSE-APACHE">Apache License, Version
2.0</a> or <a href="LICENSE-MIT">MIT license</a> at your option.
</sup>
<br>
<sub>
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in this crate by you, as defined in the Apache-2.0 license, shall
be dual licensed as above, without any additional terms or conditions.
</sub>

@ -1,41 +0,0 @@
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq)]
pub struct RobloxAssetId(pub u64);
#[derive(Debug)]
pub enum RobloxAssetIdParseErr{
Url(url::ParseError),
UnknownScheme,
ParseInt(std::num::ParseIntError),
MissingAssetId,
MissingIDQueryParam,
}
impl std::fmt::Display for RobloxAssetIdParseErr{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for RobloxAssetIdParseErr{}
impl std::str::FromStr for RobloxAssetId{
type Err=RobloxAssetIdParseErr;
fn from_str(s:&str)->Result<Self,Self::Err>{
let url=url::Url::parse(s).map_err(RobloxAssetIdParseErr::Url)?;
let parsed_asset_id=match url.scheme(){
"rbxassetid"=>url.domain().ok_or(RobloxAssetIdParseErr::MissingAssetId)?.parse(),
"http"|"https"=>{
let (_,asset_id)=url.query_pairs()
.find(|(id,_)|match id.as_ref(){
"ID"|"id"|"Id"|"iD"=>true,
_=>false,
}).ok_or(RobloxAssetIdParseErr::MissingIDQueryParam)?;
asset_id.parse()
},
_=>Err(RobloxAssetIdParseErr::UnknownScheme)?,
};
Ok(Self(parsed_asset_id.map_err(RobloxAssetIdParseErr::ParseInt)?))
}
}
#[test]
fn test_rbxassetid(){
let content="rbxassetid://255299419";
let RobloxAssetId(_asset_id)=content.parse().unwrap();
}

@ -1,7 +1,7 @@
[package] [package]
name = "roblox_emulator" name = "roblox_emulator"
version = "0.4.7" version = "0.4.7"
edition = "2024" edition = "2021"
repository = "https://git.itzana.me/StrafesNET/strafe-project" repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
description = "Run embedded Luau scripts which manipulate the DOM." description = "Run embedded Luau scripts which manipulate the DOM."
@ -12,7 +12,7 @@ default=["run-service"]
run-service=[] run-service=[]
[dependencies] [dependencies]
glam = "0.30.0" glam = "0.29.0"
mlua = { version = "0.10.1", features = ["luau"] } mlua = { version = "0.10.1", features = ["luau"] }
phf = { version = "0.11.2", features = ["macros"] } phf = { version = "0.11.2", features = ["macros"] }
rbx_dom_weak = { version = "2.7.0", registry = "strafesnet" } rbx_dom_weak = { version = "2.7.0", registry = "strafesnet" }

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

@ -59,8 +59,8 @@ fn schedule_thread(lua:&mlua::Lua,dt:mlua::Value)->Result<(),mlua::Error>{
match delay.classify(){ match delay.classify(){
std::num::FpCategory::Nan=>Err(mlua::Error::runtime("NaN"))?, std::num::FpCategory::Nan=>Err(mlua::Error::runtime("NaN"))?,
// cases where the number is too large to schedule // cases where the number is too large to schedule
std::num::FpCategory::Infinite std::num::FpCategory::Infinite=>return Ok(()),
|std::num::FpCategory::Normal if (u64::MAX as f64)<delay=>{ std::num::FpCategory::Normal=>if (u64::MAX as f64)<delay{
return Ok(()); return Ok(());
}, },
_=>(), _=>(),

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

@ -1,347 +1,98 @@
use binrw::{binrw,BinReaderExt,BinWrite,BinWriterExt}; use binrw::{BinReaderExt, binrw};
use crate::newtypes;
use crate::file::BlockId;
use strafesnet_common::physics::Time;
const VERSION:u32=0;
type TimedPhysicsInstruction=strafesnet_common::instruction::TimedInstruction<strafesnet_common::physics::Instruction,strafesnet_common::physics::Time>;
#[derive(Debug)] #[derive(Debug)]
pub enum Error{ pub enum Error{
InvalidHeader(binrw::Error), InvalidHeader,
InvalidSegment(binrw::Error), InvalidSegment(binrw::Error),
SegmentConvert(newtypes::integer::RatioError),
InstructionConvert(newtypes::physics::InstructionConvert),
InstructionWrite(binrw::Error),
InvalidSegmentId(SegmentId), InvalidSegmentId(SegmentId),
InvalidData(binrw::Error),
IO(std::io::Error),
File(crate::file::Error), File(crate::file::Error),
} }
// Bot files are simply the sequence of instructions that the physics received during the run.
// The instructions are partitioned into timestamped blocks for ease of streaming.
//
// Keyframe information required for efficient seeking
// is part of a different file, and is generated from this file.
/* block types /* block types
BLOCK_BOT_HEADER: BLOCK_BOT_HEADER:
// Segments are laid out in chronological order, u128 map_resource_uuid //which map is this bot running
// but block_id is not necessarily in ascending order. //don't include style info in bot header because it's in the simulation state
// //blocks are laid out in chronological order, but indices may jump around.
// This is to place the final segment close to the start of the file, u64 num_segments
// which allows the duration of the bot to be conveniently calculated
// from the first and last instruction timestamps.
//
// Use exact physics version for replay playback
// Use highest compatible physics version for verification
u32 physics_version
u32 num_segments
for _ in 0..num_segments{ for _ in 0..num_segments{
i64 time i64 time //simulation_state timestamp
u32 instruction_count u64 block_id
u32 block_id
} }
BLOCK_BOT_SEGMENT: BLOCK_BOT_SEGMENT:
// segments can potentially be losslessly compressed! //format version indicates what version of these structures to use
for _ in 0..instruction_count{ SimulationState simulation_state //SimulationState is just under ClientState which includes Play/Pause events that the simulation doesn't know about.
// TODO: delta encode as much as possible (time,mousepos) //to read, greedily decode instructions until eof
i64 time loop{
physics::Instruction instruction //delta encode as much as possible (time,mousepos)
//strafe ticks are implied
//physics can be implied in an input-only bot file
TimedInstruction<SimulationInstruction> instruction
} }
*/ */
#[binrw] //error hiding mock code
#[brw(little)] mod simulation{
struct SegmentHeader{ #[super::binrw]
time:i64, #[brw(little)]
instruction_count:u32, pub struct State{}
block_id:BlockId, #[super::binrw]
} #[brw(little)]
#[binrw] pub struct Instruction{}
#[brw(little)]
struct Header{
physics_version:u32,
num_segments:u32,
#[br(count=num_segments)]
segments:Vec<SegmentHeader>,
} }
// mod instruction{
// #[super::binrw]
// #[brw(little)]
// pub struct TimedInstruction<Instruction:binrw::BinRead+binrw::BinWrite>{
// time:u64,
// instruction:Instruction
// }
// }
// mod timeline{
// #[super::binrw]
// #[brw(little)]
// pub struct Timeline<Instruction:binrw::BinRead+binrw::BinWrite>{
// #[bw(try_calc(u32::try_from(instructions.len())))]
// instruction_count:u32,
// #[br(count=instruction_count)]
// instructions:Vec<super::instruction::TimedInstruction<Instruction>>
// }
// }
//serious code
#[binrw] #[binrw]
#[brw(little)] #[brw(little)]
#[derive(Clone,Copy,Debug,id::Id)] #[derive(Clone,Copy,Debug,id::Id)]
pub struct SegmentId(u32); pub struct SegmentId(u32);
#[binrw]
#[brw(little)]
pub struct Segment{ pub struct Segment{
pub instructions:Vec<TimedPhysicsInstruction> state:simulation::State,
} //#[bw(try_calc(u32::try_from(instructions.len())))]
//instruction_count:u32,
//#[br(count=instruction_count)]
//instructions:Vec<instruction::TimedInstruction<simulation::Instruction>>
#[derive(Clone,Copy,Debug)] //please remember that strafe ticks are implicit! 33% smaller bot files
pub struct SegmentInfo{
/// time of the first instruction in this segment.
time:Time,
instruction_count:u32,
/// How many total instructions in segments up to and including this segment
/// Alternatively, the id of the first instruction be in the _next_ segment
instructions_subtotal:u64,
block_id:BlockId,
} }
pub struct StreamableBot<R:BinReaderExt>{ pub struct StreamableBot<R:BinReaderExt>{
file:crate::file::File<R>, file:crate::file::File<R>,
segment_map:Vec<SegmentInfo>, //timeline:timeline::Timeline<SegmentId>,
segment_id_to_block_id:Vec<crate::file::BlockId>,
} }
impl<R:BinReaderExt> StreamableBot<R>{ impl<R:BinReaderExt> StreamableBot<R>{
pub(crate) fn new(mut file:crate::file::File<R>)->Result<Self,Error>{ pub(crate) fn new(file:crate::file::File<R>)->Result<Self,Error>{
//assume the file seek is in the right place to start reading header Err(Error::InvalidHeader)
let header:Header=file.data_mut().read_le().map_err(Error::InvalidHeader)?;
let mut instructions_subtotal=0;
let segment_map=header.segments.into_iter().map(|SegmentHeader{time,instruction_count,block_id}|{
instructions_subtotal+=instruction_count as u64;
SegmentInfo{
time:Time::raw(time),
instruction_count,
instructions_subtotal,
block_id,
}
}).collect();
Ok(Self{
file,
segment_map,
})
} }
fn get_segment_info(&self,segment_id:SegmentId)->Result<SegmentInfo,Error>{ pub fn load_segment(&mut self,segment_id:SegmentId)->Result<Segment,Error>{
Ok(*self.segment_map.get(segment_id.get() as usize).ok_or(Error::InvalidSegmentId(segment_id))?) let block_id=*self.segment_id_to_block_id.get(segment_id.get() as usize).ok_or(Error::InvalidSegmentId(segment_id))?;
} let mut block=self.file.block_reader(block_id).map_err(Error::File)?;
pub fn find_segments_instruction_range(&self,start_instruction:u64,end_instruction:u64)->&[SegmentInfo]{ let segment=block.read_le().map_err(Error::InvalidSegment)?;
let start=self.segment_map.partition_point(|segment_info|segment_info.instructions_subtotal<start_instruction);
let end=self.segment_map.partition_point(|segment_info|segment_info.instructions_subtotal<end_instruction);
&self.segment_map[start..=end]
}
// pub fn find_segments_time_range(&self,start_time:Time,end_time:Time)->&[SegmentInfo]{
// // TODO: This is off by one, both should be one less
// let start=self.segment_map.partition_point(|segment_info|segment_info.time<start_time);
// let end=self.segment_map.partition_point(|segment_info|segment_info.time<end_time);
// &self.segment_map[start..=end]
// }
fn append_to_segment(&mut self,segment_info:SegmentInfo,segment:&mut Segment)->Result<(),Error>{
let mut block=self.file.block_reader(segment_info.block_id).map_err(Error::File)?;
for _ in 0..segment_info.instruction_count{
let instruction:newtypes::physics::TimedInstruction=block.read_le().map_err(Error::InvalidSegment)?;
segment.instructions.push(instruction.try_into().map_err(Error::SegmentConvert)?);
}
Ok(())
}
pub fn load_segment(&mut self,segment_info:SegmentInfo)->Result<Segment,Error>{
let mut segment=Segment{
instructions:Vec::with_capacity(segment_info.instruction_count as usize),
};
self.append_to_segment(segment_info,&mut segment)?;
Ok(segment)
}
pub fn read_all(&mut self)->Result<Segment,Error>{
let mut segment=Segment{
instructions:Vec::new(),
};
for i in 0..self.segment_map.len(){
let segment_info=self.segment_map[i];
self.append_to_segment(segment_info,&mut segment)?;
}
Ok(segment) Ok(segment)
} }
} }
const MAX_BLOCK_SIZE:usize=64*1024;//64 kB
pub fn write_bot<W:BinWriterExt>(mut writer:W,physics_version:u32,instructions:impl IntoIterator<Item=TimedPhysicsInstruction>)->Result<(),Error>{
// decide which instructions to put in which segment
// write segment 1 to block 1
// write segment N to block 2
// write rest of segments
// 1 2 3 4 5
// becomes
// [1 5] 2 3 4
struct SegmentHeaderInfo{
time:Time,
instruction_count:u32,
range:core::ops::Range<usize>
}
let mut segment_header_infos=Vec::new();
let mut raw_segments=std::io::Cursor::new(Vec::new());
// block info
let mut start_time=Time::ZERO;
let mut start_position=raw_segments.position() as usize;
let mut instruction_count=0;
let mut last_position=start_position;
let mut iter=instructions.into_iter();
macro_rules! collect_instruction{
($instruction:expr)=>{
let time=$instruction.time;
let instruction_writable:newtypes::physics::TimedInstruction=$instruction.try_into().map_err(Error::InstructionConvert)?;
instruction_writable.write_le(&mut raw_segments).map_err(Error::InstructionWrite)?;
instruction_count+=1;
let position=raw_segments.position() as usize;
// exceeds max block size
if MAX_BLOCK_SIZE<position-last_position{
segment_header_infos.push(SegmentHeaderInfo{
time:start_time,
instruction_count,
range:start_position..last_position,
});
start_position=last_position;
instruction_count=0;
start_time=time;
}
last_position=position;
}
}
// unroll one loop iteration to grab the starting time
if let Some(instruction)=iter.next(){
start_time=instruction.time;
collect_instruction!(instruction);
}
for instruction in iter{
collect_instruction!(instruction);
}
//last block, whatever size it happens to be
{
let final_position=raw_segments.position() as usize;
segment_header_infos.push(SegmentHeaderInfo{
time:start_time,
instruction_count,
range:start_position..final_position,
});
}
// drop cursor
let raw_segments=raw_segments.into_inner();
let num_segments=segment_header_infos.len();
// segments list is in chronological order
let make_segment_header=|block_id,&SegmentHeaderInfo{time,instruction_count,range:ref _range}|SegmentHeader{
time:time.get(),
instruction_count,
block_id,
};
let segments=if 2<num_segments{
let mut segments=Vec::with_capacity(num_segments);
// segment 1 is second block
if let Some(seg)=segment_header_infos.first(){
segments.push(make_segment_header(BlockId::new(1),seg));
}
// rest of segments start at fourth block
for (i,seg) in segment_header_infos[1..num_segments-1].iter().enumerate(){
make_segment_header(BlockId::new(3+i as u32),seg);
}
// last segment is third block
if let Some(seg)=segment_header_infos.last(){
segments.push(make_segment_header(BlockId::new(2),seg));
}
segments
}else{
// all segments in order
segment_header_infos.iter().enumerate().map(|(i,seg)|
make_segment_header(BlockId::new(1+i as u32),seg)
).collect()
};
let header=Header{
physics_version,
num_segments:num_segments as u32,
segments,
};
// map header is +1
let block_count=1+num_segments as u32;
let mut offset=crate::file::Header::calculate_size(block_count) as u64;
// block_location is one longer than block_count
let mut block_location=Vec::with_capacity(1+block_count as usize);
//probe header length
let mut bot_header_data=Vec::new();
binrw::BinWrite::write_le(&header,&mut std::io::Cursor::new(&mut bot_header_data)).map_err(Error::InvalidData)?;
// the first block location is the map header
block_location.push(offset);
offset+=bot_header_data.len() as u64;
block_location.push(offset);
// priming includes file header + first 3 blocks [bot header, first segment, last segment]
let priming=if 2<num_segments{
// segment 1 is block 2
if let Some(seg)=segment_header_infos.first(){
offset+=seg.range.len() as u64;
block_location.push(offset);
}
// last segment is block 3
if let Some(seg)=segment_header_infos.last(){
offset+=seg.range.len() as u64;
block_location.push(offset);
}
let priming=offset;
// rest of segments
for seg in &segment_header_infos[1..num_segments-1]{
offset+=seg.range.len() as u64;
block_location.push(offset);
}
priming
}else{
// all segments in order
for seg in &segment_header_infos{
offset+=seg.range.len() as u64;
block_location.push(offset);
}
offset
};
let file_header=crate::file::Header{
fourcc:crate::file::FourCC::Bot,
version:VERSION,
priming,
resource:0,
block_count,
block_location,
};
// write file header
writer.write_le(&file_header).map_err(Error::InvalidData)?;
// write bot header
writer.write(&bot_header_data).map_err(Error::IO)?;
// write blocks
if 2<num_segments{
// segment 1 is block 2
if let Some(seg)=segment_header_infos.first(){
writer.write(&raw_segments[seg.range.clone()]).map_err(Error::IO)?;
}
// last segment is block 3
if let Some(seg)=segment_header_infos.last(){
writer.write(&raw_segments[seg.range.clone()]).map_err(Error::IO)?;
}
// rest of segments
for seg in &segment_header_infos[1..num_segments-1]{
writer.write(&raw_segments[seg.range.clone()]).map_err(Error::IO)?;
}
}else{
// all segments in order
for seg in segment_header_infos{
writer.write(&raw_segments[seg.range]).map_err(Error::IO)?;
}
}
Ok(())
}

@ -73,16 +73,6 @@ pub struct Header{
#[br(count=block_count+1)] #[br(count=block_count+1)]
pub block_location:Vec<u64>, pub block_location:Vec<u64>,
} }
impl Header{
pub const fn calculate_size(block_count:u32)->usize{
4 // fourcc
+4 // version
+8 // priming
+16 // resource
+4 // block_count
+(block_count as usize+1)*8 // block_location
}
}
#[binrw] #[binrw]
#[brw(little)] #[brw(little)]

@ -6,7 +6,7 @@ use crate::file::BlockId;
use binrw::{binrw,BinReaderExt,BinWriterExt}; use binrw::{binrw,BinReaderExt,BinWriterExt};
use strafesnet_common::model; use strafesnet_common::model;
use strafesnet_common::aabb::Aabb; use strafesnet_common::aabb::Aabb;
use strafesnet_common::bvh::{BvhNode,RecursiveContent}; use strafesnet_common::bvh::BvhNode;
use strafesnet_common::gameplay_modes; use strafesnet_common::gameplay_modes;
#[derive(Debug)] #[derive(Debug)]
@ -95,6 +95,21 @@ enum ResourceType{
//Video, //Video,
//Animation, //Animation,
} }
const RESOURCE_TYPE_VARIANT_COUNT:u8=2;
#[binrw]
#[brw(little)]
struct ResourceId(u128);
impl ResourceId{
fn resource_type(&self)->Option<ResourceType>{
let discriminant=self.0 as u8;
//TODO: use this when it is stabilized https://github.com/rust-lang/rust/issues/73662
//if (discriminant as usize)<std::mem::variant_count::<ResourceType>(){
match discriminant<RESOURCE_TYPE_VARIANT_COUNT{
true=>Some(unsafe{std::mem::transmute::<u8,ResourceType>(discriminant)}),
false=>None,
}
}
}
struct ResourceMap<T>{ struct ResourceMap<T>{
meshes:HashMap<strafesnet_common::model::MeshId,T>, meshes:HashMap<strafesnet_common::model::MeshId,T>,
@ -121,6 +136,11 @@ struct ResourceBlockHeader{
resource:ResourceType, resource:ResourceType,
id:BlockId, id:BlockId,
} }
#[binrw]
#[brw(little)]
struct ResourceExternalHeader{
resource_uuid:ResourceId,
}
#[binrw] #[binrw]
#[brw(little)] #[brw(little)]
@ -233,7 +253,7 @@ impl<R:BinReaderExt> StreamableMap<R>{
} }
pub fn get_intersecting_region_block_ids(&self,aabb:&Aabb)->Vec<BlockId>{ pub fn get_intersecting_region_block_ids(&self,aabb:&Aabb)->Vec<BlockId>{
let mut block_ids=Vec::new(); let mut block_ids=Vec::new();
self.bvh.sample_aabb(aabb,&mut |&block_id|block_ids.push(block_id)); self.bvh.the_tester(aabb,&mut |&block_id|block_ids.push(block_id));
block_ids block_ids
} }
pub fn load_region(&mut self,block_id:BlockId)->Result<Vec<(model::ModelId,model::Model)>,Error>{ pub fn load_region(&mut self,block_id:BlockId)->Result<Vec<(model::ModelId,model::Model)>,Error>{
@ -287,60 +307,12 @@ impl<R:BinReaderExt> StreamableMap<R>{
} }
} }
// silly redefinition of Bvh for determining the size of subnodes
// without duplicating work by running weight calculation recursion top down on every node
pub struct BvhWeightNode<W,T>{
content:RecursiveContent<BvhWeightNode<W,T>,T>,
weight:W,
aabb:Aabb,
}
impl <W,T> BvhWeightNode<W,T>{
pub const fn weight(&self)->&W{
&self.weight
}
pub const fn aabb(&self)->&Aabb{
&self.aabb
}
pub fn into_content(self)->RecursiveContent<BvhWeightNode<W,T>,T>{
self.content
}
pub fn into_visitor<F:FnMut(T)>(self,f:&mut F){
match self.content{
RecursiveContent::Leaf(model)=>f(model),
RecursiveContent::Branch(children)=>for child in children{
child.into_visitor(f)
},
}
}
}
pub fn weigh_contents<T,W:Copy+std::iter::Sum<W>,F:Fn(&T)->W>(node:BvhNode<T>,f:&F)->BvhWeightNode<W,T>{
let (content,aabb)=node.into_inner();
match content{
RecursiveContent::Leaf(model)=>BvhWeightNode{
weight:f(&model),
content:RecursiveContent::Leaf(model),
aabb,
},
RecursiveContent::Branch(children)=>{
let branch:Vec<BvhWeightNode<W,T>>=children.into_iter().map(|child|
weigh_contents(child,f)
).collect();
BvhWeightNode{
weight:branch.iter().map(|node|node.weight).sum(),
content:RecursiveContent::Branch(branch),
aabb,
}
},
}
}
const BVH_NODE_MAX_WEIGHT:usize=64*1024;//64 kB const BVH_NODE_MAX_WEIGHT:usize=64*1024;//64 kB
fn collect_spacial_blocks( fn collect_spacial_blocks(
block_location:&mut Vec<u64>, block_location:&mut Vec<u64>,
block_headers:&mut Vec<SpacialBlockHeader>, block_headers:&mut Vec<SpacialBlockHeader>,
sequential_block_data:&mut std::io::Cursor<&mut Vec<u8>>, sequential_block_data:&mut std::io::Cursor<&mut Vec<u8>>,
bvh_node:BvhWeightNode<usize,(model::ModelId,newtypes::model::Model)> bvh_node:strafesnet_common::bvh::BvhWeightNode<usize,(model::ModelId,newtypes::model::Model)>
)->Result<(),Error>{ )->Result<(),Error>{
//inspect the node weights top-down. //inspect the node weights top-down.
//When a node weighs less than the limit, //When a node weighs less than the limit,
@ -390,7 +362,7 @@ pub fn write_map<W:BinWriterExt>(mut writer:W,map:strafesnet_common::map::Comple
} }
Ok(((model::ModelId::new(model_id as u32),model.into()),aabb)) Ok(((model::ModelId::new(model_id as u32),model.into()),aabb))
}).collect::<Result<Vec<_>,_>>()?; }).collect::<Result<Vec<_>,_>>()?;
let bvh=weigh_contents(strafesnet_common::bvh::generate_bvh(boxen),&|_|std::mem::size_of::<newtypes::model::Model>()); let bvh=strafesnet_common::bvh::generate_bvh(boxen).weigh_contents(&|_|std::mem::size_of::<newtypes::model::Model>());
//build blocks //build blocks
//block location is initialized with two values //block location is initialized with two values
//the first value represents the location of the first byte after the file header //the first value represents the location of the first byte after the file header
@ -440,26 +412,28 @@ pub fn write_map<W:BinWriterExt>(mut writer:W,map:strafesnet_common::map::Comple
attributes:map.attributes.into_iter().map(Into::into).collect(), attributes:map.attributes.into_iter().map(Into::into).collect(),
render_configs:map.render_configs.into_iter().map(Into::into).collect(), render_configs:map.render_configs.into_iter().map(Into::into).collect(),
}; };
//probe header length let mut file_header=crate::file::Header{
let mut map_header_data=Vec::new();
binrw::BinWrite::write_le(&map_header,&mut std::io::Cursor::new(&mut map_header_data)).map_err(Error::InvalidData)?;
// calculate final file header
let mut offset=crate::file::Header::calculate_size(block_count) as u64;
offset+=map_header_data.len() as u64;
// priming includes map header
let priming=offset;
for position in &mut block_location{
*position+=offset;
}
let file_header=crate::file::Header{
fourcc:crate::file::FourCC::Map, fourcc:crate::file::FourCC::Map,
version:0, version:0,
priming, priming:0,
resource:0, resource:0,
block_count, block_count,
block_location, block_location,
}; };
//probe header length
let mut file_header_data=Vec::new();
binrw::BinWrite::write_le(&file_header,&mut std::io::Cursor::new(&mut file_header_data)).map_err(Error::InvalidData)?;
let mut map_header_data=Vec::new();
binrw::BinWrite::write_le(&map_header,&mut std::io::Cursor::new(&mut map_header_data)).map_err(Error::InvalidData)?;
//update file header according to probe data
let mut offset=file_header_data.len() as u64;
file_header.priming=offset;
file_header.block_location[0]=offset;
offset+=map_header_data.len() as u64;
for position in &mut file_header.block_location[1..]{
*position+=offset;
}
//write (updated) file header //write (updated) file header
writer.write_le(&file_header).map_err(Error::InvalidData)?; writer.write_le(&file_header).map_err(Error::InvalidData)?;

@ -1,9 +1,7 @@
mod common; mod common;
pub mod aabb; pub mod aabb;
pub mod model; pub mod model;
pub mod mouse;
pub mod integer; pub mod integer;
pub mod physics;
pub mod gameplay_modes; pub mod gameplay_modes;
pub mod gameplay_style; pub mod gameplay_style;
pub mod gameplay_attributes; pub mod gameplay_attributes;

@ -1,9 +1,3 @@
pub const fn flag(b:bool,mask:u8)->u8{ pub const fn flag(b:bool,mask:u8)->u8{
(-(b as i8) as u8)&mask (-(b as i8) as u8)&mask
} }
pub fn bool_from_u8(value:u8)->bool{
value!=0
}
pub fn bool_into_u8(value:&bool)->u8{
*value as u8
}

@ -38,23 +38,6 @@ pub struct Ratio64Vec2{
pub x:Ratio64, pub x:Ratio64,
pub y:Ratio64, pub y:Ratio64,
} }
impl TryInto<strafesnet_common::integer::Ratio64Vec2> for Ratio64Vec2{
type Error=RatioError;
fn try_into(self)->Result<strafesnet_common::integer::Ratio64Vec2,Self::Error>{
Ok(strafesnet_common::integer::Ratio64Vec2{
x:self.x.try_into()?,
y:self.y.try_into()?,
})
}
}
impl From<strafesnet_common::integer::Ratio64Vec2> for Ratio64Vec2{
fn from(value:strafesnet_common::integer::Ratio64Vec2)->Self{
Self{
x:value.x.into(),
y:value.y.into(),
}
}
}
pub type Angle32=i32; pub type Angle32=i32;
pub type Planar64=i64; pub type Planar64=i64;

@ -1,25 +0,0 @@
use super::integer::Time;
#[binrw::binrw]
#[brw(little)]
pub struct MouseState{
pub pos:[i32;2],
pub time:Time,
}
impl<T> Into<strafesnet_common::mouse::MouseState<T>> for MouseState{
fn into(self)->strafesnet_common::mouse::MouseState<T>{
strafesnet_common::mouse::MouseState{
pos:self.pos.into(),
time:strafesnet_common::integer::Time::raw(self.time),
}
}
}
impl<T> From<strafesnet_common::mouse::MouseState<T>> for MouseState{
fn from(value:strafesnet_common::mouse::MouseState<T>)->Self{
Self{
pos:value.pos.to_array(),
time:value.time.get(),
}
}
}

@ -1,156 +0,0 @@
use super::integer::Time;
use super::common::{bool_from_u8,bool_into_u8};
type TimedPhysicsInstruction=strafesnet_common::instruction::TimedInstruction<strafesnet_common::physics::Instruction,strafesnet_common::physics::Time>;
#[binrw::binrw]
#[brw(little)]
pub struct TimedInstruction{
pub time:Time,
pub instruction:Instruction,
}
impl TryInto<TimedPhysicsInstruction> for TimedInstruction{
type Error=super::integer::RatioError;
fn try_into(self)->Result<TimedPhysicsInstruction,Self::Error>{
Ok(strafesnet_common::instruction::TimedInstruction{
time:strafesnet_common::integer::Time::raw(self.time),
instruction:self.instruction.try_into()?,
})
}
}
impl TryFrom<TimedPhysicsInstruction> for TimedInstruction{
type Error=super::physics::InstructionConvert;
fn try_from(value:TimedPhysicsInstruction)->Result<Self,Self::Error>{
Ok(Self{
time:value.time.get(),
instruction:value.instruction.try_into()?,
})
}
}
#[binrw::binrw]
#[brw(little)]
pub enum Instruction{
#[brw(magic=0u8)]
ReplaceMouse{
m0:super::mouse::MouseState,
m1:super::mouse::MouseState
},
#[brw(magic=1u8)]
SetNextMouse(super::mouse::MouseState),
#[brw(magic=2u8)]
SetMoveRight(
#[br(map=bool_from_u8)]
#[bw(map=bool_into_u8)]
bool),
#[brw(magic=3u8)]
SetMoveUp(
#[br(map=bool_from_u8)]
#[bw(map=bool_into_u8)]
bool),
#[brw(magic=4u8)]
SetMoveBack(
#[br(map=bool_from_u8)]
#[bw(map=bool_into_u8)]
bool),
#[brw(magic=5u8)]
SetMoveLeft(
#[br(map=bool_from_u8)]
#[bw(map=bool_into_u8)]
bool),
#[brw(magic=6u8)]
SetMoveDown(
#[br(map=bool_from_u8)]
#[bw(map=bool_into_u8)]
bool),
#[brw(magic=7u8)]
SetMoveForward(
#[br(map=bool_from_u8)]
#[bw(map=bool_into_u8)]
bool),
#[brw(magic=8u8)]
SetJump(
#[br(map=bool_from_u8)]
#[bw(map=bool_into_u8)]
bool),
#[brw(magic=9u8)]
SetZoom(
#[br(map=bool_from_u8)]
#[bw(map=bool_into_u8)]
bool),
#[brw(magic=10u8)]
Reset,
#[brw(magic=11u8)]
Restart(super::gameplay_modes::ModeId),
#[brw(magic=12u8)]
Spawn(super::gameplay_modes::ModeId,super::gameplay_modes::StageId),
#[brw(magic=13u8)]
PracticeFly,
#[brw(magic=14u8)]
SetSensitivity(super::integer::Ratio64Vec2),
#[brw(magic=255u8)]
Idle,
}
#[derive(Debug)]
pub enum InstructionConvert{
/// This is an instruction that can be dropped when serializing
DropInstruction,
}
impl std::fmt::Display for InstructionConvert{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for InstructionConvert{}
impl TryInto<strafesnet_common::physics::Instruction> for Instruction{
type Error=super::integer::RatioError;
fn try_into(self)->Result<strafesnet_common::physics::Instruction,Self::Error>{
Ok(match self{
Instruction::ReplaceMouse{m0,m1}=>strafesnet_common::physics::Instruction::Mouse(strafesnet_common::physics::MouseInstruction::ReplaceMouse{m0:m0.into(),m1:m1.into()}),
Instruction::SetNextMouse(m)=>strafesnet_common::physics::Instruction::Mouse(strafesnet_common::physics::MouseInstruction::SetNextMouse(m.into())),
Instruction::SetMoveRight(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveRight(state.into())),
Instruction::SetMoveUp(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveUp(state.into())),
Instruction::SetMoveBack(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveBack(state.into())),
Instruction::SetMoveLeft(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveLeft(state.into())),
Instruction::SetMoveDown(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveDown(state.into())),
Instruction::SetMoveForward(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveForward(state.into())),
Instruction::SetJump(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetJump(state.into())),
Instruction::SetZoom(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetZoom(state.into())),
Instruction::Reset=>strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Reset),
Instruction::Restart(mode_id)=>strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Restart(strafesnet_common::gameplay_modes::ModeId::new(mode_id))),
Instruction::Spawn(mode_id,stage_id)=>strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Spawn(
strafesnet_common::gameplay_modes::ModeId::new(mode_id),
strafesnet_common::gameplay_modes::StageId::new(stage_id),
)),
Instruction::PracticeFly=>strafesnet_common::physics::Instruction::Misc(strafesnet_common::physics::MiscInstruction::PracticeFly),
Instruction::SetSensitivity(sensitivity)=>strafesnet_common::physics::Instruction::Misc(strafesnet_common::physics::MiscInstruction::SetSensitivity(sensitivity.try_into()?)),
Instruction::Idle=>strafesnet_common::physics::Instruction::Idle,
})
}
}
impl TryFrom<strafesnet_common::physics::Instruction> for Instruction{
type Error=InstructionConvert;
fn try_from(value:strafesnet_common::physics::Instruction)->Result<Self,Self::Error>{
match value{
strafesnet_common::physics::Instruction::Mouse(strafesnet_common::physics::MouseInstruction::ReplaceMouse{m0,m1})=>Ok(Instruction::ReplaceMouse{m0:m0.into(),m1:m1.into()}),
strafesnet_common::physics::Instruction::Mouse(strafesnet_common::physics::MouseInstruction::SetNextMouse(m))=>Ok(Instruction::SetNextMouse(m.into())),
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveRight(state))=>Ok(Instruction::SetMoveRight(state.into())),
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveUp(state))=>Ok(Instruction::SetMoveUp(state.into())),
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveBack(state))=>Ok(Instruction::SetMoveBack(state.into())),
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveLeft(state))=>Ok(Instruction::SetMoveLeft(state.into())),
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveDown(state))=>Ok(Instruction::SetMoveDown(state.into())),
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveForward(state))=>Ok(Instruction::SetMoveForward(state.into())),
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetJump(state))=>Ok(Instruction::SetJump(state.into())),
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetZoom(state))=>Ok(Instruction::SetZoom(state.into())),
strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Reset)=>Ok(Instruction::Reset),
strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Restart(mode_id))=>Ok(Instruction::Restart(mode_id.get())),
strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Spawn(mode_id,stage_id))=>Ok(Instruction::Spawn(
mode_id.get(),
stage_id.get(),
)),
strafesnet_common::physics::Instruction::Misc(strafesnet_common::physics::MiscInstruction::PracticeFly)=>Ok(Instruction::PracticeFly),
strafesnet_common::physics::Instruction::Misc(strafesnet_common::physics::MiscInstruction::SetSensitivity(sensitivity))=>Ok(Instruction::SetSensitivity(sensitivity.into())),
strafesnet_common::physics::Instruction::Idle=>Ok(Instruction::Idle),
}
}
}

@ -1,38 +0,0 @@
[package]
name = "map-tool"
version = "1.7.0"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.75"
clap = { version = "4.4.2", features = ["derive"] }
flate2 = "1.0.27"
futures = "0.3.31"
image = "0.25.2"
image_dds = "0.7.1"
lazy-regex = "3.1.0"
rbx_asset = { version = "0.2.5", registry = "strafesnet" }
rbx_binary = { version = "0.7.4", registry = "strafesnet" }
rbx_dom_weak = { version = "2.7.0", registry = "strafesnet" }
rbx_reflection_database = { version = "0.2.10", registry = "strafesnet" }
rbx_xml = { version = "0.13.3", 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" }
thiserror = "2.0.11"
tokio = { version = "1.43.0", features = ["macros", "rt-multi-thread", "fs"] }
vbsp = { version = "0.7.0-codegen5", registry = "strafesnet", default-features = false }
vbsp-entities = { version = "0.1.0", registry = "strafesnet", default-features = false, features = ["css"]}
vmdl = "0.2.0"
vmt-parser = "0.2.0"
vpk = "0.2.0"
vtf = "0.3.0"
#[profile.release]
#lto = true
#strip = true
#codegen-units = 1

@ -1,23 +0,0 @@
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

@ -1,2 +0,0 @@
# map-tool

@ -1,30 +0,0 @@
mod roblox;
mod source;
use clap::{Parser,Subcommand};
use anyhow::Result as AResult;
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
#[command(propagate_version = true)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands{
#[command(flatten)]
Roblox(roblox::Commands),
#[command(flatten)]
Source(source::Commands),
}
#[tokio::main]
async fn main()->AResult<()>{
let cli=Cli::parse();
match cli.command{
Commands::Roblox(commands)=>commands.run().await,
Commands::Source(commands)=>commands.run().await,
}
}

@ -1,431 +0,0 @@
use std::path::{Path,PathBuf};
use std::io::{Cursor,Read,Seek};
use std::collections::HashSet;
use clap::{Args,Subcommand};
use anyhow::Result as AResult;
use rbx_dom_weak::Instance;
use strafesnet_deferred_loader::deferred_loader::LoadFailureMode;
use rbxassetid::RobloxAssetId;
use tokio::io::AsyncReadExt;
const DOWNLOAD_LIMIT:usize=16;
#[derive(Subcommand)]
pub enum Commands{
RobloxToSNF(RobloxToSNFSubcommand),
DownloadAssets(DownloadAssetsSubcommand),
}
#[derive(Args)]
pub struct RobloxToSNFSubcommand {
#[arg(long)]
output_folder:PathBuf,
#[arg(required=true)]
input_files:Vec<PathBuf>,
}
#[derive(Args)]
pub struct DownloadAssetsSubcommand{
#[arg(required=true)]
roblox_files:Vec<PathBuf>,
// #[arg(long)]
// cookie_file:Option<String>,
}
impl Commands{
pub async fn run(self)->AResult<()>{
match self{
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()),
).await,
}
}
}
#[allow(unused)]
#[derive(Debug)]
enum LoadDomError{
IO(std::io::Error),
Binary(rbx_binary::DecodeError),
Xml(rbx_xml::DecodeError),
UnknownFormat,
}
fn load_dom<R:Read+Seek>(mut input:R)->Result<rbx_dom_weak::WeakDom,LoadDomError>{
let mut first_8=[0u8;8];
input.read_exact(&mut first_8).map_err(LoadDomError::IO)?;
input.rewind().map_err(LoadDomError::IO)?;
match &first_8{
b"<roblox!"=>rbx_binary::from_reader(input).map_err(LoadDomError::Binary),
b"<roblox "=>rbx_xml::from_reader(input,rbx_xml::DecodeOptions::default()).map_err(LoadDomError::Xml),
_=>Err(LoadDomError::UnknownFormat),
}
}
/* The ones I'm interested in:
Beam.Texture
Decal.Texture
FileMesh.MeshId
FileMesh.TextureId
MaterialVariant.ColorMap
MaterialVariant.MetalnessMap
MaterialVariant.NormalMap
MaterialVariant.RoughnessMap
MeshPart.MeshId
MeshPart.TextureID
ParticleEmitter.Texture
Sky.MoonTextureId
Sky.SkyboxBk
Sky.SkyboxDn
Sky.SkyboxFt
Sky.SkyboxLf
Sky.SkyboxRt
Sky.SkyboxUp
Sky.SunTextureId
SurfaceAppearance.ColorMap
SurfaceAppearance.MetalnessMap
SurfaceAppearance.NormalMap
SurfaceAppearance.RoughnessMap
SurfaceAppearance.TexturePack
*/
fn accumulate_content_id(content_list:&mut HashSet<RobloxAssetId>,object:&Instance,property:&str){
if let Some(rbx_dom_weak::types::Variant::Content(content))=object.properties.get(property){
let url:&str=content.as_ref();
if let Ok(asset_id)=url.parse(){
content_list.insert(asset_id);
}else{
println!("Content failed to parse into AssetID: {:?}",content);
}
}else{
println!("property={} does not exist for class={}",property,object.class.as_str());
}
}
async fn read_entire_file(path:impl AsRef<Path>)->Result<Cursor<Vec<u8>>,std::io::Error>{
let mut file=tokio::fs::File::open(path).await?;
let mut data=Vec::new();
file.read_to_end(&mut data).await?;
Ok(Cursor::new(data))
}
#[derive(Default)]
struct UniqueAssets{
meshes:HashSet<RobloxAssetId>,
unions:HashSet<RobloxAssetId>,
textures:HashSet<RobloxAssetId>,
}
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"),
"FileMesh"=>accumulate_content_id(&mut self.textures,object,"TextureId"),
"MeshPart"=>{
accumulate_content_id(&mut self.textures,object,"TextureID");
accumulate_content_id(&mut self.meshes,object,"MeshId");
},
"SpecialMesh"=>accumulate_content_id(&mut self.meshes,object,"MeshId"),
"ParticleEmitter"=>accumulate_content_id(&mut self.textures,object,"Texture"),
"Sky"=>{
accumulate_content_id(&mut self.textures,object,"MoonTextureId");
accumulate_content_id(&mut self.textures,object,"SkyboxBk");
accumulate_content_id(&mut self.textures,object,"SkyboxDn");
accumulate_content_id(&mut self.textures,object,"SkyboxFt");
accumulate_content_id(&mut self.textures,object,"SkyboxLf");
accumulate_content_id(&mut self.textures,object,"SkyboxRt");
accumulate_content_id(&mut self.textures,object,"SkyboxUp");
accumulate_content_id(&mut self.textures,object,"SunTextureId");
},
"UnionOperation"=>accumulate_content_id(&mut self.unions,object,"AssetId"),
_=>(),
}
}
}
#[allow(unused)]
#[derive(Debug)]
enum UniqueAssetError{
IO(std::io::Error),
LoadDom(LoadDomError),
}
async fn unique_assets(path:&Path)->Result<UniqueAssets,UniqueAssetError>{
// read entire file
let mut assets=UniqueAssets::default();
let data=read_entire_file(path).await.map_err(UniqueAssetError::IO)?;
let dom=load_dom(data).map_err(UniqueAssetError::LoadDom)?;
for object in dom.into_raw().1.into_values(){
assets.collect(&object);
}
Ok(assets)
}
enum DownloadType{
Texture(RobloxAssetId),
Mesh(RobloxAssetId),
Union(RobloxAssetId),
}
impl DownloadType{
fn path(&self)->PathBuf{
match self{
DownloadType::Texture(asset_id)=>format!("downloaded_textures/{}",asset_id.0.to_string()).into(),
DownloadType::Mesh(asset_id)=>format!("meshes/{}",asset_id.0.to_string()).into(),
DownloadType::Union(asset_id)=>format!("unions/{}",asset_id.0.to_string()).into(),
}
}
fn asset_id(&self)->u64{
match self{
DownloadType::Texture(asset_id)=>asset_id.0,
DownloadType::Mesh(asset_id)=>asset_id.0,
DownloadType::Union(asset_id)=>asset_id.0,
}
}
}
enum DownloadResult{
Cached(PathBuf),
Data(Vec<u8>),
Failed,
}
#[derive(Default,Debug)]
struct Stats{
total_assets:u32,
cached_assets:u32,
downloaded_assets:u32,
failed_downloads:u32,
timed_out_downloads:u32,
}
async fn download_retry(stats:&mut Stats,context:&rbx_asset::cookie::CookieContext,download_instruction:DownloadType)->Result<DownloadResult,std::io::Error>{
stats.total_assets+=1;
let download_instruction=download_instruction;
// check if file exists on disk
let path=download_instruction.path();
if tokio::fs::try_exists(path.as_path()).await?{
stats.cached_assets+=1;
return Ok(DownloadResult::Cached(path));
}
let asset_id=download_instruction.asset_id();
// if not, download file
let mut retry=0;
const BACKOFF_MUL:f32=1.3956124250860895286;//exp(1/3)
let mut backoff=1000f32;
loop{
let asset_result=context.get_asset(rbx_asset::cookie::GetAssetRequest{
asset_id,
version:None,
}).await;
match asset_result{
Ok(asset_result)=>{
stats.downloaded_assets+=1;
tokio::fs::write(path,&asset_result).await?;
break Ok(DownloadResult::Data(asset_result));
},
Err(rbx_asset::cookie::GetError::Response(rbx_asset::ResponseError::StatusCodeWithUrlAndBody(scwuab)))=>{
if scwuab.status_code.as_u16()==429{
if retry==12{
println!("Giving up asset download {asset_id}");
stats.timed_out_downloads+=1;
break Ok(DownloadResult::Failed);
}
println!("Hit roblox rate limit, waiting {:.0}ms...",backoff);
tokio::time::sleep(std::time::Duration::from_millis(backoff as u64)).await;
backoff*=BACKOFF_MUL;
retry+=1;
}else{
stats.failed_downloads+=1;
println!("weird scuwab error: {scwuab:?}");
break Ok(DownloadResult::Failed);
}
},
Err(e)=>{
stats.failed_downloads+=1;
println!("sadly error: {e}");
break Ok(DownloadResult::Failed);
},
}
}
}
#[derive(Debug,thiserror::Error)]
enum ConvertTextureError{
#[error("Io error {0:?}")]
Io(#[from]std::io::Error),
#[error("Image error {0:?}")]
Image(#[from]image::ImageError),
#[error("DDS create error {0:?}")]
DDS(#[from]image_dds::CreateDdsError),
#[error("DDS write error {0:?}")]
DDSWrite(#[from]image_dds::ddsfile::Error),
}
async fn convert_texture(asset_id:RobloxAssetId,download_result:DownloadResult)->Result<(),ConvertTextureError>{
let data=match download_result{
DownloadResult::Cached(path)=>tokio::fs::read(path).await?,
DownloadResult::Data(data)=>data,
DownloadResult::Failed=>return Ok(()),
};
// image::ImageFormat::Png
// image::ImageFormat::Jpeg
let image=image::load_from_memory(&data)?.to_rgba8();
// pick format
let format=if image.width()%4!=0||image.height()%4!=0{
image_dds::ImageFormat::Rgba8UnormSrgb
}else{
image_dds::ImageFormat::BC7RgbaUnormSrgb
};
//this fails if the image dimensions are not a multiple of 4
let dds=image_dds::dds_from_image(
&image,
format,
image_dds::Quality::Slow,
image_dds::Mipmaps::GeneratedAutomatic,
)?;
let file_name=format!("textures/{}.dds",asset_id.0);
let mut file=std::fs::File::create(file_name)?;
dds.write(&mut file)?;
Ok(())
}
async fn download_assets(paths:Vec<PathBuf>,cookie:rbx_asset::cookie::Cookie)->AResult<()>{
tokio::try_join!(
tokio::fs::create_dir_all("downloaded_textures"),
tokio::fs::create_dir_all("textures"),
tokio::fs::create_dir_all("meshes"),
tokio::fs::create_dir_all("unions"),
)?;
// use mpsc
let thread_limit=std::thread::available_parallelism()?.get();
let (send_assets,mut recv_assets)=tokio::sync::mpsc::channel(DOWNLOAD_LIMIT);
let (send_texture,mut recv_texture)=tokio::sync::mpsc::channel(thread_limit);
// map decode dispatcher
// read files multithreaded
// produce UniqueAssetsResult per file
tokio::spawn(async move{
// move send so it gets dropped when all maps have been decoded
// closing the channel
let mut it=paths.into_iter();
static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
SEM.add_permits(thread_limit);
while let (Ok(permit),Some(path))=(SEM.acquire().await,it.next()){
let send=send_assets.clone();
tokio::spawn(async move{
let result=unique_assets(path.as_path()).await;
_=send.send(result).await;
drop(permit);
});
}
});
// download manager
// insert into global unique assets guy
// add to download queue if the asset is globally unique and does not already exist on disk
let mut stats=Stats::default();
let context=rbx_asset::cookie::CookieContext::new(cookie);
let mut globally_unique_assets=UniqueAssets::default();
// pop a job = retry_queue.pop_front() or ingest(recv.recv().await)
// SLOW MODE:
// acquire all permits
// drop all permits
// pop one job
// if it succeeds go into fast mode
// FAST MODE:
// acquire one permit
// pop a job
let download_thread=tokio::spawn(async move{
while let Some(result)=recv_assets.recv().await{
let unique_assets=match result{
Ok(unique_assets)=>unique_assets,
Err(e)=>{
println!("error: {e:?}");
continue;
},
};
for texture_id in unique_assets.textures{
if globally_unique_assets.textures.insert(texture_id){
let data=download_retry(&mut stats,&context,DownloadType::Texture(texture_id)).await?;
send_texture.send((texture_id,data)).await?;
}
}
for mesh_id in unique_assets.meshes{
if globally_unique_assets.meshes.insert(mesh_id){
download_retry(&mut stats,&context,DownloadType::Mesh(mesh_id)).await?;
}
}
for union_id in unique_assets.unions{
if globally_unique_assets.unions.insert(union_id){
download_retry(&mut stats,&context,DownloadType::Union(union_id)).await?;
}
}
}
dbg!(stats);
Ok::<(),anyhow::Error>(())
});
static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
SEM.add_permits(thread_limit);
while let (Ok(permit),Some((asset_id,download_result)))=(SEM.acquire().await,recv_texture.recv().await){
tokio::spawn(async move{
let result=convert_texture(asset_id,download_result).await;
drop(permit);
result.unwrap();
});
}
download_thread.await??;
_=SEM.acquire_many(thread_limit as u32).await.unwrap();
Ok(())
}
#[derive(Debug)]
#[allow(dead_code)]
enum ConvertError{
IO(std::io::Error),
SNFMap(strafesnet_snf::map::Error),
RobloxRead(strafesnet_rbx_loader::ReadError),
RobloxLoad(strafesnet_rbx_loader::LoadError),
}
impl std::fmt::Display for ConvertError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
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?;
let model=strafesnet_rbx_loader::read(
std::io::Cursor::new(entire_file)
).map_err(ConvertError::RobloxRead)?;
let mut place=model.into_place();
place.run_scripts();
let map=place.to_snf(LoadFailureMode::DefaultToNone).map_err(ConvertError::RobloxLoad)?;
let mut dest=output_folder;
dest.push(path.file_stem().unwrap());
dest.set_extension("snfm");
let file=std::fs::File::create(dest).map_err(ConvertError::IO)?;
strafesnet_snf::map::write_map(file,map).map_err(ConvertError::SNFMap)?;
Ok(())
}
async fn roblox_to_snf(paths:Vec<std::path::PathBuf>,output_folder:PathBuf)->AResult<()>{
let start=std::time::Instant::now();
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);
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;
drop(permit);
match result{
Ok(())=>(),
Err(e)=>println!("Convert error: {e:?}"),
}
});
}
_=SEM.acquire_many(thread_limit as u32).await.unwrap();
println!("elapsed={:?}", start.elapsed());
Ok(())
}

@ -1,514 +0,0 @@
use std::path::{Path,PathBuf};
use std::borrow::Cow;
use clap::{Args,Subcommand};
use anyhow::Result as AResult;
use futures::StreamExt;
use strafesnet_bsp_loader::loader::BspFinder;
use strafesnet_deferred_loader::loader::Loader;
use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLoader,RenderConfigDeferredLoader};
use vbsp_entities::css::Entity;
#[derive(Subcommand)]
pub enum Commands{
SourceToSNF(SourceToSNFSubcommand),
ExtractTextures(ExtractTexturesSubcommand),
VPKContents(VPKContentsSubcommand),
BSPContents(BSPContentsSubcommand),
}
#[derive(Args)]
pub struct SourceToSNFSubcommand {
#[arg(long)]
output_folder:PathBuf,
#[arg(required=true)]
input_files:Vec<PathBuf>,
#[arg(long)]
vpks:Vec<PathBuf>,
}
#[derive(Args)]
pub struct ExtractTexturesSubcommand{
#[arg(required=true)]
bsp_files:Vec<PathBuf>,
#[arg(long)]
vpks:Vec<PathBuf>,
}
#[derive(Args)]
pub struct VPKContentsSubcommand {
#[arg(long)]
input_file:PathBuf,
}
#[derive(Args)]
pub struct BSPContentsSubcommand {
#[arg(long)]
input_file:PathBuf,
}
impl Commands{
pub async fn run(self)->AResult<()>{
match self{
Commands::SourceToSNF(subcommand)=>source_to_snf(subcommand.input_files,subcommand.output_folder,subcommand.vpks).await,
Commands::ExtractTextures(subcommand)=>extract_textures(subcommand.bsp_files,subcommand.vpks).await,
Commands::VPKContents(subcommand)=>vpk_contents(subcommand.input_file),
Commands::BSPContents(subcommand)=>bsp_contents(subcommand.input_file),
}
}
}
enum VMTContent{
VMT(String),
VTF(String),
Patch(vmt_parser::material::PatchMaterial),
Unsupported,//don't want to deal with whatever vmt variant
Unresolved,//could not locate a texture because of vmt content
}
impl VMTContent{
fn vtf(opt:Option<String>)->Self{
match opt{
Some(s)=>Self::VTF(s),
None=>Self::Unresolved,
}
}
}
fn get_some_texture(material:vmt_parser::material::Material)->VMTContent{
//just grab some texture from somewhere for now
match material{
vmt_parser::material::Material::LightMappedGeneric(mat)=>VMTContent::vtf(Some(mat.base_texture)),
vmt_parser::material::Material::VertexLitGeneric(mat)=>VMTContent::vtf(mat.base_texture.or(mat.decal_texture)),//this just dies if there is none
vmt_parser::material::Material::VertexLitGenericDx6(mat)=>VMTContent::vtf(mat.base_texture.or(mat.decal_texture)),
vmt_parser::material::Material::UnlitGeneric(mat)=>VMTContent::vtf(mat.base_texture),
vmt_parser::material::Material::UnlitTwoTexture(mat)=>VMTContent::vtf(mat.base_texture),
vmt_parser::material::Material::Water(mat)=>VMTContent::vtf(mat.base_texture),
vmt_parser::material::Material::WorldVertexTransition(mat)=>VMTContent::vtf(Some(mat.base_texture)),
vmt_parser::material::Material::EyeRefract(mat)=>VMTContent::vtf(Some(mat.cornea_texture)),
vmt_parser::material::Material::SubRect(mat)=>VMTContent::VMT(mat.material),//recursive
vmt_parser::material::Material::Sprite(mat)=>VMTContent::vtf(Some(mat.base_texture)),
vmt_parser::material::Material::SpriteCard(mat)=>VMTContent::vtf(mat.base_texture),
vmt_parser::material::Material::Cable(mat)=>VMTContent::vtf(Some(mat.base_texture)),
vmt_parser::material::Material::Refract(mat)=>VMTContent::vtf(mat.base_texture),
vmt_parser::material::Material::Modulate(mat)=>VMTContent::vtf(Some(mat.base_texture)),
vmt_parser::material::Material::DecalModulate(mat)=>VMTContent::vtf(Some(mat.base_texture)),
vmt_parser::material::Material::Sky(mat)=>VMTContent::vtf(Some(mat.base_texture)),
vmt_parser::material::Material::Replacements(_mat)=>VMTContent::Unsupported,
vmt_parser::material::Material::Patch(mat)=>VMTContent::Patch(mat),
_=>unreachable!(),
}
}
#[derive(Debug,thiserror::Error)]
enum GetVMTError{
#[error("Bsp error {0:?}")]
Bsp(#[from]vbsp::BspError),
#[error("Utf8 error {0:?}")]
Utf8(#[from]std::str::Utf8Error),
#[error("Vdf error {0:?}")]
Vdf(#[from]vmt_parser::VdfError),
#[error("Vmt not found")]
NotFound,
}
fn get_vmt(finder:BspFinder,search_name:&str)->Result<vmt_parser::material::Material,GetVMTError>{
let vmt_data=finder.find(search_name)?.ok_or(GetVMTError::NotFound)?;
//decode vmt and then write
let vmt_str=core::str::from_utf8(&vmt_data)?;
let material=vmt_parser::from_str(vmt_str)?;
//println!("vmt material={:?}",material);
Ok(material)
}
#[derive(Debug,thiserror::Error)]
enum LoadVMTError{
#[error("Bsp error {0:?}")]
Bsp(#[from]vbsp::BspError),
#[error("GetVMT error {0:?}")]
GetVMT(#[from]GetVMTError),
#[error("FromUtf8 error {0:?}")]
FromUtf8(#[from]std::string::FromUtf8Error),
#[error("Vdf error {0:?}")]
Vdf(#[from]vmt_parser::VdfError),
#[error("Vmt unsupported")]
Unsupported,
#[error("Vmt unresolved")]
Unresolved,
#[error("Vmt not found")]
NotFound,
}
fn recursive_vmt_loader<'bsp,'vpk,'a>(finder:BspFinder<'bsp,'vpk>,material:vmt_parser::material::Material)->Result<Option<Cow<'a,[u8]>>,LoadVMTError>
where
'bsp:'a,
'vpk:'a,
{
match get_some_texture(material){
VMTContent::VMT(mut s)=>{
s.make_ascii_lowercase();
recursive_vmt_loader(finder,get_vmt(finder,&s)?)
},
VMTContent::VTF(s)=>{
let mut texture_file_name=PathBuf::from("materials");
texture_file_name.push(s);
texture_file_name.set_extension("vtf");
texture_file_name.as_mut_os_str().make_ascii_lowercase();
Ok(finder.find(texture_file_name.to_str().unwrap())?)
},
VMTContent::Patch(mat)=>recursive_vmt_loader(finder,
mat.resolve(|search_name|{
let name_lowercase=search_name.to_lowercase();
match finder.find(&name_lowercase)?{
Some(bytes)=>Ok(String::from_utf8(bytes.into_owned())?),
None=>Err(LoadVMTError::NotFound),
}
})?
),
VMTContent::Unsupported=>Err(LoadVMTError::Unsupported),
VMTContent::Unresolved=>Err(LoadVMTError::Unresolved),
}
}
fn load_texture<'bsp,'vpk,'a>(finder:BspFinder<'bsp,'vpk>,texture_name:&str)->Result<Option<Cow<'a,[u8]>>,LoadVMTError>
where
'bsp:'a,
'vpk:'a,
{
let mut texture_file_name=PathBuf::from("materials");
//lower case
texture_file_name.push(texture_name);
texture_file_name.as_mut_os_str().make_ascii_lowercase();
//remove stem and search for both vtf and vmt files
let stem=texture_file_name.file_stem().unwrap().to_owned();
texture_file_name.pop();
texture_file_name.push(stem);
if let Some(stuff)=finder.find(texture_file_name.to_str().unwrap())?{
return Ok(Some(stuff));
}
// search for both vmt,vtf
let mut texture_file_name_vmt=texture_file_name.clone();
texture_file_name_vmt.set_extension("vmt");
let get_vmt_result=get_vmt(finder,texture_file_name_vmt.to_str().unwrap());
match get_vmt_result{
Ok(material)=>{
let vmt_result=recursive_vmt_loader(finder,material);
match vmt_result{
Ok(Some(stuff))=>return Ok(Some(stuff)),
Ok(None)
|Err(LoadVMTError::NotFound)=>(),
|Err(LoadVMTError::GetVMT(GetVMTError::NotFound))=>(),
Err(e)=>return Err(e),
}
}
|Err(GetVMTError::NotFound)=>(),
Err(e)=>Err(e)?,
}
// try looking for vtf
let mut texture_file_name_vtf=texture_file_name.clone();
texture_file_name_vtf.set_extension("vtf");
let get_vtf_result=get_vmt(finder,texture_file_name_vtf.to_str().unwrap());
match get_vtf_result{
Ok(material)=>{
let vtf_result=recursive_vmt_loader(finder,material);
match vtf_result{
Ok(Some(stuff))=>return Ok(Some(stuff)),
Ok(None)
|Err(LoadVMTError::NotFound)=>(),
|Err(LoadVMTError::GetVMT(GetVMTError::NotFound))=>(),
Err(e)=>return Err(e),
}
}
|Err(GetVMTError::NotFound)=>(),
Err(e)=>Err(e)?,
}
Ok(None)
}
#[derive(Debug,thiserror::Error)]
enum ExtractTextureError{
#[error("Io error {0:?}")]
Io(#[from]std::io::Error),
#[error("Bsp error {0:?}")]
Bsp(#[from]vbsp::BspError),
#[error("MeshLoad error {0:?}")]
MeshLoad(#[from]strafesnet_bsp_loader::loader::MeshError),
#[error("Load VMT error {0:?}")]
LoadVMT(#[from]LoadVMTError),
}
async fn gimme_them_textures(path:&Path,vpk_list:&[strafesnet_bsp_loader::Vpk],send_texture:tokio::sync::mpsc::Sender<(Vec<u8>,String)>)->Result<(),ExtractTextureError>{
let bsp=vbsp::Bsp::read(tokio::fs::read(path).await?.as_ref())?;
let loader_bsp=strafesnet_bsp_loader::Bsp::new(bsp);
let bsp=loader_bsp.as_ref();
let mut texture_deferred_loader=RenderConfigDeferredLoader::new();
for texture in bsp.textures(){
texture_deferred_loader.acquire_render_config_id(Some(Cow::Borrowed(texture.name())));
}
let mut mesh_deferred_loader=MeshDeferredLoader::new();
for name in &bsp.static_props.dict.name{
mesh_deferred_loader.acquire_mesh_id(name.as_str());
}
for raw_ent in &bsp.entities{
let model=match raw_ent.parse(){
Ok(Entity::Cycler(brush))=>brush.model,
Ok(Entity::EnvSprite(brush))=>brush.model,
Ok(Entity::FuncBreakable(brush))=>brush.model,
Ok(Entity::FuncBrush(brush))=>brush.model,
Ok(Entity::FuncButton(brush))=>brush.model,
Ok(Entity::FuncDoor(brush))=>brush.model,
Ok(Entity::FuncDoorRotating(brush))=>brush.model,
Ok(Entity::FuncIllusionary(brush))=>brush.model,
Ok(Entity::FuncMonitor(brush))=>brush.model,
Ok(Entity::FuncMovelinear(brush))=>brush.model,
Ok(Entity::FuncPhysbox(brush))=>brush.model,
Ok(Entity::FuncPhysboxMultiplayer(brush))=>brush.model,
Ok(Entity::FuncRotButton(brush))=>brush.model,
Ok(Entity::FuncRotating(brush))=>brush.model,
Ok(Entity::FuncTracktrain(brush))=>brush.model,
Ok(Entity::FuncTrain(brush))=>brush.model,
Ok(Entity::FuncWall(brush))=>brush.model,
Ok(Entity::FuncWallToggle(brush))=>brush.model,
Ok(Entity::FuncWaterAnalog(brush))=>brush.model,
Ok(Entity::PropDoorRotating(brush))=>brush.model,
Ok(Entity::PropDynamic(brush))=>brush.model,
Ok(Entity::PropDynamicOverride(brush))=>brush.model,
Ok(Entity::PropPhysics(brush))=>brush.model,
Ok(Entity::PropPhysicsMultiplayer(brush))=>brush.model,
Ok(Entity::PropPhysicsOverride(brush))=>brush.model,
Ok(Entity::PropRagdoll(brush))=>brush.model,
Ok(Entity::TriggerGravity(brush))=>brush.model,
Ok(Entity::TriggerHurt(brush))=>brush.model,
Ok(Entity::TriggerLook(brush))=>brush.model,
Ok(Entity::TriggerMultiple(brush))=>brush.model.unwrap_or_default(),
Ok(Entity::TriggerOnce(brush))=>brush.model,
Ok(Entity::TriggerProximity(brush))=>brush.model,
Ok(Entity::TriggerPush(brush))=>brush.model,
Ok(Entity::TriggerSoundscape(brush))=>brush.model,
Ok(Entity::TriggerTeleport(brush))=>brush.model.unwrap_or_default(),
Ok(Entity::TriggerVphysicsMotion(brush))=>brush.model,
Ok(Entity::TriggerWind(brush))=>brush.model,
_=>continue,
};
match model.chars().next(){
Some('*')=>(),
_=>{
mesh_deferred_loader.acquire_mesh_id(model);
},
}
}
let finder=BspFinder{
bsp:&loader_bsp,
vpks:vpk_list
};
let mut mesh_loader=strafesnet_bsp_loader::loader::ModelLoader::new(finder);
// load models and collect requested textures
for model_path in mesh_deferred_loader.into_indices(){
let model:vmdl::Model=match mesh_loader.load(model_path){
Ok(model)=>model,
Err(e)=>{
println!("Model={model_path} Load model error: {e}");
continue;
},
};
for texture in model.textures(){
for search_path in &texture.search_paths{
let mut path=PathBuf::from(search_path.as_str());
path.push(texture.name.as_str());
let path=path.to_str().unwrap().to_owned();
texture_deferred_loader.acquire_render_config_id(Some(Cow::Owned(path)));
}
}
}
for texture_path in texture_deferred_loader.into_indices(){
match load_texture(finder,&texture_path){
Ok(Some(texture))=>send_texture.send(
(texture.into_owned(),texture_path.into_owned())
).await.unwrap(),
Ok(None)=>(),
Err(e)=>println!("Texture={texture_path} Load error: {e}"),
}
}
Ok(())
}
#[derive(Debug,thiserror::Error)]
enum ConvertTextureError{
#[error("Bsp error {0:?}")]
Bsp(#[from]vbsp::BspError),
#[error("Vtf error {0:?}")]
Vtf(#[from]vtf::Error),
#[error("DDS create error {0:?}")]
DDS(#[from]image_dds::CreateDdsError),
#[error("DDS write error {0:?}")]
DDSWrite(#[from]image_dds::ddsfile::Error),
#[error("Io error {0:?}")]
Io(#[from]std::io::Error),
}
async fn convert_texture(texture:Vec<u8>,write_file_name:impl AsRef<Path>)->Result<(),ConvertTextureError>{
let image=vtf::from_bytes(&texture)?.highres_image.decode(0)?.to_rgba8();
let format=if image.width()%4!=0||image.height()%4!=0{
image_dds::ImageFormat::Rgba8UnormSrgb
}else{
image_dds::ImageFormat::BC7RgbaUnormSrgb
};
//this fails if the image dimensions are not a multiple of 4
let dds = image_dds::dds_from_image(
&image,
format,
image_dds::Quality::Slow,
image_dds::Mipmaps::GeneratedAutomatic,
)?;
//write dds
let mut dest=PathBuf::from("textures");
dest.push(write_file_name);
dest.set_extension("dds");
std::fs::create_dir_all(dest.parent().unwrap())?;
let mut writer=std::io::BufWriter::new(std::fs::File::create(dest)?);
dds.write(&mut writer)?;
Ok(())
}
async fn read_vpks(vpk_paths:Vec<PathBuf>,thread_limit:usize)->Vec<strafesnet_bsp_loader::Vpk>{
futures::stream::iter(vpk_paths).map(|vpk_path|async{
// idk why it doesn't want to pass out the errors but this is fatal anyways
tokio::task::spawn_blocking(move||Ok::<_,vpk::Error>(strafesnet_bsp_loader::Vpk::new(vpk::VPK::read(&vpk_path)?))).await.unwrap().unwrap()
})
.buffer_unordered(thread_limit)
.collect().await
}
async fn extract_textures(paths:Vec<PathBuf>,vpk_paths:Vec<PathBuf>)->AResult<()>{
tokio::try_join!(
tokio::fs::create_dir_all("extracted_textures"),
tokio::fs::create_dir_all("textures"),
tokio::fs::create_dir_all("meshes"),
)?;
let thread_limit=std::thread::available_parallelism()?.get();
// load vpk list and leak for static lifetime
let vpk_list:&[strafesnet_bsp_loader::Vpk]=read_vpks(vpk_paths,thread_limit).await.leak();
let (send_texture,mut recv_texture)=tokio::sync::mpsc::channel(thread_limit);
let mut it=paths.into_iter();
let extract_thread=tokio::spawn(async move{
static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
SEM.add_permits(thread_limit);
while let (Ok(permit),Some(path))=(SEM.acquire().await,it.next()){
let send=send_texture.clone();
tokio::spawn(async move{
let result=gimme_them_textures(&path,vpk_list,send).await;
drop(permit);
match result{
Ok(())=>(),
Err(e)=>println!("Map={path:?} Decode error: {e:?}"),
}
});
}
});
// convert images
static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
SEM.add_permits(thread_limit);
while let (Ok(permit),Some((data,dest)))=(SEM.acquire().await,recv_texture.recv().await){
// TODO: dedup dest?
tokio::spawn(async move{
let result=convert_texture(data,dest).await;
drop(permit);
match result{
Ok(())=>(),
Err(e)=>println!("Convert error: {e:?}"),
}
});
}
extract_thread.await?;
_=SEM.acquire_many(thread_limit as u32).await?;
Ok(())
}
fn vpk_contents(vpk_path:PathBuf)->AResult<()>{
let vpk_index=vpk::VPK::read(&vpk_path)?;
for (label,entry) in vpk_index.tree.into_iter(){
println!("vpk label={} entry={:?}",label,entry);
}
Ok(())
}
fn bsp_contents(path:PathBuf)->AResult<()>{
let bsp=vbsp::Bsp::read(std::fs::read(path)?.as_ref())?;
for file_name in bsp.pack.into_zip().into_inner().unwrap().file_names(){
println!("file_name={:?}",file_name);
}
Ok(())
}
#[derive(Debug)]
#[allow(dead_code)]
enum ConvertError{
IO(std::io::Error),
SNFMap(strafesnet_snf::map::Error),
BspRead(strafesnet_bsp_loader::ReadError),
BspLoad(strafesnet_bsp_loader::LoadError),
}
impl std::fmt::Display for ConvertError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for ConvertError{}
async fn convert_to_snf(path:&Path,vpk_list:&[strafesnet_bsp_loader::Vpk],output_folder:PathBuf)->AResult<()>{
let entire_file=tokio::fs::read(path).await?;
let bsp=strafesnet_bsp_loader::read(
std::io::Cursor::new(entire_file)
).map_err(ConvertError::BspRead)?;
let map=bsp.to_snf(LoadFailureMode::DefaultToNone,vpk_list).map_err(ConvertError::BspLoad)?;
let mut dest=output_folder;
dest.push(path.file_stem().unwrap());
dest.set_extension("snfm");
let file=std::fs::File::create(dest).map_err(ConvertError::IO)?;
strafesnet_snf::map::write_map(file,map).map_err(ConvertError::SNFMap)?;
Ok(())
}
async fn source_to_snf(paths:Vec<std::path::PathBuf>,output_folder:PathBuf,vpk_paths:Vec<PathBuf>)->AResult<()>{
let start=std::time::Instant::now();
let thread_limit=std::thread::available_parallelism()?.get();
// load vpk list and leak for static lifetime
let vpk_list:&[strafesnet_bsp_loader::Vpk]=read_vpks(vpk_paths,thread_limit).await.leak();
let mut it=paths.into_iter();
static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
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(),vpk_list,output_folder).await;
drop(permit);
match result{
Ok(())=>(),
Err(e)=>println!("Convert error: {e:?}"),
}
});
}
_=SEM.acquire_many(thread_limit as u32).await.unwrap();
println!("elapsed={:?}", start.elapsed());
Ok(())
}

@ -1,7 +1,7 @@
[package] [package]
name = "strafe-client" name = "strafe-client"
version = "0.11.0" version = "0.10.5"
edition = "2024" edition = "2021"
repository = "https://git.itzana.me/StrafesNET/strafe-project" repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "Custom" license = "Custom"
description = "StrafesNET game client for bhop and surf." description = "StrafesNET game client for bhop and surf."
@ -9,24 +9,25 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features] [features]
user-install=[] # as opposed to portable install
default = ["snf"] default = ["snf"]
snf = ["dep:strafesnet_snf"] snf = ["dep:strafesnet_snf"]
source = ["dep:strafesnet_deferred_loader", "dep:strafesnet_bsp_loader"] source = ["dep:strafesnet_deferred_loader", "dep:strafesnet_bsp_loader"]
roblox = ["dep:strafesnet_deferred_loader", "dep:strafesnet_rbx_loader"] roblox = ["dep:strafesnet_deferred_loader", "dep:strafesnet_rbx_loader"]
[dependencies] [dependencies]
glam = "0.30.0" arrayvec = "0.7.6"
bytemuck = { version = "1.13.1", features = ["derive"] }
configparser = "3.0.2"
ddsfile = "0.5.1"
glam = "0.29.0"
id = { version = "0.1.0", registry = "strafesnet" }
parking_lot = "0.12.1" parking_lot = "0.12.1"
pollster = "0.4.0" pollster = "0.4.0"
replace_with = "0.1.7"
strafesnet_bsp_loader = { path = "../lib/bsp_loader", registry = "strafesnet", optional = true } strafesnet_bsp_loader = { path = "../lib/bsp_loader", registry = "strafesnet", optional = true }
strafesnet_common = { path = "../lib/common", registry = "strafesnet" } strafesnet_common = { path = "../lib/common", registry = "strafesnet" }
strafesnet_deferred_loader = { path = "../lib/deferred_loader", registry = "strafesnet", optional = true } strafesnet_deferred_loader = { path = "../lib/deferred_loader", features = ["legacy"], registry = "strafesnet", optional = true }
strafesnet_graphics = { path = "../engine/graphics", registry = "strafesnet" }
strafesnet_physics = { path = "../engine/physics", registry = "strafesnet" }
strafesnet_rbx_loader = { path = "../lib/rbx_loader", registry = "strafesnet", optional = true } strafesnet_rbx_loader = { path = "../lib/rbx_loader", registry = "strafesnet", optional = true }
strafesnet_session = { path = "../engine/session", registry = "strafesnet" }
strafesnet_settings = { path = "../engine/settings", registry = "strafesnet" }
strafesnet_snf = { path = "../lib/snf", registry = "strafesnet", optional = true } strafesnet_snf = { path = "../lib/snf", registry = "strafesnet", optional = true }
wgpu = "24.0.0" wgpu = "23.0.1"
winit = "0.30.7" winit = "0.30.7"

@ -1,68 +0,0 @@
use crate::window::Instruction;
use strafesnet_common::integer;
use strafesnet_common::instruction::TimedInstruction;
use strafesnet_common::session::Time as SessionTime;
pub struct App<'a>{
root_time:std::time::Instant,
window_thread:crate::compat_worker::QNWorker<'a,TimedInstruction<Instruction,SessionTime>>,
}
impl<'a> App<'a>{
pub fn new(
root_time:std::time::Instant,
window_thread:crate::compat_worker::QNWorker<'a,TimedInstruction<Instruction,SessionTime>>,
)->App<'a>{
Self{
root_time,
window_thread,
}
}
fn send_timed_instruction(&mut self,instruction:Instruction){
let time=integer::Time::from_nanos(self.root_time.elapsed().as_nanos() as i64);
self.window_thread.send(TimedInstruction{time,instruction}).unwrap();
}
}
impl winit::application::ApplicationHandler for App<'_>{
fn resumed(&mut self,_event_loop:&winit::event_loop::ActiveEventLoop){
//
}
fn window_event(
&mut self,
event_loop:&winit::event_loop::ActiveEventLoop,
_window_id:winit::window::WindowId,
event:winit::event::WindowEvent,
){
match event{
winit::event::WindowEvent::KeyboardInput{
event:winit::event::KeyEvent{
logical_key:winit::keyboard::Key::Named(winit::keyboard::NamedKey::Escape),
state:winit::event::ElementState::Pressed,
..
},
..
}
|winit::event::WindowEvent::CloseRequested=>{
event_loop.exit();
},
_=>(),
}
self.send_timed_instruction(Instruction::WindowEvent(event));
}
fn device_event(
&mut self,
_event_loop:&winit::event_loop::ActiveEventLoop,
_device_id:winit::event::DeviceId,
event:winit::event::DeviceEvent,
){
self.send_timed_instruction(Instruction::DeviceEvent(event));
}
fn about_to_wait(
&mut self,
_event_loop:&winit::event_loop::ActiveEventLoop
){
self.send_timed_instruction(Instruction::WindowEvent(winit::event::WindowEvent::RedrawRequested));
}
}

@ -31,14 +31,6 @@ impl<T> Body<T>
time, time,
} }
} }
pub const fn relative_to<'a>(&'a self,body0:&'a Body<T>)->VirtualBody<'a,T>{
//(p0,v0,a0,t0)
//(p1,v1,a1,t1)
VirtualBody{
body0,
body1:self,
}
}
pub fn extrapolated_position(&self,time:Time<T>)->Planar64Vec3{ pub fn extrapolated_position(&self,time:Time<T>)->Planar64Vec3{
let dt=time-self.time; let dt=time-self.time;
self.position self.position
@ -90,7 +82,7 @@ impl<T> Body<T>
// a*dt + v // a*dt + v
self.acceleration.map(|elem|(dt*elem).divide().fix())+self.velocity self.acceleration.map(|elem|(dt*elem).divide().fix())+self.velocity
} }
pub fn advance_time_ratio_dt(&mut self,dt:crate::model::GigaTime){ pub fn advance_time_ratio_dt(&mut self,dt:crate::model_physics::GigaTime){
self.position=self.extrapolated_position_ratio_dt(dt); self.position=self.extrapolated_position_ratio_dt(dt);
self.velocity=self.extrapolated_velocity_ratio_dt(dt); self.velocity=self.extrapolated_velocity_ratio_dt(dt);
self.time+=dt.into(); self.time+=dt.into();
@ -145,6 +137,14 @@ pub struct VirtualBody<'a,T>{
impl<T> VirtualBody<'_,T> impl<T> VirtualBody<'_,T>
where Time<T>:Copy, where Time<T>:Copy,
{ {
pub const fn relative<'a>(body0:&'a Body<T>,body1:&'a Body<T>)->VirtualBody<'a,T>{
//(p0,v0,a0,t0)
//(p1,v1,a1,t1)
VirtualBody{
body0,
body1,
}
}
pub fn extrapolated_position(&self,time:Time<T>)->Planar64Vec3{ pub fn extrapolated_position(&self,time:Time<T>)->Planar64Vec3{
self.body1.extrapolated_position(time)-self.body0.extrapolated_position(time) self.body1.extrapolated_position(time)-self.body0.extrapolated_position(time)
} }

@ -1,4 +1,4 @@
use crate::model::{GigaTime,FEV,MeshQuery,DirectedEdge}; use crate::model_physics::{GigaTime,FEV,MeshQuery,DirectedEdge};
use strafesnet_common::integer::{Fixed,Ratio,vec3::Vector3}; use strafesnet_common::integer::{Fixed,Ratio,vec3::Vector3};
use crate::physics::{Time,Body}; use crate::physics::{Time,Body};
@ -12,20 +12,6 @@ pub enum CrawlResult<M:MeshQuery>{
Miss(FEV<M>), Miss(FEV<M>),
Hit(M::Face,GigaTime), Hit(M::Face,GigaTime),
} }
impl<M:MeshQuery> CrawlResult<M>{
pub fn hit(self)->Option<(M::Face,GigaTime)>{
match self{
CrawlResult::Miss(_)=>None,
CrawlResult::Hit(face,time)=>Some((face,time)),
}
}
pub fn miss(self)->Option<FEV<M>>{
match self{
CrawlResult::Miss(fev)=>Some(fev),
CrawlResult::Hit(_,_)=>None,
}
}
}
impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M> impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
where where

@ -1,9 +1,5 @@
use std::io::Read; use std::io::Read;
#[cfg(any(feature="roblox",feature="source"))]
use strafesnet_deferred_loader::deferred_loader::LoadFailureMode;
#[allow(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub enum ReadError{ pub enum ReadError{
#[cfg(feature="roblox")] #[cfg(feature="roblox")]
@ -14,8 +10,6 @@ pub enum ReadError{
StrafesNET(strafesnet_snf::Error), StrafesNET(strafesnet_snf::Error),
#[cfg(feature="snf")] #[cfg(feature="snf")]
StrafesNETMap(strafesnet_snf::map::Error), StrafesNETMap(strafesnet_snf::map::Error),
#[cfg(feature="snf")]
StrafesNETBot(strafesnet_snf::bot::Error),
Io(std::io::Error), Io(std::io::Error),
UnknownFileFormat, UnknownFileFormat,
} }
@ -26,52 +20,37 @@ impl std::fmt::Display for ReadError{
} }
impl std::error::Error for ReadError{} impl std::error::Error for ReadError{}
pub enum ReadFormat{ pub enum DataStructure{
#[cfg(feature="roblox")] #[cfg(feature="roblox")]
Roblox(strafesnet_rbx_loader::Model), Roblox(strafesnet_rbx_loader::Model),
#[cfg(feature="source")] #[cfg(feature="source")]
Source(strafesnet_bsp_loader::Bsp), Source(strafesnet_bsp_loader::Bsp),
#[cfg(feature="snf")] #[cfg(feature="snf")]
SNFM(strafesnet_common::map::CompleteMap), StrafesNET(strafesnet_common::map::CompleteMap),
#[cfg(feature="snf")]
SNFB(strafesnet_snf::bot::Segment),
} }
pub fn read<R:Read+std::io::Seek>(input:R)->Result<ReadFormat,ReadError>{ pub fn read<R:Read+std::io::Seek>(input:R)->Result<DataStructure,ReadError>{
let mut buf=std::io::BufReader::new(input); let mut buf=std::io::BufReader::new(input);
let peek=std::io::BufRead::fill_buf(&mut buf).map_err(ReadError::Io)?[0..4].to_owned(); let peek=std::io::BufRead::fill_buf(&mut buf).map_err(ReadError::Io)?;
// reading the entire file is way faster than round tripping the disk constantly match &peek[0..4]{
let mut entire_file=Vec::new();
buf.read_to_end(&mut entire_file).map_err(ReadError::Io)?;
let cursor=std::io::Cursor::new(entire_file);
match peek.as_slice(){
#[cfg(feature="roblox")] #[cfg(feature="roblox")]
b"<rob"=>Ok(ReadFormat::Roblox(strafesnet_rbx_loader::read(cursor).map_err(ReadError::Roblox)?)), b"<rob"=>Ok(DataStructure::Roblox(strafesnet_rbx_loader::read(buf).map_err(ReadError::Roblox)?)),
#[cfg(feature="source")] #[cfg(feature="source")]
b"VBSP"=>Ok(ReadFormat::Source(strafesnet_bsp_loader::read(cursor).map_err(ReadError::Source)?)), b"VBSP"=>Ok(DataStructure::Source(strafesnet_bsp_loader::read(buf).map_err(ReadError::Source)?)),
#[cfg(feature="snf")] #[cfg(feature="snf")]
b"SNFM"=>Ok(ReadFormat::SNFM( b"SNFM"=>Ok(DataStructure::StrafesNET(
strafesnet_snf::read_map(cursor).map_err(ReadError::StrafesNET)? strafesnet_snf::read_map(buf).map_err(ReadError::StrafesNET)?
.into_complete_map().map_err(ReadError::StrafesNETMap)? .into_complete_map().map_err(ReadError::StrafesNETMap)?
)), )),
#[cfg(feature="snf")]
b"SNFB"=>Ok(ReadFormat::SNFB(
strafesnet_snf::read_bot(cursor).map_err(ReadError::StrafesNET)?
.read_all().map_err(ReadError::StrafesNETBot)?
)),
_=>Err(ReadError::UnknownFileFormat), _=>Err(ReadError::UnknownFileFormat),
} }
} }
#[allow(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub enum LoadError{ pub enum LoadError{
ReadError(ReadError), ReadError(ReadError),
File(std::io::Error), File(std::io::Error),
#[cfg(feature="roblox")] Io(std::io::Error),
LoadRoblox(strafesnet_rbx_loader::LoadError),
#[cfg(feature="source")]
LoadSource(strafesnet_bsp_loader::LoadError),
} }
impl std::fmt::Display for LoadError{ impl std::fmt::Display for LoadError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
@ -80,32 +59,86 @@ impl std::fmt::Display for LoadError{
} }
impl std::error::Error for LoadError{} impl std::error::Error for LoadError{}
pub enum LoadFormat{ pub fn load<P:AsRef<std::path::Path>>(path:P)->Result<strafesnet_common::map::CompleteMap,LoadError>{
#[cfg(any(feature="snf",feature="roblox",feature="source"))]
Map(strafesnet_common::map::CompleteMap),
#[cfg(feature="snf")]
Bot(strafesnet_snf::bot::Segment),
}
pub fn load<P:AsRef<std::path::Path>>(path:P)->Result<LoadFormat,LoadError>{
//blocking because it's simpler... //blocking because it's simpler...
let file=std::fs::File::open(path).map_err(LoadError::File)?; let file=std::fs::File::open(path).map_err(LoadError::File)?;
match read(file).map_err(LoadError::ReadError)?{ match read(file).map_err(LoadError::ReadError)?{
#[cfg(feature="snf")] #[cfg(feature="snf")]
ReadFormat::SNFB(bot)=>Ok(LoadFormat::Bot(bot)), DataStructure::StrafesNET(map)=>Ok(map),
#[cfg(feature="snf")]
ReadFormat::SNFM(map)=>Ok(LoadFormat::Map(map)),
#[cfg(feature="roblox")] #[cfg(feature="roblox")]
ReadFormat::Roblox(model)=>{ DataStructure::Roblox(model)=>{
let mut place=model.into_place(); let mut place=model.into_place();
place.run_scripts(); place.run_scripts();
Ok(LoadFormat::Map(
place.to_snf(LoadFailureMode::DefaultToNone).map_err(LoadError::LoadRoblox)? let mut loader=strafesnet_deferred_loader::roblox_legacy();
))
let (texture_loader,mesh_loader)=loader.get_inner_mut();
let map_step1=strafesnet_rbx_loader::convert(
&place,
|name|texture_loader.acquire_render_config_id(name),
|name|mesh_loader.acquire_mesh_id(name),
);
let meshpart_meshes=mesh_loader.load_meshes().map_err(LoadError::Io)?;
let map_step2=map_step1.add_meshpart_meshes_and_calculate_attributes(
meshpart_meshes.into_iter().map(|(mesh_id,loader_model)|
(mesh_id,strafesnet_rbx_loader::data::RobloxMeshBytes::new(loader_model.get()))
)
);
let (textures,render_configs)=loader.into_render_configs().map_err(LoadError::Io)?.consume();
let map=map_step2.add_render_configs_and_textures(
render_configs.into_iter(),
textures.into_iter().map(|(texture_id,texture)|
(texture_id,match texture{
strafesnet_deferred_loader::texture::Texture::ImageDDS(data)=>data,
})
)
);
Ok(map)
}, },
#[cfg(feature="source")] #[cfg(feature="source")]
ReadFormat::Source(bsp)=>Ok(LoadFormat::Map( DataStructure::Source(bsp)=>{
bsp.to_snf(LoadFailureMode::DefaultToNone,&[]).map_err(LoadError::LoadSource)? let mut loader=strafesnet_deferred_loader::source_legacy();
)),
let (texture_loader,mesh_loader)=loader.get_inner_mut();
let map_step1=strafesnet_bsp_loader::convert(
&bsp,
|name|texture_loader.acquire_render_config_id(name),
|name|mesh_loader.acquire_mesh_id(name),
);
let prop_meshes=mesh_loader.load_meshes(bsp.as_ref());
let map_step2=map_step1.add_prop_meshes(
//the type conflagulator 9000
prop_meshes.into_iter().map(|(mesh_id,loader_model)|
(mesh_id,strafesnet_bsp_loader::data::ModelData{
mdl:strafesnet_bsp_loader::data::MdlData::new(loader_model.mdl.get()),
vtx:strafesnet_bsp_loader::data::VtxData::new(loader_model.vtx.get()),
vvd:strafesnet_bsp_loader::data::VvdData::new(loader_model.vvd.get()),
})
),
|name|texture_loader.acquire_render_config_id(name),
);
let (textures,render_configs)=loader.into_render_configs().map_err(LoadError::Io)?.consume();
let map=map_step2.add_render_configs_and_textures(
render_configs.into_iter(),
textures.into_iter().map(|(texture_id,texture)|
(texture_id,match texture{
strafesnet_deferred_loader::texture::Texture::ImageDDS(data)=>data,
})
),
);
Ok(map)
},
} }
} }

@ -1,15 +1,9 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::{HashSet,HashMap}; use std::collections::{HashSet,HashMap};
use strafesnet_common::map; use strafesnet_common::map;
use strafesnet_settings::settings;
use strafesnet_session::session;
use strafesnet_common::model::{self, ColorId, NormalId, PolygonIter, PositionId, RenderConfigId, TextureCoordinateId, VertexId}; use strafesnet_common::model::{self, ColorId, NormalId, PolygonIter, PositionId, RenderConfigId, TextureCoordinateId, VertexId};
use wgpu::{util::DeviceExt,AstcBlock,AstcChannel}; use wgpu::{util::DeviceExt,AstcBlock,AstcChannel};
use crate::model::{self as model_graphics,IndexedGraphicsMeshOwnedRenderConfig,IndexedGraphicsMeshOwnedRenderConfigId,GraphicsMeshOwnedRenderConfig,GraphicsModelColor4,GraphicsModelOwned,GraphicsVertex}; use crate::model_graphics::{self,IndexedGraphicsMeshOwnedRenderConfig,IndexedGraphicsMeshOwnedRenderConfigId,GraphicsMeshOwnedRenderConfig,GraphicsModelColor4,GraphicsModelOwned,GraphicsVertex};
pub fn required_limits()->wgpu::Limits{
wgpu::Limits::default()
}
struct Indices{ struct Indices{
count:u32, count:u32,
@ -142,7 +136,7 @@ impl GraphicsState{
pub fn clear(&mut self){ pub fn clear(&mut self){
self.models.clear(); self.models.clear();
} }
pub fn load_user_settings(&mut self,user_settings:&settings::UserSettings){ pub fn load_user_settings(&mut self,user_settings:&crate::settings::UserSettings){
self.camera.fov=user_settings.calculate_fov(1.0,&self.camera.screen_size).as_vec2(); self.camera.fov=user_settings.calculate_fov(1.0,&self.camera.screen_size).as_vec2();
} }
pub fn generate_models(&mut self,device:&wgpu::Device,queue:&wgpu::Queue,map:&map::CompleteMap){ pub fn generate_models(&mut self,device:&wgpu::Device,queue:&wgpu::Queue,map:&map::CompleteMap){
@ -454,7 +448,7 @@ impl GraphicsState{
//.into_iter() the modeldata vec so entities can be /moved/ to models.entities //.into_iter() the modeldata vec so entities can be /moved/ to models.entities
let mut model_count=0; let mut model_count=0;
let mut instance_count=0; let mut instance_count=0;
let uniform_buffer_binding_size=required_limits().max_uniform_buffer_binding_size as usize; let uniform_buffer_binding_size=crate::setup::required_limits().max_uniform_buffer_binding_size as usize;
let chunk_size=uniform_buffer_binding_size/MODEL_BUFFER_SIZE_BYTES; let chunk_size=uniform_buffer_binding_size/MODEL_BUFFER_SIZE_BYTES;
self.models.reserve(models.len()); self.models.reserve(models.len());
for model in models.into_iter(){ for model in models.into_iter(){
@ -614,7 +608,7 @@ impl GraphicsState{
// Create the render pipeline // Create the render pipeline
let shader=device.create_shader_module(wgpu::ShaderModuleDescriptor{ let shader=device.create_shader_module(wgpu::ShaderModuleDescriptor{
label:None, label:None,
source:wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("../../../strafe-client/src/shader.wgsl"))), source:wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shader.wgsl"))),
}); });
//load textures //load textures
@ -642,10 +636,10 @@ impl GraphicsState{
wgpu::TextureFormat::Astc{ wgpu::TextureFormat::Astc{
block:AstcBlock::B4x4, block:AstcBlock::B4x4,
channel:AstcChannel::UnormSrgb, channel:AstcChannel::UnormSrgb,
}=>&include_bytes!("../../../strafe-client/images/astc.dds")[..], }=>&include_bytes!("../images/astc.dds")[..],
wgpu::TextureFormat::Etc2Rgb8UnormSrgb=>&include_bytes!("../../../strafe-client/images/etc2.dds")[..], wgpu::TextureFormat::Etc2Rgb8UnormSrgb=>&include_bytes!("../images/etc2.dds")[..],
wgpu::TextureFormat::Bc1RgbaUnormSrgb=>&include_bytes!("../../../strafe-client/images/bc1.dds")[..], wgpu::TextureFormat::Bc1RgbaUnormSrgb=>&include_bytes!("../images/bc1.dds")[..],
wgpu::TextureFormat::Bgra8UnormSrgb=>&include_bytes!("../../../strafe-client/images/bgra.dds")[..], wgpu::TextureFormat::Bgra8UnormSrgb=>&include_bytes!("../images/bgra.dds")[..],
_=>unreachable!(), _=>unreachable!(),
}; };
@ -688,7 +682,7 @@ impl GraphicsState{
//squid //squid
let squid_texture_view={ let squid_texture_view={
let bytes=include_bytes!("../../../strafe-client/images/squid.dds"); let bytes=include_bytes!("../images/squid.dds");
let image=ddsfile::Dds::read(&mut std::io::Cursor::new(bytes)).unwrap(); let image=ddsfile::Dds::read(&mut std::io::Cursor::new(bytes)).unwrap();
@ -870,7 +864,7 @@ impl GraphicsState{
&mut self, &mut self,
device:&wgpu::Device, device:&wgpu::Device,
config:&wgpu::SurfaceConfiguration, config:&wgpu::SurfaceConfiguration,
user_settings:&settings::UserSettings, user_settings:&crate::settings::UserSettings,
){ ){
self.depth_view=Self::create_depth_texture(config,device); self.depth_view=Self::create_depth_texture(config,device);
self.camera.screen_size=glam::uvec2(config.width,config.height); self.camera.screen_size=glam::uvec2(config.width,config.height);
@ -881,7 +875,7 @@ impl GraphicsState{
view:&wgpu::TextureView, view:&wgpu::TextureView,
device:&wgpu::Device, device:&wgpu::Device,
queue:&wgpu::Queue, queue:&wgpu::Queue,
frame_state:session::FrameState, frame_state:crate::session::FrameState,
){ ){
//TODO:use scheduled frame times to create beautiful smoothing simulation physics extrapolation assuming no input //TODO:use scheduled frame times to create beautiful smoothing simulation physics extrapolation assuming no input

@ -1,11 +1,7 @@
use strafesnet_graphics::graphics;
use strafesnet_session::session;
use strafesnet_settings::settings;
pub enum Instruction{ pub enum Instruction{
Render(session::FrameState), Render(crate::session::FrameState),
//UpdateModel(graphics::GraphicsModelUpdate), //UpdateModel(crate::graphics::GraphicsModelUpdate),
Resize(winit::dpi::PhysicalSize<u32>,settings::UserSettings), Resize(winit::dpi::PhysicalSize<u32>,crate::settings::UserSettings),
ChangeMap(strafesnet_common::map::CompleteMap), ChangeMap(strafesnet_common::map::CompleteMap),
} }
@ -19,7 +15,7 @@ WorkerDescription{
//up to three frames in flight, dropping new frame requests when all three are busy, and dropping output frames when one renders out of order //up to three frames in flight, dropping new frame requests when all three are busy, and dropping output frames when one renders out of order
pub fn new( pub fn new(
mut graphics:graphics::GraphicsState, mut graphics:crate::graphics::GraphicsState,
mut config:wgpu::SurfaceConfiguration, mut config:wgpu::SurfaceConfiguration,
surface:wgpu::Surface, surface:wgpu::Surface,
device:wgpu::Device, device:wgpu::Device,

@ -1,10 +1,20 @@
mod app; mod body;
mod file; mod file;
mod setup; mod setup;
mod window; mod window;
mod worker;
mod physics;
mod session;
mod graphics;
mod settings;
mod push_solve;
mod face_crawler;
mod compat_worker; mod compat_worker;
mod model_physics;
mod model_graphics;
mod physics_worker; mod physics_worker;
mod graphics_worker; mod graphics_worker;
mod mouse_interpolator;
const TITLE:&'static str=concat!("Strafe Client v",env!("CARGO_PKG_VERSION")); const TITLE:&'static str=concat!("Strafe Client v",env!("CARGO_PKG_VERSION"));

@ -1,6 +1,5 @@
use std::borrow::{Borrow,Cow}; use std::borrow::{Borrow,Cow};
use std::collections::{HashSet,HashMap}; use std::collections::{HashSet,HashMap};
use core::ops::Range;
use strafesnet_common::integer::vec3::Vector3; use strafesnet_common::integer::vec3::Vector3;
use strafesnet_common::model::{self,MeshId,PolygonIter}; use strafesnet_common::model::{self,MeshId,PolygonIter};
use strafesnet_common::integer::{self,vec3,Fixed,Planar64,Planar64Vec3,Ratio}; use strafesnet_common::integer::{self,vec3,Fixed,Planar64,Planar64Vec3,Ratio};
@ -67,6 +66,7 @@ struct Face{
normal:Planar64Vec3, normal:Planar64Vec3,
dot:Planar64, dot:Planar64,
} }
#[derive(Clone)]
struct Vert(Planar64Vec3); struct Vert(Planar64Vec3);
pub trait MeshQuery{ pub trait MeshQuery{
type Face:Clone; type Face:Clone;
@ -91,35 +91,39 @@ pub trait MeshQuery{
fn vert_edges(&self,vert_id:Self::Vert)->Cow<[Self::Edge]>; fn vert_edges(&self,vert_id:Self::Vert)->Cow<[Self::Edge]>;
fn vert_faces(&self,vert_id:Self::Vert)->Cow<[Self::Face]>; fn vert_faces(&self,vert_id:Self::Vert)->Cow<[Self::Face]>;
} }
#[derive(Clone)]
struct FaceRefs{ struct FaceRefs{
edges:Vec<SubmeshDirectedEdgeId>, edges:Cow<'static,[SubmeshDirectedEdgeId]>,
//verts:Vec<VertId>, //verts:Cow<'static,[VertId]>,
} }
#[derive(Clone)]
struct EdgeRefs{ struct EdgeRefs{
faces:[SubmeshFaceId;2],//left, right faces:[SubmeshFaceId;2],//left, right
verts:[SubmeshVertId;2],//bottom, top verts:[SubmeshVertId;2],//bottom, top
} }
#[derive(Clone)]
struct VertRefs{ struct VertRefs{
faces:Vec<SubmeshFaceId>, faces:Cow<'static,[SubmeshFaceId]>,
edges:Vec<SubmeshDirectedEdgeId>, edges:Cow<'static,[SubmeshDirectedEdgeId]>,
} }
// I don't want to use cow in the mesh, I'd rather have another type and monomorphize using the MeshQuery trait
pub struct PhysicsMeshData{ pub struct PhysicsMeshData{
//this contains all real and virtual faces used in both the complete mesh and convex submeshes //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 //faces are sorted such that all faces that belong to the complete mesh appear first, and then
//all remaining faces are virtual to operate internal logic of the face crawler //all remaining faces are virtual to operate internal logic of the face crawler
//and cannot be part of a physics collision //and cannot be part of a physics collision
//virtual faces are only used in convex submeshes. //virtual faces are only used in convex submeshes.
faces:Vec<Face>,//MeshFaceId indexes this list faces:Cow<'static,[Face]>,//MeshFaceId indexes this list
verts:Vec<Vert>,//MeshVertId indexes this list verts:Cow<'static,[Vert]>,//MeshVertId indexes this list
} }
pub struct PhysicsMeshTopology{ pub struct PhysicsMeshTopology{
//mapping of local ids to PhysicsMeshData ids //mapping of local ids to PhysicsMeshData ids
faces:Vec<MeshFaceId>,//SubmeshFaceId indexes this list faces:Cow<'static,[MeshFaceId]>,//SubmeshFaceId indexes this list
verts:Vec<MeshVertId>,//SubmeshVertId indexes this list verts:Cow<'static,[MeshVertId]>,//SubmeshVertId indexes this list
//all ids here are local to this object //all ids here are local to this object
face_topology:Vec<FaceRefs>, face_topology:Cow<'static,[FaceRefs]>,
edge_topology:Vec<EdgeRefs>, edge_topology:Cow<'static,[EdgeRefs]>,
vert_topology:Vec<VertRefs>, vert_topology:Cow<'static,[VertRefs]>,
} }
#[derive(Clone,Copy,Hash,id::Id,Eq,PartialEq)] #[derive(Clone,Copy,Hash,id::Id,Eq,PartialEq)]
pub struct PhysicsMeshId(u32); pub struct PhysicsMeshId(u32);
@ -145,39 +149,41 @@ pub struct PhysicsMesh{
} }
impl PhysicsMesh{ impl PhysicsMesh{
pub fn unit_cube()->Self{ pub fn unit_cube()->Self{
const MESH_DATA_FACES:[Face;6]=[
Face{normal:vec3::raw_xyz( 4294967296, 0, 0),dot:Planar64::raw(4294967296)},
Face{normal:vec3::raw_xyz( 0, 4294967296, 0),dot:Planar64::raw(4294967296)},
Face{normal:vec3::raw_xyz( 0, 0, 4294967296),dot:Planar64::raw(4294967296)},
Face{normal:vec3::raw_xyz(-4294967296, 0, 0),dot:Planar64::raw(4294967296)},
Face{normal:vec3::raw_xyz( 0,-4294967296, 0),dot:Planar64::raw(4294967296)},
Face{normal:vec3::raw_xyz( 0, 0,-4294967296),dot:Planar64::raw(4294967296)}
];
const MESH_DATA_VERTS:[Vert;8]=[
Vert(vec3::raw_xyz( 4294967296,-4294967296,-4294967296)),
Vert(vec3::raw_xyz( 4294967296, 4294967296,-4294967296)),
Vert(vec3::raw_xyz( 4294967296, 4294967296, 4294967296)),
Vert(vec3::raw_xyz( 4294967296,-4294967296, 4294967296)),
Vert(vec3::raw_xyz(-4294967296, 4294967296,-4294967296)),
Vert(vec3::raw_xyz(-4294967296, 4294967296, 4294967296)),
Vert(vec3::raw_xyz(-4294967296,-4294967296, 4294967296)),
Vert(vec3::raw_xyz(-4294967296,-4294967296,-4294967296))
];
//go go gadget debug print mesh //go go gadget debug print mesh
let data=PhysicsMeshData{ let data=PhysicsMeshData{
faces:vec![ faces:Cow::Borrowed(&MESH_DATA_FACES),
Face{normal:vec3::raw_xyz( 4294967296, 0, 0),dot:Planar64::raw(4294967296)}, verts:Cow::Borrowed(&MESH_DATA_VERTS)
Face{normal:vec3::raw_xyz( 0, 4294967296, 0),dot:Planar64::raw(4294967296)},
Face{normal:vec3::raw_xyz( 0, 0, 4294967296),dot:Planar64::raw(4294967296)},
Face{normal:vec3::raw_xyz(-4294967296, 0, 0),dot:Planar64::raw(4294967296)},
Face{normal:vec3::raw_xyz( 0,-4294967296, 0),dot:Planar64::raw(4294967296)},
Face{normal:vec3::raw_xyz( 0, 0,-4294967296),dot:Planar64::raw(4294967296)}
],
verts:vec![
Vert(vec3::raw_xyz( 4294967296,-4294967296,-4294967296)),
Vert(vec3::raw_xyz( 4294967296, 4294967296,-4294967296)),
Vert(vec3::raw_xyz( 4294967296, 4294967296, 4294967296)),
Vert(vec3::raw_xyz( 4294967296,-4294967296, 4294967296)),
Vert(vec3::raw_xyz(-4294967296, 4294967296,-4294967296)),
Vert(vec3::raw_xyz(-4294967296, 4294967296, 4294967296)),
Vert(vec3::raw_xyz(-4294967296,-4294967296, 4294967296)),
Vert(vec3::raw_xyz(-4294967296,-4294967296,-4294967296))
]
}; };
let mesh_topology=PhysicsMeshTopology{ let mesh_topology=PhysicsMeshTopology{
faces:(0..data.faces.len() as u32).map(MeshFaceId::new).collect(), faces:(0..data.faces.len() as u32).map(MeshFaceId::new).collect(),
verts:(0..data.verts.len() as u32).map(MeshVertId::new).collect(), verts:(0..data.verts.len() as u32).map(MeshVertId::new).collect(),
face_topology:vec![ face_topology:Cow::Borrowed(&[
FaceRefs{edges:vec![SubmeshDirectedEdgeId((9223372036854775808u64-(1<<63)+(1<<31)) as u32),SubmeshDirectedEdgeId((9223372036854775809u64-(1<<63)+(1<<31)) as u32),SubmeshDirectedEdgeId((9223372036854775810u64-(1<<63)+(1<<31)) as u32),SubmeshDirectedEdgeId(3)]}, FaceRefs{edges:Cow::Borrowed(&[SubmeshDirectedEdgeId((9223372036854775808u64-(1<<63)+(1<<31)) as u32),SubmeshDirectedEdgeId((9223372036854775809u64-(1<<63)+(1<<31)) as u32),SubmeshDirectedEdgeId((9223372036854775810u64-(1<<63)+(1<<31)) as u32),SubmeshDirectedEdgeId(3)])},
FaceRefs{edges:vec![SubmeshDirectedEdgeId((9223372036854775812u64-(1<<63)+(1<<31)) as u32),SubmeshDirectedEdgeId((9223372036854775813u64-(1<<63)+(1<<31)) as u32),SubmeshDirectedEdgeId(6),SubmeshDirectedEdgeId(1)]}, FaceRefs{edges:Cow::Borrowed(&[SubmeshDirectedEdgeId((9223372036854775812u64-(1<<63)+(1<<31)) as u32),SubmeshDirectedEdgeId((9223372036854775813u64-(1<<63)+(1<<31)) as u32),SubmeshDirectedEdgeId(6),SubmeshDirectedEdgeId(1)])},
FaceRefs{edges:vec![SubmeshDirectedEdgeId(7),SubmeshDirectedEdgeId(2),SubmeshDirectedEdgeId((9223372036854775814u64-(1<<63)+(1<<31)) as u32),SubmeshDirectedEdgeId((9223372036854775816u64-(1<<63)+(1<<31)) as u32)]}, FaceRefs{edges:Cow::Borrowed(&[SubmeshDirectedEdgeId(7),SubmeshDirectedEdgeId(2),SubmeshDirectedEdgeId((9223372036854775814u64-(1<<63)+(1<<31)) as u32),SubmeshDirectedEdgeId((9223372036854775816u64-(1<<63)+(1<<31)) as u32)])},
FaceRefs{edges:vec![SubmeshDirectedEdgeId(8),SubmeshDirectedEdgeId(5),SubmeshDirectedEdgeId((9223372036854775817u64-(1<<63)+(1<<31)) as u32),SubmeshDirectedEdgeId(10)]}, FaceRefs{edges:Cow::Borrowed(&[SubmeshDirectedEdgeId(8),SubmeshDirectedEdgeId(5),SubmeshDirectedEdgeId((9223372036854775817u64-(1<<63)+(1<<31)) as u32),SubmeshDirectedEdgeId(10)])},
FaceRefs{edges:vec![SubmeshDirectedEdgeId((9223372036854775815u64-(1<<63)+(1<<31)) as u32),SubmeshDirectedEdgeId((9223372036854775818u64-(1<<63)+(1<<31)) as u32),SubmeshDirectedEdgeId(11),SubmeshDirectedEdgeId((9223372036854775811u64-(1<<63)+(1<<31)) as u32)]}, FaceRefs{edges:Cow::Borrowed(&[SubmeshDirectedEdgeId((9223372036854775815u64-(1<<63)+(1<<31)) as u32),SubmeshDirectedEdgeId((9223372036854775818u64-(1<<63)+(1<<31)) as u32),SubmeshDirectedEdgeId(11),SubmeshDirectedEdgeId((9223372036854775811u64-(1<<63)+(1<<31)) as u32)])},
FaceRefs{edges:vec![SubmeshDirectedEdgeId(4),SubmeshDirectedEdgeId(0),SubmeshDirectedEdgeId((9223372036854775819u64-(1<<63)+(1<<31)) as u32),SubmeshDirectedEdgeId(9)]} FaceRefs{edges:Cow::Borrowed(&[SubmeshDirectedEdgeId(4),SubmeshDirectedEdgeId(0),SubmeshDirectedEdgeId((9223372036854775819u64-(1<<63)+(1<<31)) as u32),SubmeshDirectedEdgeId(9)])},
], ]),
edge_topology:vec![ edge_topology:Cow::Borrowed(&[
EdgeRefs{faces:[SubmeshFaceId(0),SubmeshFaceId(5)],verts:[SubmeshVertId(0),SubmeshVertId(1)]}, EdgeRefs{faces:[SubmeshFaceId(0),SubmeshFaceId(5)],verts:[SubmeshVertId(0),SubmeshVertId(1)]},
EdgeRefs{faces:[SubmeshFaceId(0),SubmeshFaceId(1)],verts:[SubmeshVertId(1),SubmeshVertId(2)]}, EdgeRefs{faces:[SubmeshFaceId(0),SubmeshFaceId(1)],verts:[SubmeshVertId(1),SubmeshVertId(2)]},
EdgeRefs{faces:[SubmeshFaceId(0),SubmeshFaceId(2)],verts:[SubmeshVertId(2),SubmeshVertId(3)]}, EdgeRefs{faces:[SubmeshFaceId(0),SubmeshFaceId(2)],verts:[SubmeshVertId(2),SubmeshVertId(3)]},
@ -190,17 +196,17 @@ impl PhysicsMesh{
EdgeRefs{faces:[SubmeshFaceId(3),SubmeshFaceId(5)],verts:[SubmeshVertId(4),SubmeshVertId(7)]}, EdgeRefs{faces:[SubmeshFaceId(3),SubmeshFaceId(5)],verts:[SubmeshVertId(4),SubmeshVertId(7)]},
EdgeRefs{faces:[SubmeshFaceId(4),SubmeshFaceId(3)],verts:[SubmeshVertId(6),SubmeshVertId(7)]}, EdgeRefs{faces:[SubmeshFaceId(4),SubmeshFaceId(3)],verts:[SubmeshVertId(6),SubmeshVertId(7)]},
EdgeRefs{faces:[SubmeshFaceId(5),SubmeshFaceId(4)],verts:[SubmeshVertId(0),SubmeshVertId(7)]} EdgeRefs{faces:[SubmeshFaceId(5),SubmeshFaceId(4)],verts:[SubmeshVertId(0),SubmeshVertId(7)]}
], ]),
vert_topology:vec![ vert_topology:Cow::Borrowed(&[
VertRefs{faces:vec![SubmeshFaceId(0),SubmeshFaceId(4),SubmeshFaceId(5)],edges:vec![SubmeshDirectedEdgeId((9223372036854775811u64-(1<<63)+(1<<31)) as u32),SubmeshDirectedEdgeId((9223372036854775819u64-(1<<63)+(1<<31)) as u32),SubmeshDirectedEdgeId((9223372036854775808u64-(1<<63)+(1<<31)) as u32)]}, VertRefs{faces:Cow::Borrowed(&[SubmeshFaceId(0),SubmeshFaceId(4),SubmeshFaceId(5)]),edges:Cow::Borrowed(&[SubmeshDirectedEdgeId((9223372036854775811u64-(1<<63)+(1<<31)) as u32),SubmeshDirectedEdgeId((9223372036854775819u64-(1<<63)+(1<<31)) as u32),SubmeshDirectedEdgeId((9223372036854775808u64-(1<<63)+(1<<31)) as u32)])},
VertRefs{faces:vec![SubmeshFaceId(0),SubmeshFaceId(5),SubmeshFaceId(1)],edges:vec![SubmeshDirectedEdgeId((9223372036854775812u64-(1<<63)+(1<<31)) as u32),SubmeshDirectedEdgeId(0),SubmeshDirectedEdgeId((9223372036854775809u64-(1<<63)+(1<<31)) as u32)]}, VertRefs{faces:Cow::Borrowed(&[SubmeshFaceId(0),SubmeshFaceId(5),SubmeshFaceId(1)]),edges:Cow::Borrowed(&[SubmeshDirectedEdgeId((9223372036854775812u64-(1<<63)+(1<<31)) as u32),SubmeshDirectedEdgeId(0),SubmeshDirectedEdgeId((9223372036854775809u64-(1<<63)+(1<<31)) as u32)])},
VertRefs{faces:vec![SubmeshFaceId(0),SubmeshFaceId(2),SubmeshFaceId(1)],edges:vec![SubmeshDirectedEdgeId(1),SubmeshDirectedEdgeId((9223372036854775810u64-(1<<63)+(1<<31)) as u32),SubmeshDirectedEdgeId((9223372036854775814u64-(1<<63)+(1<<31)) as u32)]}, VertRefs{faces:Cow::Borrowed(&[SubmeshFaceId(0),SubmeshFaceId(2),SubmeshFaceId(1)]),edges:Cow::Borrowed(&[SubmeshDirectedEdgeId(1),SubmeshDirectedEdgeId((9223372036854775810u64-(1<<63)+(1<<31)) as u32),SubmeshDirectedEdgeId((9223372036854775814u64-(1<<63)+(1<<31)) as u32)])},
VertRefs{faces:vec![SubmeshFaceId(0),SubmeshFaceId(2),SubmeshFaceId(4)],edges:vec![SubmeshDirectedEdgeId(2),SubmeshDirectedEdgeId(3),SubmeshDirectedEdgeId((9223372036854775815u64-(1<<63)+(1<<31)) as u32)]}, VertRefs{faces:Cow::Borrowed(&[SubmeshFaceId(0),SubmeshFaceId(2),SubmeshFaceId(4)]),edges:Cow::Borrowed(&[SubmeshDirectedEdgeId(2),SubmeshDirectedEdgeId(3),SubmeshDirectedEdgeId((9223372036854775815u64-(1<<63)+(1<<31)) as u32)])},
VertRefs{faces:vec![SubmeshFaceId(3),SubmeshFaceId(5),SubmeshFaceId(1)],edges:vec![SubmeshDirectedEdgeId(4),SubmeshDirectedEdgeId((9223372036854775817u64-(1<<63)+(1<<31)) as u32),SubmeshDirectedEdgeId((9223372036854775813u64-(1<<63)+(1<<31)) as u32)]}, VertRefs{faces:Cow::Borrowed(&[SubmeshFaceId(3),SubmeshFaceId(5),SubmeshFaceId(1)]),edges:Cow::Borrowed(&[SubmeshDirectedEdgeId(4),SubmeshDirectedEdgeId((9223372036854775817u64-(1<<63)+(1<<31)) as u32),SubmeshDirectedEdgeId((9223372036854775813u64-(1<<63)+(1<<31)) as u32)])},
VertRefs{faces:vec![SubmeshFaceId(2),SubmeshFaceId(3),SubmeshFaceId(1)],edges:vec![SubmeshDirectedEdgeId(5),SubmeshDirectedEdgeId(6),SubmeshDirectedEdgeId((9223372036854775816u64-(1<<63)+(1<<31)) as u32)]}, VertRefs{faces:Cow::Borrowed(&[SubmeshFaceId(2),SubmeshFaceId(3),SubmeshFaceId(1)]),edges:Cow::Borrowed(&[SubmeshDirectedEdgeId(5),SubmeshDirectedEdgeId(6),SubmeshDirectedEdgeId((9223372036854775816u64-(1<<63)+(1<<31)) as u32)])},
VertRefs{faces:vec![SubmeshFaceId(2),SubmeshFaceId(3),SubmeshFaceId(4)],edges:vec![SubmeshDirectedEdgeId(7),SubmeshDirectedEdgeId(8),SubmeshDirectedEdgeId((9223372036854775818u64-(1<<63)+(1<<31)) as u32)]}, VertRefs{faces:Cow::Borrowed(&[SubmeshFaceId(2),SubmeshFaceId(3),SubmeshFaceId(4)]),edges:Cow::Borrowed(&[SubmeshDirectedEdgeId(7),SubmeshDirectedEdgeId(8),SubmeshDirectedEdgeId((9223372036854775818u64-(1<<63)+(1<<31)) as u32)])},
VertRefs{faces:vec![SubmeshFaceId(4),SubmeshFaceId(3),SubmeshFaceId(5)],edges:vec![SubmeshDirectedEdgeId(10),SubmeshDirectedEdgeId(11),SubmeshDirectedEdgeId(9)]} VertRefs{faces:Cow::Borrowed(&[SubmeshFaceId(4),SubmeshFaceId(3),SubmeshFaceId(5)]),edges:Cow::Borrowed(&[SubmeshDirectedEdgeId(10),SubmeshDirectedEdgeId(11),SubmeshDirectedEdgeId(9)])},
] ])
}; };
Self{ Self{
data, data,
@ -389,10 +395,10 @@ impl TryFrom<&model::Mesh> for PhysicsMesh{
} }
} }
PhysicsMeshTopology{ PhysicsMeshTopology{
faces:submesh_faces, faces:submesh_faces.into(),
verts:submesh_verts, verts:submesh_verts.into(),
face_topology:face_ref_guys.into_iter().map(|face_ref_guy|{ face_topology:face_ref_guys.into_iter().map(|face_ref_guy|{
FaceRefs{edges:face_ref_guy.0} FaceRefs{edges:face_ref_guy.0.into()}
}).collect(), }).collect(),
edge_topology:edge_pool.edge_guys.into_iter().map(|(edge_ref_verts,edge_ref_faces)| 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} EdgeRefs{faces:edge_ref_faces.0,verts:edge_ref_verts.0}
@ -407,7 +413,7 @@ impl TryFrom<&model::Mesh> for PhysicsMesh{
}).collect(); }).collect();
Ok(Self{ Ok(Self{
data:PhysicsMeshData{ data:PhysicsMeshData{
faces, faces:faces.into(),
verts, verts,
}, },
complete_mesh:mesh_topologies.pop().ok_or(PhysicsMeshError::NoPhysicsGroups)?, complete_mesh:mesh_topologies.pop().ok_or(PhysicsMeshError::NoPhysicsGroups)?,
@ -484,15 +490,12 @@ impl TransformedMesh<'_>{
pub fn verts<'a>(&'a self)->impl Iterator<Item=vec3::Vector3<Fixed<2,64>>>+'a{ 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)) 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{ fn farthest_vert(&self,dir:Planar64Vec3)->SubmeshVertId{
//this happens to be well-defined. there are no virtual virtices //this happens to be well-defined. there are no virtual virtices
SubmeshVertId::new( SubmeshVertId::new(
self.view.topology.verts.iter() self.view.topology.verts.iter()
.enumerate() .enumerate()
.max_by_key(|&(_,&vert_id)| .max_by_key(|(_,&vert_id)|
dir.dot(self.transform.vertex.transform_point3(self.view.data.verts[vert_id.get() as usize].0)) dir.dot(self.transform.vertex.transform_point3(self.view.data.verts[vert_id.get() as usize].0))
) )
//assume there is more than zero vertices. //assume there is more than zero vertices.
@ -722,40 +725,48 @@ impl MinkowskiMesh<'_>{
// //
// Most of the calculation time is just calculating the starting point // Most of the calculation time is just calculating the starting point
// for the "actual" crawling algorithm below (predict_collision_{in|out}). // for the "actual" crawling algorithm below (predict_collision_{in|out}).
fn closest_fev_not_inside(&self,mut infinity_body:Body,start_time:Time)->Option<FEV<MinkowskiMesh>>{ fn closest_fev_not_inside(&self,mut infinity_body:Body)->Option<FEV<MinkowskiMesh>>{
infinity_body.infinity_dir().and_then(|dir|{ infinity_body.infinity_dir().map_or(None,|dir|{
let infinity_fev=self.infinity_fev(-dir,infinity_body.position); let infinity_fev=self.infinity_fev(-dir,infinity_body.position);
//a line is simpler to solve than a parabola //a line is simpler to solve than a parabola
infinity_body.velocity=dir; infinity_body.velocity=dir;
infinity_body.acceleration=vec3::ZERO; infinity_body.acceleration=vec3::ZERO;
//crawl in from negative infinity along a tangent line to get the closest fev //crawl in from negative infinity along a tangent line to get the closest fev
// TODO: change crawl_fev args to delta time? Optional values? // TODO: change crawl_fev args to delta time? Optional values?
infinity_fev.crawl(self,&infinity_body,Time::MIN/4,start_time).miss() match infinity_fev.crawl(self,&infinity_body,Time::MIN/4,infinity_body.time){
crate::face_crawler::CrawlResult::Miss(fev)=>Some(fev),
crate::face_crawler::CrawlResult::Hit(_,_)=>None,
}
}) })
} }
pub fn predict_collision_in(&self,relative_body:&Body,Range{start:start_time,end:time_limit}:Range<Time>)->Option<(MinkowskiFace,GigaTime)>{ pub fn predict_collision_in(&self,relative_body:&Body,time_limit:Time)->Option<(MinkowskiFace,GigaTime)>{
self.closest_fev_not_inside(relative_body.clone(),start_time).and_then(|fev|{ self.closest_fev_not_inside(relative_body.clone()).map_or(None,|fev|{
//continue forwards along the body parabola //continue forwards along the body parabola
fev.crawl(self,relative_body,start_time,time_limit).hit() match fev.crawl(self,relative_body,relative_body.time,time_limit){
crate::face_crawler::CrawlResult::Miss(_)=>None,
crate::face_crawler::CrawlResult::Hit(face,time)=>Some((face,time)),
}
}) })
} }
pub fn predict_collision_out(&self,relative_body:&Body,Range{start:start_time,end:time_limit}:Range<Time>)->Option<(MinkowskiFace,GigaTime)>{ pub fn predict_collision_out(&self,relative_body:&Body,time_limit:Time)->Option<(MinkowskiFace,GigaTime)>{
//create an extrapolated body at time_limit //create an extrapolated body at time_limit
let infinity_body=-relative_body.clone(); let infinity_body=Body::new(
self.closest_fev_not_inside(infinity_body,-time_limit).and_then(|fev|{ relative_body.extrapolated_position(time_limit),
-relative_body.extrapolated_velocity(time_limit),
relative_body.acceleration,
-time_limit,
);
self.closest_fev_not_inside(infinity_body).map_or(None,|fev|{
//continue backwards along the body parabola //continue backwards along the body parabola
fev.crawl(self,&infinity_body,-time_limit,-start_time).hit() match fev.crawl(self,&-relative_body.clone(),-time_limit,-relative_body.time){
//no need to test -time<time_limit because of the first step crate::face_crawler::CrawlResult::Miss(_)=>None,
.map(|(face,time)|(face,-time)) crate::face_crawler::CrawlResult::Hit(face,time)=>Some((face,-time)),//no need to test -time<time_limit because of the first step
}
}) })
} }
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,time_limit:Time,contact_face_id:MinkowskiFace)->Option<(MinkowskiEdge,GigaTime)>{
//no algorithm needed, there is only one state and two cases (Edge,None) //no algorithm needed, there is only one state and two cases (Edge,None)
//determine when it passes an edge ("sliding off" case) //determine when it passes an edge ("sliding off" case)
let start_time={
let r=(start_time-relative_body.time).to_ratio();
Ratio::new(r.num,r.den)
};
let mut best_time={ let mut best_time={
let r=(time_limit-relative_body.time).to_ratio(); let r=(time_limit-relative_body.time).to_ratio();
Ratio::new(r.num.fix_4(),r.den.fix_4()) Ratio::new(r.num.fix_4(),r.den.fix_4())
@ -771,7 +782,7 @@ impl MinkowskiMesh<'_>{
//WARNING! d outside of *2 //WARNING! d outside of *2
//WARNING: truncated precision //WARNING: truncated precision
for dt in Fixed::<4,128>::zeroes2(((n.dot(relative_body.position))*2-d).fix_4(),n.dot(relative_body.velocity).fix_4()*2,n.dot(relative_body.acceleration).fix_4()){ for dt in Fixed::<4,128>::zeroes2(((n.dot(relative_body.position))*2-d).fix_4(),n.dot(relative_body.velocity).fix_4()*2,n.dot(relative_body.acceleration).fix_4()){
if start_time.le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(relative_body.extrapolated_velocity_ratio_dt(dt)).is_negative(){ if Ratio::new(Planar64::ZERO,Planar64::EPSILON).le_ratio(dt)&&dt.lt_ratio(best_time)&&n.dot(relative_body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
best_time=dt; best_time=dt;
best_edge=Some(directed_edge_id); best_edge=Some(directed_edge_id);
break; break;
@ -782,7 +793,10 @@ impl MinkowskiMesh<'_>{
} }
fn infinity_in(&self,infinity_body:Body)->Option<(MinkowskiFace,GigaTime)>{ fn infinity_in(&self,infinity_body:Body)->Option<(MinkowskiFace,GigaTime)>{
let infinity_fev=self.infinity_fev(-infinity_body.velocity,infinity_body.position); let infinity_fev=self.infinity_fev(-infinity_body.velocity,infinity_body.position);
infinity_fev.crawl(self,&infinity_body,Time::MIN/4,infinity_body.time).hit() match infinity_fev.crawl(self,&infinity_body,Time::MIN/4,infinity_body.time){
crate::face_crawler::CrawlResult::Miss(_)=>None,
crate::face_crawler::CrawlResult::Hit(face,time)=>Some((face,time)),
}
} }
pub fn is_point_in_mesh(&self,point:Planar64Vec3)->bool{ pub fn is_point_in_mesh(&self,point:Planar64Vec3)->bool{
let infinity_body=Body::new(point,vec3::Y,vec3::ZERO,Time::ZERO); let infinity_body=Body::new(point,vec3::Y,vec3::ZERO,Time::ZERO);
@ -988,7 +1002,7 @@ fn is_empty_volume(normals:Vec<Vector3<Fixed<3,96>>>)->bool{
for k in 0..len{ for k in 0..len{
if k!=i&&k!=j{ if k!=i&&k!=j{
let d=n.dot(normals[k]).is_negative(); let d=n.dot(normals[k]).is_negative();
if let &Some(comp)=&d_comp{ if let Some(comp)=&d_comp{
// This is testing if d_comp*d < 0 // This is testing if d_comp*d < 0
if comp^d{ if comp^d{
return true; return true;
@ -1008,3 +1022,9 @@ fn test_is_empty_volume(){
assert!(!is_empty_volume([vec3::X.fix_3(),vec3::Y.fix_3(),vec3::Z.fix_3()].to_vec())); assert!(!is_empty_volume([vec3::X.fix_3(),vec3::Y.fix_3(),vec3::Z.fix_3()].to_vec()));
assert!(is_empty_volume([vec3::X.fix_3(),vec3::Y.fix_3(),vec3::Z.fix_3(),vec3::NEG_X.fix_3()].to_vec())); assert!(is_empty_volume([vec3::X.fix_3(),vec3::Y.fix_3(),vec3::Z.fix_3(),vec3::NEG_X.fix_3()].to_vec()));
} }
#[test]
fn build_me_a_cube(){
let mesh=PhysicsMesh::unit_cube();
//println!("mesh={:?}",mesh);
}

@ -5,13 +5,13 @@ use strafesnet_common::physics::{
TimeInner as PhysicsTimeInner, TimeInner as PhysicsTimeInner,
Time as PhysicsTime, Time as PhysicsTime,
}; };
use strafesnet_common::session::Time as SessionTime; use strafesnet_common::session::{Time as SessionTime,TimeInner as SessionTimeInner};
use strafesnet_common::instruction::{InstructionConsumer,InstructionEmitter,TimedInstruction}; use strafesnet_common::instruction::{InstructionConsumer,InstructionEmitter,TimedInstruction};
type TimedSelfInstruction=TimedInstruction<Instruction,PhysicsTime>; type TimedSelfInstruction=TimedInstruction<Instruction,PhysicsTimeInner>;
type DoubleTimedSelfInstruction=TimedInstruction<TimedSelfInstruction,SessionTime>; type DoubleTimedSelfInstruction=TimedInstruction<TimedSelfInstruction,SessionTimeInner>;
type TimedPhysicsInstruction=TimedInstruction<PhysicsInstruction,PhysicsTime>; type TimedPhysicsInstruction=TimedInstruction<PhysicsInstruction,PhysicsTimeInner>;
const MOUSE_TIMEOUT:SessionTime=SessionTime::from_millis(10); const MOUSE_TIMEOUT:SessionTime=SessionTime::from_millis(10);
@ -89,14 +89,14 @@ pub struct MouseInterpolator{
// Maybe MouseInterpolator manipulation is better expressed using impls // Maybe MouseInterpolator manipulation is better expressed using impls
// and called from Instruction trait impls in session // and called from Instruction trait impls in session
impl InstructionConsumer<TimedSelfInstruction> for MouseInterpolator{ impl InstructionConsumer<TimedSelfInstruction> for MouseInterpolator{
type Time=SessionTime; type TimeInner=SessionTimeInner;
fn process_instruction(&mut self,ins:DoubleTimedSelfInstruction){ fn process_instruction(&mut self,ins:DoubleTimedSelfInstruction){
self.push_unbuffered_input(ins.time,ins.instruction.time,ins.instruction.instruction.into()) self.push_unbuffered_input(ins.time,ins.instruction.time,ins.instruction.instruction.into())
} }
} }
impl InstructionEmitter<StepInstruction> for MouseInterpolator{ impl InstructionEmitter<StepInstruction> for MouseInterpolator{
type Time=SessionTime; type TimeInner=SessionTimeInner;
fn next_instruction(&self,time_limit:SessionTime)->Option<TimedInstruction<StepInstruction,Self::Time>>{ fn next_instruction(&self,time_limit:SessionTime)->Option<TimedInstruction<StepInstruction,Self::TimeInner>>{
self.buffered_instruction_with_timeout(time_limit) self.buffered_instruction_with_timeout(time_limit)
} }
} }
@ -108,7 +108,7 @@ impl MouseInterpolator{
output:std::collections::VecDeque::new(), output:std::collections::VecDeque::new(),
} }
} }
fn push_mouse_and_flush_buffer(&mut self,ins:TimedInstruction<MouseInstruction,PhysicsTime>){ fn push_mouse_and_flush_buffer(&mut self,ins:TimedInstruction<MouseInstruction,PhysicsTimeInner>){
self.buffer.push_front(TimedInstruction{ self.buffer.push_front(TimedInstruction{
time:ins.time, time:ins.time,
instruction:BufferedInstruction::Mouse(ins.instruction).into(), instruction:BufferedInstruction::Mouse(ins.instruction).into(),
@ -219,7 +219,7 @@ impl MouseInterpolator{
} }
} }
} }
fn buffered_instruction_with_timeout(&self,time_limit:SessionTime)->Option<TimedInstruction<StepInstruction,SessionTime>>{ fn buffered_instruction_with_timeout(&self,time_limit:SessionTime)->Option<TimedInstruction<StepInstruction,SessionTimeInner>>{
match self.get_mouse_timedout_at(time_limit){ match self.get_mouse_timedout_at(time_limit){
Some(timeout)=>Some(TimedInstruction{ Some(timeout)=>Some(TimedInstruction{
time:timeout, time:timeout,
@ -232,7 +232,7 @@ impl MouseInterpolator{
}), }),
} }
} }
pub fn pop_buffered_instruction(&mut self,ins:TimedInstruction<StepInstruction,PhysicsTime>)->Option<TimedPhysicsInstruction>{ pub fn pop_buffered_instruction(&mut self,ins:TimedInstruction<StepInstruction,PhysicsTimeInner>)->Option<TimedPhysicsInstruction>{
match ins.instruction{ match ins.instruction{
StepInstruction::Pop=>(), StepInstruction::Pop=>(),
StepInstruction::Timeout=>self.timeout_mouse(ins.time), StepInstruction::Timeout=>self.timeout_mouse(ins.time),
@ -244,7 +244,6 @@ impl MouseInterpolator{
#[cfg(test)] #[cfg(test)]
mod test{ mod test{
use super::*; use super::*;
use strafesnet_common::session::TimeInner as SessionTimeInner;
#[test] #[test]
fn test(){ fn test(){
let mut interpolator=MouseInterpolator::new(); let mut interpolator=MouseInterpolator::new();

@ -1,5 +1,5 @@
use std::collections::{HashMap,HashSet}; use std::collections::{HashMap,HashSet};
use crate::model::{self as model_physics,PhysicsMesh,PhysicsMeshTransform,TransformedMesh,MeshQuery,PhysicsMeshId,PhysicsSubmeshId}; use crate::model_physics::{self,PhysicsMesh,PhysicsMeshTransform,TransformedMesh,MeshQuery,PhysicsMeshId,PhysicsSubmeshId};
use strafesnet_common::bvh; use strafesnet_common::bvh;
use strafesnet_common::map; use strafesnet_common::map;
use strafesnet_common::run; use strafesnet_common::run;
@ -32,7 +32,7 @@ pub enum InternalInstruction{
// Water, // Water,
} }
#[derive(Clone,Debug)] #[derive(Clone,Debug,Default)]
pub struct InputState{ pub struct InputState{
mouse:MouseState, mouse:MouseState,
next_mouse:MouseState, next_mouse:MouseState,
@ -40,15 +40,10 @@ pub struct InputState{
} }
impl InputState{ impl InputState{
fn set_next_mouse(&mut self,next_mouse:MouseState){ fn set_next_mouse(&mut self,next_mouse:MouseState){
// would this be correct?
// if self.next_mouse.time==next_mouse.time{
// self.next_mouse=next_mouse;
// }else{
//I like your functions magic language //I like your functions magic language
self.mouse=std::mem::replace(&mut self.next_mouse,next_mouse); self.mouse=std::mem::replace(&mut self.next_mouse,next_mouse);
//equivalently: //equivalently:
//(self.next_mouse,self.mouse)=(next_mouse,self.next_mouse.clone()); //(self.next_mouse,self.mouse)=(next_mouse,self.next_mouse.clone());
// }
} }
fn replace_mouse(&mut self,mouse:MouseState,next_mouse:MouseState){ fn replace_mouse(&mut self,mouse:MouseState,next_mouse:MouseState){
(self.next_mouse,self.mouse)=(next_mouse,mouse); (self.next_mouse,self.mouse)=(next_mouse,mouse);
@ -70,15 +65,6 @@ impl InputState{
((dm*t)/dt).as_ivec2() ((dm*t)/dt).as_ivec2()
} }
} }
impl Default for InputState{
fn default()->Self{
Self{
mouse:MouseState{pos:Default::default(),time:Time::ZERO-Time::EPSILON*2},
next_mouse:MouseState{pos:Default::default(),time:Time::ZERO-Time::EPSILON},
controls:Default::default(),
}
}
}
#[derive(Clone,Debug)] #[derive(Clone,Debug)]
enum JumpDirection{ enum JumpDirection{
Exactly(Planar64Vec3), Exactly(Planar64Vec3),
@ -161,17 +147,17 @@ fn ground_things(walk_settings:&gameplay_style::WalkSettings,contact:&ContactCol
let normal=contact_normal(models,hitbox_mesh,contact); let normal=contact_normal(models,hitbox_mesh,contact);
let gravity=touching.base_acceleration(models,style,camera,input_state); let gravity=touching.base_acceleration(models,style,camera,input_state);
let control_dir=style.get_y_control_dir(camera,input_state.controls); let control_dir=style.get_y_control_dir(camera,input_state.controls);
let target_velocity=walk_settings.get_walk_target_velocity(control_dir,normal); let mut target_velocity=walk_settings.get_walk_target_velocity(control_dir,normal);
let target_velocity_clipped=touching.constrain_velocity(models,hitbox_mesh,target_velocity); touching.constrain_velocity(models,hitbox_mesh,&mut target_velocity);
(gravity,target_velocity_clipped) (gravity,target_velocity)
} }
fn ladder_things(ladder_settings:&gameplay_style::LadderSettings,contact:&ContactCollision,touching:&TouchingState,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,style:&StyleModifiers,camera:&PhysicsCamera,input_state:&InputState)->(Planar64Vec3,Planar64Vec3){ fn ladder_things(ladder_settings:&gameplay_style::LadderSettings,contact:&ContactCollision,touching:&TouchingState,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,style:&StyleModifiers,camera:&PhysicsCamera,input_state:&InputState)->(Planar64Vec3,Planar64Vec3){
let normal=contact_normal(models,hitbox_mesh,contact); let normal=contact_normal(models,hitbox_mesh,contact);
let gravity=touching.base_acceleration(models,style,camera,input_state); let gravity=touching.base_acceleration(models,style,camera,input_state);
let control_dir=style.get_y_control_dir(camera,input_state.controls); let control_dir=style.get_y_control_dir(camera,input_state.controls);
let target_velocity=ladder_settings.get_ladder_target_velocity(control_dir,normal); let mut target_velocity=ladder_settings.get_ladder_target_velocity(control_dir,normal);
let target_velocity_clipped=touching.constrain_velocity(models,hitbox_mesh,target_velocity); touching.constrain_velocity(models,hitbox_mesh,&mut target_velocity);
(gravity,target_velocity_clipped) (gravity,target_velocity)
} }
#[derive(Default)] #[derive(Default)]
@ -229,6 +215,12 @@ impl PhysicsModels{
.map(|model|&model.transform), .map(|model|&model.transform),
} }
} }
fn contact_model(&self,model_id:ContactModelId)->&ContactModel{
&self.contact_models[&model_id]
}
fn intersect_model(&self,model_id:IntersectModelId)->&IntersectModel{
&self.intersect_models[&model_id]
}
fn contact_attr(&self,model_id:ContactModelId)->&gameplay_attributes::ContactAttributes{ fn contact_attr(&self,model_id:ContactModelId)->&gameplay_attributes::ContactAttributes{
&self.contact_attributes[&self.contact_models[&model_id].attr_id] &self.contact_attributes[&self.contact_models[&model_id].attr_id]
} }
@ -263,7 +255,7 @@ impl PhysicsCamera{
); );
self.clamped_mouse_pos=unclamped_mouse_pos; self.clamped_mouse_pos=unclamped_mouse_pos;
} }
pub fn simulate_move_angles(&self,mouse_delta:glam::IVec2)->glam::Vec2{ pub fn simulate_move_angles(&self,mouse_delta:glam::IVec2)->glam::Vec2 {
let a=-self.sensitivity.mul_int((self.clamped_mouse_pos+mouse_delta).as_i64vec2()); let a=-self.sensitivity.mul_int((self.clamped_mouse_pos+mouse_delta).as_i64vec2());
let ax=Angle32::wrap_from_i64(a.x); let ax=Angle32::wrap_from_i64(a.x);
let ay=Angle32::clamp_from_i64(a.y) let ay=Angle32::clamp_from_i64(a.y)
@ -556,7 +548,7 @@ impl MoveState{
=>None, =>None,
} }
} }
fn next_move_instruction(&self,strafe:&Option<gameplay_style::StrafeSettings>,time:Time)->Option<TimedInstruction<InternalInstruction,Time>>{ fn next_move_instruction(&self,strafe:&Option<gameplay_style::StrafeSettings>,time:Time)->Option<TimedInstruction<InternalInstruction,TimeInner>>{
//check if you have a valid walk state and create an instruction //check if you have a valid walk state and create an instruction
match self{ match self{
MoveState::Walk(walk_state)|MoveState::Ladder(walk_state)=>match &walk_state.target{ MoveState::Walk(walk_state)|MoveState::Ladder(walk_state)=>match &walk_state.target{
@ -762,8 +754,8 @@ impl TouchingState{
//TODO: add water //TODO: add water
a a
} }
fn constrain_velocity(&self,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,velocity:Planar64Vec3)->Planar64Vec3{ fn constrain_velocity(&self,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,velocity:&mut Planar64Vec3){
let contacts:Vec<_>=self.contacts.iter().map(|contact|{ let contacts=self.contacts.iter().map(|contact|{
let n=contact_normal(models,hitbox_mesh,contact); let n=contact_normal(models,hitbox_mesh,contact);
crate::push_solve::Contact{ crate::push_solve::Contact{
position:vec3::ZERO, position:vec3::ZERO,
@ -771,10 +763,10 @@ impl TouchingState{
normal:n, normal:n,
} }
}).collect(); }).collect();
crate::push_solve::push_solve(&contacts,velocity) *velocity=crate::push_solve::push_solve(&contacts,*velocity);
} }
fn constrain_acceleration(&self,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,acceleration:Planar64Vec3)->Planar64Vec3{ fn constrain_acceleration(&self,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,acceleration:&mut Planar64Vec3){
let contacts:Vec<_>=self.contacts.iter().map(|contact|{ let contacts=self.contacts.iter().map(|contact|{
let n=contact_normal(models,hitbox_mesh,contact); let n=contact_normal(models,hitbox_mesh,contact);
crate::push_solve::Contact{ crate::push_solve::Contact{
position:vec3::ZERO, position:vec3::ZERO,
@ -782,16 +774,15 @@ impl TouchingState{
normal:n, normal:n,
} }
}).collect(); }).collect();
crate::push_solve::push_solve(&contacts,acceleration) *acceleration=crate::push_solve::push_solve(&contacts,*acceleration);
} }
fn predict_collision_end(&self,collector:&mut instruction::InstructionCollector<InternalInstruction,Time>,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,body:&Body,start_time:Time){ fn predict_collision_end(&self,collector:&mut instruction::InstructionCollector<InternalInstruction,TimeInner>,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,body:&Body,time:Time){
// let relative_body=body.relative_to(&Body::ZERO); let relative_body=crate::body::VirtualBody::relative(&Body::ZERO,body).body(time);
let relative_body=body;
for contact in &self.contacts{ for contact in &self.contacts{
//detect face slide off //detect face slide off
let model_mesh=models.contact_mesh(contact); let model_mesh=models.contact_mesh(contact);
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,hitbox_mesh.transformed_mesh()); let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,hitbox_mesh.transformed_mesh());
collector.collect(minkowski.predict_collision_face_out(&relative_body,start_time..collector.time(),contact.face_id).map(|(_face,time)|{ collector.collect(minkowski.predict_collision_face_out(&relative_body,collector.time(),contact.face_id).map(|(_face,time)|{
TimedInstruction{ TimedInstruction{
time:relative_body.time+time.into(), time:relative_body.time+time.into(),
instruction:InternalInstruction::CollisionEnd( instruction:InternalInstruction::CollisionEnd(
@ -805,7 +796,7 @@ impl TouchingState{
//detect model collision in reverse //detect model collision in reverse
let model_mesh=models.intersect_mesh(intersect); let model_mesh=models.intersect_mesh(intersect);
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,hitbox_mesh.transformed_mesh()); let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,hitbox_mesh.transformed_mesh());
collector.collect(minkowski.predict_collision_out(&relative_body,start_time..collector.time()).map(|(_face,time)|{ collector.collect(minkowski.predict_collision_out(&relative_body,collector.time()).map(|(_face,time)|{
TimedInstruction{ TimedInstruction{
time:relative_body.time+time.into(), time:relative_body.time+time.into(),
instruction:InternalInstruction::CollisionEnd( instruction:InternalInstruction::CollisionEnd(
@ -866,19 +857,13 @@ impl PhysicsState{
pub const fn camera(&self)->PhysicsCamera{ pub const fn camera(&self)->PhysicsCamera{
self.camera self.camera
} }
pub const fn mode(&self)->gameplay_modes::ModeId{
self.mode_state.get_mode_id()
}
pub fn get_finish_time(&self)->Option<run::Time>{
self.run.get_finish_time()
}
pub fn clear(&mut self){ pub fn clear(&mut self){
self.touching.clear(); self.touching.clear();
} }
fn reset_to_default(&mut self){ fn reset_to_default(&mut self){
*self=Self::default(); *self=Self::default();
} }
fn next_move_instruction(&self)->Option<TimedInstruction<InternalInstruction,Time>>{ fn next_move_instruction(&self)->Option<TimedInstruction<InternalInstruction,TimeInner>>{
self.move_state.next_move_instruction(&self.style.strafe,self.time) self.move_state.next_move_instruction(&self.style.strafe,self.time)
} }
fn cull_velocity(&mut self,data:&PhysicsData,velocity:Planar64Vec3){ fn cull_velocity(&mut self,data:&PhysicsData,velocity:Planar64Vec3){
@ -935,7 +920,7 @@ pub struct PhysicsData{
impl Default for PhysicsData{ impl Default for PhysicsData{
fn default()->Self{ fn default()->Self{
Self{ Self{
bvh:bvh::BvhNode::empty(), bvh:bvh::BvhNode::default(),
models:Default::default(), models:Default::default(),
modes:Default::default(), modes:Default::default(),
hitbox_mesh:StyleModifiers::default().calculate_mesh(), hitbox_mesh:StyleModifiers::default().calculate_mesh(),
@ -950,21 +935,21 @@ pub struct PhysicsContext<'a>{
// the physics consumes both Instruction and PhysicsInternalInstruction, // the physics consumes both Instruction and PhysicsInternalInstruction,
// but can only emit PhysicsInternalInstruction // but can only emit PhysicsInternalInstruction
impl InstructionConsumer<InternalInstruction> for PhysicsContext<'_>{ impl InstructionConsumer<InternalInstruction> for PhysicsContext<'_>{
type Time=Time; type TimeInner=TimeInner;
fn process_instruction(&mut self,ins:TimedInstruction<InternalInstruction,Time>){ fn process_instruction(&mut self,ins:TimedInstruction<InternalInstruction,TimeInner>){
atomic_internal_instruction(&mut self.state,&self.data,ins) atomic_internal_instruction(&mut self.state,&self.data,ins)
} }
} }
impl InstructionConsumer<Instruction> for PhysicsContext<'_>{ impl InstructionConsumer<Instruction> for PhysicsContext<'_>{
type Time=Time; type TimeInner=TimeInner;
fn process_instruction(&mut self,ins:TimedInstruction<Instruction,Time>){ fn process_instruction(&mut self,ins:TimedInstruction<Instruction,TimeInner>){
atomic_input_instruction(&mut self.state,&self.data,ins) atomic_input_instruction(&mut self.state,&self.data,ins)
} }
} }
impl InstructionEmitter<InternalInstruction> for PhysicsContext<'_>{ impl InstructionEmitter<InternalInstruction> for PhysicsContext<'_>{
type Time=Time; type TimeInner=TimeInner;
//this little next instruction function could cache its return value and invalidate the cached value by watching the State. //this little next instruction function could cache its return value and invalidate the cached value by watching the State.
fn next_instruction(&self,time_limit:Time)->Option<TimedInstruction<InternalInstruction,Time>>{ fn next_instruction(&self,time_limit:Time)->Option<TimedInstruction<InternalInstruction,TimeInner>>{
next_instruction_internal(&self.state,&self.data,time_limit) next_instruction_internal(&self.state,&self.data,time_limit)
} }
} }
@ -972,7 +957,7 @@ impl PhysicsContext<'_>{
pub fn run_input_instruction( pub fn run_input_instruction(
state:&mut PhysicsState, state:&mut PhysicsState,
data:&PhysicsData, data:&PhysicsData,
instruction:TimedInstruction<Instruction,Time> instruction:TimedInstruction<Instruction,TimeInner>
){ ){
let mut context=PhysicsContext{state,data}; let mut context=PhysicsContext{state,data};
context.process_exhaustive(instruction.time); context.process_exhaustive(instruction.time);
@ -1121,9 +1106,9 @@ impl PhysicsData{
} }
//this is the one who asks //this is the one who asks
fn next_instruction_internal(state:&PhysicsState,data:&PhysicsData,time_limit:Time)->Option<TimedInstruction<InternalInstruction,Time>>{ fn next_instruction_internal(state:&PhysicsState,data:&PhysicsData,time_limit:Time)->Option<TimedInstruction<InternalInstruction,TimeInner>>{
//JUST POLLING!!! NO MUTATION //JUST POLLING!!! NO MUTATION
let mut collector=instruction::InstructionCollector::new(time_limit); let mut collector = instruction::InstructionCollector::new(time_limit);
collector.collect(state.next_move_instruction()); collector.collect(state.next_move_instruction());
@ -1134,15 +1119,15 @@ impl PhysicsData{
state.body.grow_aabb(&mut aabb,state.time,collector.time()); state.body.grow_aabb(&mut aabb,state.time,collector.time());
aabb.inflate(data.hitbox_mesh.halfsize); aabb.inflate(data.hitbox_mesh.halfsize);
//relative to moving platforms //relative to moving platforms
//let relative_body=state.body.relative_to(&Body::ZERO); //let relative_body=&VirtualBody::relative(&Body::default(),&state.body).body(state.time);
let relative_body=&state.body; let relative_body=&state.body;
data.bvh.sample_aabb(&aabb,&mut |&convex_mesh_id|{ data.bvh.the_tester(&aabb,&mut |&convex_mesh_id|{
//no checks are needed because of the time limits. //no checks are needed because of the time limits.
let model_mesh=data.models.mesh(convex_mesh_id); let model_mesh=data.models.mesh(convex_mesh_id);
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,data.hitbox_mesh.transformed_mesh()); let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,data.hitbox_mesh.transformed_mesh());
collector.collect(minkowski.predict_collision_in(relative_body,state.time..collector.time()) collector.collect(minkowski.predict_collision_in(relative_body,collector.time())
//temp (?) code to avoid collision loops //temp (?) code to avoid collision loops
.and_then(|(face,dt)|{ .map_or(None,|(face,dt)|{
// this must be rounded to avoid the infinite loop when hitting the start zone // this must be rounded to avoid the infinite loop when hitting the start zone
let time=relative_body.time+dt.into(); let time=relative_body.time+dt.into();
(state.time<time).then_some((time,face,dt)) (state.time<time).then_some((time,face,dt))
@ -1157,7 +1142,7 @@ impl PhysicsData{
) )
); );
}); });
collector.take() collector.instruction()
} }
@ -1197,8 +1182,8 @@ fn recalculate_touching(
aabb.grow(body.position); aabb.grow(body.position);
aabb.inflate(hitbox_mesh.halfsize); aabb.inflate(hitbox_mesh.halfsize);
//relative to moving platforms //relative to moving platforms
//let relative_body=state.body.relative_to(&Body::ZERO); //let relative_body=&VirtualBody::relative(&Body::default(),&state.body).body(state.time);
bvh.sample_aabb(&aabb,&mut |&convex_mesh_id|{ bvh.the_tester(&aabb,&mut |&convex_mesh_id|{
//no checks are needed because of the time limits. //no checks are needed because of the time limits.
let model_mesh=models.mesh(convex_mesh_id); let model_mesh=models.mesh(convex_mesh_id);
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,hitbox_mesh.transformed_mesh()); let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,hitbox_mesh.transformed_mesh());
@ -1252,14 +1237,16 @@ fn set_velocity_cull(body:&mut Body,touching:&mut TouchingState,models:&PhysicsM
let r=n.dot(v).is_positive(); let r=n.dot(v).is_positive();
if r{ if r{
culled=true; culled=true;
println!("set_velocity_cull contact={:?}",contact);
} }
!r !r
}); });
set_velocity(body,touching,models,hitbox_mesh,v); set_velocity(body,touching,models,hitbox_mesh,v);
culled culled
} }
fn set_velocity(body:&mut Body,touching:&TouchingState,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,v:Planar64Vec3){ fn set_velocity(body:&mut Body,touching:&TouchingState,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,mut v:Planar64Vec3){
body.velocity=touching.constrain_velocity(models,hitbox_mesh,v); touching.constrain_velocity(models,hitbox_mesh,&mut v);
body.velocity=v;
} }
fn set_acceleration_cull(body:&mut Body,touching:&mut TouchingState,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,a:Planar64Vec3)->bool{ 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 //This is not correct but is better than what I have
@ -1269,14 +1256,16 @@ fn set_acceleration_cull(body:&mut Body,touching:&mut TouchingState,models:&Phys
let r=n.dot(a).is_positive(); let r=n.dot(a).is_positive();
if r{ if r{
culled=true; culled=true;
println!("set_acceleration_cull contact={:?}",contact);
} }
!r !r
}); });
set_acceleration(body,touching,models,hitbox_mesh,a); set_acceleration(body,touching,models,hitbox_mesh,a);
culled culled
} }
fn set_acceleration(body:&mut Body,touching:&TouchingState,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,a:Planar64Vec3){ fn set_acceleration(body:&mut Body,touching:&TouchingState,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,mut a:Planar64Vec3){
body.acceleration=touching.constrain_acceleration(models,hitbox_mesh,a); touching.constrain_acceleration(models,hitbox_mesh,&mut a);
body.acceleration=a;
} }
fn teleport( fn teleport(
@ -1485,7 +1474,7 @@ fn collision_start_contact(
let model_id=contact.model_id.into(); let model_id=contact.model_id.into();
let mut allow_run_teleport_behaviour=not_spawn_at(mode,model_id); let mut allow_run_teleport_behaviour=not_spawn_at(mode,model_id);
match &attr.contacting.contact_behaviour{ match &attr.contacting.contact_behaviour{
Some(gameplay_attributes::ContactingBehaviour::Surf)=>(), Some(gameplay_attributes::ContactingBehaviour::Surf)=>println!("I'm surfing!"),
Some(gameplay_attributes::ContactingBehaviour::Cling)=>println!("Unimplemented!"), Some(gameplay_attributes::ContactingBehaviour::Cling)=>println!("Unimplemented!"),
&Some(gameplay_attributes::ContactingBehaviour::Elastic(elasticity))=>{ &Some(gameplay_attributes::ContactingBehaviour::Elastic(elasticity))=>{
let reflected_velocity=body.velocity+((body.velocity-incident_velocity)*Planar64::raw(elasticity as i64+1)).fix_1(); let reflected_velocity=body.velocity+((body.velocity-incident_velocity)*Planar64::raw(elasticity as i64+1)).fix_1();
@ -1515,21 +1504,6 @@ fn collision_start_contact(
} }
}, },
} }
match &attr.general.trajectory{
Some(trajectory)=>{
match trajectory{
gameplay_attributes::SetTrajectory::AirTime(_)=>todo!(),
gameplay_attributes::SetTrajectory::Height(_)=>todo!(),
gameplay_attributes::SetTrajectory::TargetPointTime{..}=>todo!(),
gameplay_attributes::SetTrajectory::TargetPointSpeed{..}=>todo!(),
&gameplay_attributes::SetTrajectory::Velocity(velocity)=>{
move_state.cull_velocity(velocity,body,touching,models,hitbox_mesh,style,camera,input_state);
},
gameplay_attributes::SetTrajectory::DotVelocity{..}=>todo!(),
}
},
None=>(),
}
//I love making functions with 10 arguments to dodge the borrow checker //I love making functions with 10 arguments to dodge the borrow checker
if allow_run_teleport_behaviour{ if allow_run_teleport_behaviour{
run_teleport_behaviour(model_id,attr.general.wormhole.as_ref(),mode,move_state,body,touching,run,mode_state,models,hitbox_mesh,bvh,style,camera,input_state,time); run_teleport_behaviour(model_id,attr.general.wormhole.as_ref(),mode,move_state,body,touching,run,mode_state,models,hitbox_mesh,bvh,style,camera,input_state,time);
@ -1557,6 +1531,21 @@ fn collision_start_contact(
} }
} }
} }
match &attr.general.trajectory{
Some(trajectory)=>{
match trajectory{
gameplay_attributes::SetTrajectory::AirTime(_)=>todo!(),
gameplay_attributes::SetTrajectory::Height(_)=>todo!(),
gameplay_attributes::SetTrajectory::TargetPointTime { target_point: _, time: _ }=>todo!(),
gameplay_attributes::SetTrajectory::TargetPointSpeed { target_point: _, speed: _, trajectory_choice: _ }=>todo!(),
&gameplay_attributes::SetTrajectory::Velocity(velocity)=>{
move_state.cull_velocity(velocity,body,touching,models,hitbox_mesh,style,camera,input_state);
},
gameplay_attributes::SetTrajectory::DotVelocity { direction: _, dot: _ }=>todo!(),
}
},
None=>(),
}
//doing enum to set the acceleration when surfing //doing enum to set the acceleration when surfing
//doing input_and_body to refresh the walk state if you hit a wall while accelerating //doing input_and_body to refresh the walk state if you hit a wall while accelerating
move_state.apply_enum_and_input_and_body(body,touching,models,hitbox_mesh,style,camera,input_state); move_state.apply_enum_and_input_and_body(body,touching,models,hitbox_mesh,style,camera,input_state);
@ -1651,7 +1640,7 @@ fn collision_end_intersect(
} }
} }
} }
fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedInstruction<InternalInstruction,Time>){ fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedInstruction<InternalInstruction,TimeInner>){
state.time=ins.time; state.time=ins.time;
let (should_advance_body,goober_time)=match ins.instruction{ let (should_advance_body,goober_time)=match ins.instruction{
InternalInstruction::CollisionStart(_,dt) InternalInstruction::CollisionStart(_,dt)
@ -1727,19 +1716,19 @@ fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:Tim
|MoveState::Fly |MoveState::Fly
=>println!("ReachWalkTargetVelocity fired for non-walking MoveState"), =>println!("ReachWalkTargetVelocity fired for non-walking MoveState"),
MoveState::Walk(walk_state)|MoveState::Ladder(walk_state)=>{ MoveState::Walk(walk_state)|MoveState::Ladder(walk_state)=>{
//velocity is already handled by advance_time match &walk_state.target{
//we know that the acceleration is precisely zero because the walk target is known to be reachable
//which means that gravity can be fully cancelled
//ignore moving platforms for now
let target=core::mem::replace(&mut walk_state.target,TransientAcceleration::Reached);
set_acceleration(&mut state.body,&state.touching,&data.models,&data.hitbox_mesh,vec3::ZERO);
// check what the target was to see if it was invalid
match target{
//you are not supposed to reach a walk target which is already reached! //you are not supposed to reach a walk target which is already reached!
TransientAcceleration::Reached=>println!("Invalid walk target: Reached"), TransientAcceleration::Reached=>unreachable!(),
TransientAcceleration::Reachable{..}=>(), TransientAcceleration::Reachable{acceleration:_,time:_}=>{
//velocity is already handled by advance_time
//we know that the acceleration is precisely zero because the walk target is known to be reachable
//which means that gravity can be fully cancelled
//ignore moving platforms for now
set_acceleration(&mut state.body,&state.touching,&data.models,&data.hitbox_mesh,vec3::ZERO);
walk_state.target=TransientAcceleration::Reached;
},
//you are not supposed to reach an unreachable walk target! //you are not supposed to reach an unreachable walk target!
TransientAcceleration::Unreachable{..}=>println!("Invalid walk target: Unreachable"), TransientAcceleration::Unreachable{acceleration:_}=>unreachable!(),
} }
} }
} }
@ -1747,7 +1736,7 @@ fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:Tim
} }
} }
fn atomic_input_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedInstruction<Instruction,Time>){ fn atomic_input_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedInstruction<Instruction,TimeInner>){
state.time=ins.time; state.time=ins.time;
let should_advance_body=match ins.instruction{ let should_advance_body=match ins.instruction{
//the body may as well be a quantum wave function //the body may as well be a quantum wave function
@ -1798,7 +1787,7 @@ fn atomic_input_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedI
let jump_dir=walk_state.jump_direction.direction(&data.models,&data.hitbox_mesh,&walk_state.contact); let jump_dir=walk_state.jump_direction.direction(&data.models,&data.hitbox_mesh,&walk_state.contact);
let booster_option=data.models.contact_attr(walk_state.contact.model_id).general.booster.as_ref(); let booster_option=data.models.contact_attr(walk_state.contact.model_id).general.booster.as_ref();
let jumped_velocity=jump_settings.jumped_velocity(&state.style,jump_dir,state.body.velocity,booster_option); let jumped_velocity=jump_settings.jumped_velocity(&state.style,jump_dir,state.body.velocity,booster_option);
state.cull_velocity(data,jumped_velocity); state.cull_velocity(&data,jumped_velocity);
} }
} }
b_refresh_walk_target=false; b_refresh_walk_target=false;
@ -1812,9 +1801,9 @@ fn atomic_input_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedI
state.reset_to_default(); state.reset_to_default();
b_refresh_walk_target=false; b_refresh_walk_target=false;
}, },
Instruction::Mode(ModeInstruction::Restart(mode_id))=>{ Instruction::Mode(ModeInstruction::Restart)=>{
//teleport to mode start zone //teleport to start zone
let mode=data.modes.get_mode(mode_id); let mode=data.modes.get_mode(state.mode_state.get_mode_id());
let spawn_point=mode.and_then(|mode| let spawn_point=mode.and_then(|mode|
//TODO: spawn at the bottom of the start zone plus the hitbox size //TODO: spawn at the bottom of the start zone plus the hitbox size
//TODO: set camera andles to face the same way as the start zone //TODO: set camera andles to face the same way as the start zone
@ -1868,6 +1857,7 @@ fn atomic_input_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedI
#[cfg(test)] #[cfg(test)]
mod test{ mod test{
use strafesnet_common::integer::{vec3::{self,int as int3},mat3}; use strafesnet_common::integer::{vec3::{self,int as int3},mat3};
use crate::body::VirtualBody;
use super::*; use super::*;
fn test_collision_axis_aligned(relative_body:Body,expected_collision_time:Option<Time>){ fn test_collision_axis_aligned(relative_body:Body,expected_collision_time:Option<Time>){
let h0=HitboxMesh::new(PhysicsMesh::unit_cube(),integer::Planar64Affine3::new(mat3::from_diagonal(int3(5,1,5)>>1),vec3::ZERO)); let h0=HitboxMesh::new(PhysicsMesh::unit_cube(),integer::Planar64Affine3::new(mat3::from_diagonal(int3(5,1,5)>>1),vec3::ZERO));
@ -1875,7 +1865,7 @@ mod test{
let hitbox_mesh=h1.transformed_mesh(); let hitbox_mesh=h1.transformed_mesh();
let platform_mesh=h0.transformed_mesh(); let platform_mesh=h0.transformed_mesh();
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(platform_mesh,hitbox_mesh); let minkowski=model_physics::MinkowskiMesh::minkowski_sum(platform_mesh,hitbox_mesh);
let collision=minkowski.predict_collision_in(&relative_body,Time::ZERO..Time::from_secs(10)); let collision=minkowski.predict_collision_in(&relative_body,Time::from_secs(10));
assert_eq!(collision.map(|tup|relative_body.time+tup.1.into()),expected_collision_time,"Incorrect time of collision"); assert_eq!(collision.map(|tup|relative_body.time+tup.1.into()),expected_collision_time,"Incorrect time of collision");
} }
fn test_collision_rotated(relative_body:Body,expected_collision_time:Option<Time>){ fn test_collision_rotated(relative_body:Body,expected_collision_time:Option<Time>){
@ -1893,7 +1883,7 @@ mod test{
let hitbox_mesh=h1.transformed_mesh(); let hitbox_mesh=h1.transformed_mesh();
let platform_mesh=h0.transformed_mesh(); let platform_mesh=h0.transformed_mesh();
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(platform_mesh,hitbox_mesh); let minkowski=model_physics::MinkowskiMesh::minkowski_sum(platform_mesh,hitbox_mesh);
let collision=minkowski.predict_collision_in(&relative_body,Time::ZERO..Time::from_secs(10)); let collision=minkowski.predict_collision_in(&relative_body,Time::from_secs(10));
assert_eq!(collision.map(|tup|relative_body.time+tup.1.into()),expected_collision_time,"Incorrect time of collision"); assert_eq!(collision.map(|tup|relative_body.time+tup.1.into()),expected_collision_time,"Incorrect time of collision");
} }
fn test_collision(relative_body:Body,expected_collision_time:Option<Time>){ fn test_collision(relative_body:Body,expected_collision_time:Option<Time>){
@ -1947,111 +1937,111 @@ mod test{
} }
#[test] #[test]
fn test_collision_parabola_edge_east_from_west(){ fn test_collision_parabola_edge_east_from_west(){
test_collision(Body::new( test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
int3(3,3,0), int3(3,3,0),
int3(100,-1,0), int3(100,-1,0),
int3(0,-1,0), int3(0,-1,0),
Time::ZERO Time::ZERO
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0))); )).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
} }
#[test] #[test]
fn test_collision_parabola_edge_south_from_north(){ fn test_collision_parabola_edge_south_from_north(){
test_collision(Body::new( test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
int3(0,3,3), int3(0,3,3),
int3(0,-1,100), int3(0,-1,100),
int3(0,-1,0), int3(0,-1,0),
Time::ZERO Time::ZERO
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0))); )).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
} }
#[test] #[test]
fn test_collision_parabola_edge_west_from_east(){ fn test_collision_parabola_edge_west_from_east(){
test_collision(Body::new( test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
int3(-3,3,0), int3(-3,3,0),
int3(-100,-1,0), int3(-100,-1,0),
int3(0,-1,0), int3(0,-1,0),
Time::ZERO Time::ZERO
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0))); )).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
} }
#[test] #[test]
fn test_collision_parabola_edge_north_from_south(){ fn test_collision_parabola_edge_north_from_south(){
test_collision(Body::new( test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
int3(0,3,-3), int3(0,3,-3),
int3(0,-1,-100), int3(0,-1,-100),
int3(0,-1,0), int3(0,-1,0),
Time::ZERO Time::ZERO
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0))); )).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
} }
#[test] #[test]
fn test_collision_parabola_edge_north_from_ne(){ fn test_collision_parabola_edge_north_from_ne(){
test_collision(Body::new( test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
int3(0,6,-7)>>1, int3(0,6,-7)>>1,
int3(-10,-1,1), int3(-10,-1,1),
int3(0,-1,0), int3(0,-1,0),
Time::ZERO Time::ZERO
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0))); )).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
} }
#[test] #[test]
fn test_collision_parabola_edge_north_from_nw(){ fn test_collision_parabola_edge_north_from_nw(){
test_collision(Body::new( test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
int3(0,6,-7)>>1, int3(0,6,-7)>>1,
int3(10,-1,1), int3(10,-1,1),
int3(0,-1,0), int3(0,-1,0),
Time::ZERO Time::ZERO
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0))); )).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
} }
#[test] #[test]
fn test_collision_parabola_edge_east_from_se(){ fn test_collision_parabola_edge_east_from_se(){
test_collision(Body::new( test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
int3(7,6,0)>>1, int3(7,6,0)>>1,
int3(-1,-1,-10), int3(-1,-1,-10),
int3(0,-1,0), int3(0,-1,0),
Time::ZERO Time::ZERO
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0))); )).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
} }
#[test] #[test]
fn test_collision_parabola_edge_east_from_ne(){ fn test_collision_parabola_edge_east_from_ne(){
test_collision(Body::new( test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
int3(7,6,0)>>1, int3(7,6,0)>>1,
int3(-1,-1,10), int3(-1,-1,10),
int3(0,-1,0), int3(0,-1,0),
Time::ZERO Time::ZERO
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0))); )).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
} }
#[test] #[test]
fn test_collision_parabola_edge_south_from_se(){ fn test_collision_parabola_edge_south_from_se(){
test_collision(Body::new( test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
int3(0,6,7)>>1, int3(0,6,7)>>1,
int3(-10,-1,-1), int3(-10,-1,-1),
int3(0,-1,0), int3(0,-1,0),
Time::ZERO Time::ZERO
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0))); )).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
} }
#[test] #[test]
fn test_collision_parabola_edge_south_from_sw(){ fn test_collision_parabola_edge_south_from_sw(){
test_collision(Body::new( test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
int3(0,6,7)>>1, int3(0,6,7)>>1,
int3(10,-1,-1), int3(10,-1,-1),
int3(0,-1,0), int3(0,-1,0),
Time::ZERO Time::ZERO
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0))); )).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
} }
#[test] #[test]
fn test_collision_parabola_edge_west_from_se(){ fn test_collision_parabola_edge_west_from_se(){
test_collision(Body::new( test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
int3(-7,6,0)>>1, int3(-7,6,0)>>1,
int3(1,-1,-10), int3(1,-1,-10),
int3(0,-1,0), int3(0,-1,0),
Time::ZERO Time::ZERO
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0))); )).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
} }
#[test] #[test]
fn test_collision_parabola_edge_west_from_ne(){ fn test_collision_parabola_edge_west_from_ne(){
test_collision(Body::new( test_collision(VirtualBody::relative(&Body::ZERO,&Body::new(
int3(-7,6,0)>>1, int3(-7,6,0)>>1,
int3(1,-1,10), int3(1,-1,10),
int3(0,-1,0), int3(0,-1,0),
Time::ZERO Time::ZERO
).relative_to(&Body::ZERO).body(Time::from_secs(-1)),Some(Time::from_secs(0))); )).body(Time::from_secs(-1)),Some(Time::from_secs(0)));
} }
#[test] #[test]
fn test_collision_oblique(){ fn test_collision_oblique(){

Some files were not shown because too many files have changed in this diff Show More