forked from StrafesNET/strafe-client
Compare commits
13 Commits
master
...
physics-th
Author | SHA1 | Date | |
---|---|---|---|
ede287acd6 | |||
9df4da9d26 | |||
77d060b90c | |||
f320520292 | |||
6fc585e01e | |||
928654c7df | |||
8000d1db29 | |||
194b9b9a11 | |||
4933543dbf | |||
285a19a73a | |||
f3f624c44c | |||
e547940f2b | |||
51aac7b214 |
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -1692,6 +1692,7 @@ dependencies = [
|
|||||||
"lazy-regex",
|
"lazy-regex",
|
||||||
"log",
|
"log",
|
||||||
"obj",
|
"obj",
|
||||||
|
"parking_lot",
|
||||||
"pollster",
|
"pollster",
|
||||||
"rbx_binary",
|
"rbx_binary",
|
||||||
"rbx_dom_weak",
|
"rbx_dom_weak",
|
||||||
|
@ -14,6 +14,7 @@ glam = "0.24.1"
|
|||||||
lazy-regex = "3.0.2"
|
lazy-regex = "3.0.2"
|
||||||
log = "0.4.20"
|
log = "0.4.20"
|
||||||
obj = "0.10.2"
|
obj = "0.10.2"
|
||||||
|
parking_lot = "0.12.1"
|
||||||
pollster = "0.3.0"
|
pollster = "0.3.0"
|
||||||
rbx_binary = "0.7.1"
|
rbx_binary = "0.7.1"
|
||||||
rbx_dom_weak = "2.5.0"
|
rbx_dom_weak = "2.5.0"
|
||||||
|
221
src/body.rs
221
src/body.rs
@ -34,7 +34,7 @@ pub enum InputInstruction {
|
|||||||
//for interpolation / networking / playback reasons, most playback heads will always want
|
//for interpolation / networking / playback reasons, most playback heads will always want
|
||||||
//to be 1 instruction ahead to generate the next state for interpolation.
|
//to be 1 instruction ahead to generate the next state for interpolation.
|
||||||
}
|
}
|
||||||
|
#[derive(Clone,Debug)]
|
||||||
pub struct Body {
|
pub struct Body {
|
||||||
position: glam::Vec3,//I64 where 2^32 = 1 u
|
position: glam::Vec3,//I64 where 2^32 = 1 u
|
||||||
velocity: glam::Vec3,//I64 where 2^32 = 1 u/s
|
velocity: glam::Vec3,//I64 where 2^32 = 1 u/s
|
||||||
@ -45,20 +45,20 @@ trait MyHash{
|
|||||||
fn hash(&self) -> u64;
|
fn hash(&self) -> u64;
|
||||||
}
|
}
|
||||||
impl MyHash for Body {
|
impl MyHash for Body {
|
||||||
fn hash(&self) -> u64 {
|
fn hash(&self) -> u64 {
|
||||||
let mut hasher=std::collections::hash_map::DefaultHasher::new();
|
let mut hasher=std::collections::hash_map::DefaultHasher::new();
|
||||||
for &el in self.position.as_ref().iter() {
|
for &el in self.position.as_ref().iter() {
|
||||||
std::hash::Hasher::write(&mut hasher, el.to_ne_bytes().as_slice());
|
std::hash::Hasher::write(&mut hasher, el.to_ne_bytes().as_slice());
|
||||||
}
|
}
|
||||||
for &el in self.velocity.as_ref().iter() {
|
for &el in self.velocity.as_ref().iter() {
|
||||||
std::hash::Hasher::write(&mut hasher, el.to_ne_bytes().as_slice());
|
std::hash::Hasher::write(&mut hasher, el.to_ne_bytes().as_slice());
|
||||||
}
|
}
|
||||||
for &el in self.acceleration.as_ref().iter() {
|
for &el in self.acceleration.as_ref().iter() {
|
||||||
std::hash::Hasher::write(&mut hasher, el.to_ne_bytes().as_slice());
|
std::hash::Hasher::write(&mut hasher, el.to_ne_bytes().as_slice());
|
||||||
}
|
}
|
||||||
std::hash::Hasher::write(&mut hasher, self.time.to_ne_bytes().as_slice());
|
std::hash::Hasher::write(&mut hasher, self.time.to_ne_bytes().as_slice());
|
||||||
return std::hash::Hasher::finish(&hasher);//hash check to see if walk target is valid
|
return std::hash::Hasher::finish(&hasher);//hash check to see if walk target is valid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum MoveRestriction {
|
pub enum MoveRestriction {
|
||||||
@ -80,9 +80,9 @@ impl InputState {
|
|||||||
}
|
}
|
||||||
impl crate::instruction::InstructionEmitter<InputInstruction> for InputState{
|
impl crate::instruction::InstructionEmitter<InputInstruction> for InputState{
|
||||||
fn next_instruction(&self, time_limit:crate::body::TIME) -> Option<TimedInstruction<InputInstruction>> {
|
fn next_instruction(&self, time_limit:crate::body::TIME) -> Option<TimedInstruction<InputInstruction>> {
|
||||||
//this is polled by PhysicsState for actions like Jump
|
//this is polled by PhysicsState for actions like Jump
|
||||||
//no, it has to be the other way around. physics is run up until the jump instruction, and then the jump instruction is pushed.
|
//no, it has to be the other way around. physics is run up until the jump instruction, and then the jump instruction is pushed.
|
||||||
self.queue.get(0)
|
self.queue.get(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl crate::instruction::InstructionConsumer<InputInstruction> for InputState{
|
impl crate::instruction::InstructionConsumer<InputInstruction> for InputState{
|
||||||
@ -171,12 +171,12 @@ pub struct Camera {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn mat3_from_rotation_y_f64(angle: f64) -> glam::Mat3 {
|
fn mat3_from_rotation_y_f64(angle: f64) -> glam::Mat3 {
|
||||||
let (sina, cosa) = angle.sin_cos();
|
let (sina, cosa) = angle.sin_cos();
|
||||||
glam::Mat3::from_cols(
|
glam::Mat3::from_cols(
|
||||||
glam::Vec3::new(cosa as f32, 0.0, -sina as f32),
|
glam::Vec3::new(cosa as f32, 0.0, -sina as f32),
|
||||||
glam::Vec3::Y,
|
glam::Vec3::Y,
|
||||||
glam::Vec3::new(sina as f32, 0.0, cosa as f32),
|
glam::Vec3::new(sina as f32, 0.0, cosa as f32),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
#[inline]
|
#[inline]
|
||||||
fn perspective_rh(fov_x_slope: f32, fov_y_slope: f32, z_near: f32, z_far: f32) -> glam::Mat4 {
|
fn perspective_rh(fov_x_slope: f32, fov_y_slope: f32, z_near: f32, z_far: f32) -> glam::Mat4 {
|
||||||
@ -192,11 +192,11 @@ fn perspective_rh(fov_x_slope: f32, fov_y_slope: f32, z_near: f32, z_far: f32) -
|
|||||||
impl Camera {
|
impl Camera {
|
||||||
pub fn from_offset(offset:glam::Vec3,aspect:f32) -> Self {
|
pub fn from_offset(offset:glam::Vec3,aspect:f32) -> Self {
|
||||||
Self{
|
Self{
|
||||||
offset,
|
offset,
|
||||||
angles: glam::DVec2::ZERO,
|
angles: glam::DVec2::ZERO,
|
||||||
fov: glam::vec2(aspect,1.0),
|
fov: glam::vec2(aspect,1.0),
|
||||||
sensitivity: glam::dvec2(1.0/6144.0,1.0/6144.0),
|
sensitivity: glam::dvec2(1.0/16384.0,1.0/16384.0),
|
||||||
time: 0,
|
time: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn simulate_move_angles(&self, delta: glam::IVec2) -> glam::DVec2 {
|
fn simulate_move_angles(&self, delta: glam::IVec2) -> glam::DVec2 {
|
||||||
@ -221,13 +221,13 @@ impl Camera {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct GameMechanicsState{
|
pub struct GameMechanicsState{
|
||||||
pub spawn_id:u32,
|
pub stage_id:u32,
|
||||||
//jump_counts:HashMap<u32,u32>,
|
//jump_counts:HashMap<u32,u32>,
|
||||||
}
|
}
|
||||||
impl std::default::Default for GameMechanicsState{
|
impl std::default::Default for GameMechanicsState{
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self{
|
Self{
|
||||||
spawn_id:0,
|
stage_id:0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -317,7 +317,8 @@ pub struct PhysicsState{
|
|||||||
pub world:WorldState,//currently there is only one state the world can be in
|
pub world:WorldState,//currently there is only one state the world can be in
|
||||||
pub game:GameMechanicsState,
|
pub game:GameMechanicsState,
|
||||||
pub style:StyleModifiers,
|
pub style:StyleModifiers,
|
||||||
pub contacts:std::collections::HashSet::<RelativeCollision>,
|
pub contacts:std::collections::HashMap::<u32,RelativeCollision>,
|
||||||
|
pub intersects:std::collections::HashMap::<u32,RelativeCollision>,
|
||||||
//pub intersections: Vec<ModelId>,
|
//pub intersections: Vec<ModelId>,
|
||||||
//camera must exist in state because wormholes modify the camera, also camera punch
|
//camera must exist in state because wormholes modify the camera, also camera punch
|
||||||
pub camera:Camera,
|
pub camera:Camera,
|
||||||
@ -329,6 +330,7 @@ pub struct PhysicsState{
|
|||||||
pub models:Vec<ModelPhysics>,
|
pub models:Vec<ModelPhysics>,
|
||||||
|
|
||||||
pub modes:Vec<crate::model::ModeDescription>,
|
pub modes:Vec<crate::model::ModeDescription>,
|
||||||
|
pub mode_from_mode_id:std::collections::HashMap::<u32,usize>,
|
||||||
//the spawn point is where you spawn when you load into the map.
|
//the spawn point is where you spawn when you load into the map.
|
||||||
//This is not the same as Reset which teleports you to Spawn0
|
//This is not the same as Reset which teleports you to Spawn0
|
||||||
pub spawn_point:glam::Vec3,
|
pub spawn_point:glam::Vec3,
|
||||||
@ -458,6 +460,7 @@ pub struct ModelPhysics {
|
|||||||
//A model is a thing that has a hitbox. can be represented by a list of TreyMesh-es
|
//A model is a thing that has a hitbox. can be represented by a list of TreyMesh-es
|
||||||
//in this iteration, all it needs is extents.
|
//in this iteration, all it needs is extents.
|
||||||
mesh: TreyMesh,
|
mesh: TreyMesh,
|
||||||
|
transform:glam::Affine3A,
|
||||||
attributes:PhysicsCollisionAttributes,
|
attributes:PhysicsCollisionAttributes,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -470,13 +473,14 @@ impl ModelPhysics {
|
|||||||
Self{
|
Self{
|
||||||
mesh:aabb,
|
mesh:aabb,
|
||||||
attributes,
|
attributes,
|
||||||
|
transform:transform.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn from_model(model:&crate::model::IndexedModel,instance:&crate::model::ModelInstance) -> Option<Self> {
|
pub fn from_model(model:&crate::model::IndexedModel,instance:&crate::model::ModelInstance) -> Option<Self> {
|
||||||
match &instance.attributes{
|
match &instance.attributes{
|
||||||
crate::model::CollisionAttributes::Decoration=>None,
|
|
||||||
crate::model::CollisionAttributes::Contact{contacting,general}=>Some(ModelPhysics::from_model_transform_attributes(model,&instance.transform,PhysicsCollisionAttributes::Contact{contacting:contacting.clone(),general:general.clone()})),
|
crate::model::CollisionAttributes::Contact{contacting,general}=>Some(ModelPhysics::from_model_transform_attributes(model,&instance.transform,PhysicsCollisionAttributes::Contact{contacting:contacting.clone(),general:general.clone()})),
|
||||||
crate::model::CollisionAttributes::Intersect{intersecting,general}=>None,//Some(ModelPhysics::from_model_transform_attributes(model,&instance.transform,PhysicsCollisionAttributes::Intersecting{intersecting,general})),
|
crate::model::CollisionAttributes::Intersect{intersecting,general}=>Some(ModelPhysics::from_model_transform_attributes(model,&instance.transform,PhysicsCollisionAttributes::Intersect{intersecting:intersecting.clone(),general:general.clone()})),
|
||||||
|
crate::model::CollisionAttributes::Decoration=>None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn unit_vertices(&self) -> [glam::Vec3;8] {
|
pub fn unit_vertices(&self) -> [glam::Vec3;8] {
|
||||||
@ -556,6 +560,15 @@ impl PhysicsState {
|
|||||||
pub fn clear(&mut self){
|
pub fn clear(&mut self){
|
||||||
self.models.clear();
|
self.models.clear();
|
||||||
self.modes.clear();
|
self.modes.clear();
|
||||||
|
self.contacts.clear();
|
||||||
|
self.intersects.clear();
|
||||||
|
}
|
||||||
|
pub fn get_mode(&self,mode_id:u32)->Option<&crate::model::ModeDescription>{
|
||||||
|
if let Some(&mode)=self.mode_from_mode_id.get(&mode_id){
|
||||||
|
self.modes.get(mode)
|
||||||
|
}else{
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
//tickless gaming
|
//tickless gaming
|
||||||
pub fn run(&mut self, time_limit:TIME){
|
pub fn run(&mut self, time_limit:TIME){
|
||||||
@ -583,7 +596,7 @@ impl PhysicsState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn contact_constrain_velocity(&self,velocity:&mut glam::Vec3){
|
fn contact_constrain_velocity(&self,velocity:&mut glam::Vec3){
|
||||||
for contact in self.contacts.iter() {
|
for (_,contact) in &self.contacts {
|
||||||
let n=contact.normal(&self.models);
|
let n=contact.normal(&self.models);
|
||||||
let d=velocity.dot(n);
|
let d=velocity.dot(n);
|
||||||
if d<0f32{
|
if d<0f32{
|
||||||
@ -592,7 +605,7 @@ impl PhysicsState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn contact_constrain_acceleration(&self,acceleration:&mut glam::Vec3){
|
fn contact_constrain_acceleration(&self,acceleration:&mut glam::Vec3){
|
||||||
for contact in self.contacts.iter() {
|
for (_,contact) in &self.contacts {
|
||||||
let n=contact.normal(&self.models);
|
let n=contact.normal(&self.models);
|
||||||
let d=acceleration.dot(n);
|
let d=acceleration.dot(n);
|
||||||
if d<0f32{
|
if d<0f32{
|
||||||
@ -965,12 +978,19 @@ impl crate::instruction::InstructionEmitter<PhysicsInstruction> for PhysicsState
|
|||||||
//JUST POLLING!!! NO MUTATION
|
//JUST POLLING!!! NO MUTATION
|
||||||
let mut collector = crate::instruction::InstructionCollector::new(time_limit);
|
let mut collector = crate::instruction::InstructionCollector::new(time_limit);
|
||||||
//check for collision stop instructions with curent contacts
|
//check for collision stop instructions with curent contacts
|
||||||
for collision_data in self.contacts.iter() {
|
for (_,collision_data) in &self.contacts {
|
||||||
collector.collect(self.predict_collision_end(self.time,time_limit,collision_data));
|
collector.collect(self.predict_collision_end(self.time,time_limit,collision_data));
|
||||||
}
|
}
|
||||||
|
// for collision_data in &self.intersects{
|
||||||
|
// collector.collect(self.predict_collision_end2(self.time,time_limit,collision_data));
|
||||||
|
// }
|
||||||
//check for collision start instructions (against every part in the game with no optimization!!)
|
//check for collision start instructions (against every part in the game with no optimization!!)
|
||||||
for i in 0..self.models.len() {
|
for i in 0..self.models.len() {
|
||||||
collector.collect(self.predict_collision_start(self.time,time_limit,i as u32));
|
let i=i as u32;
|
||||||
|
if self.contacts.contains_key(&i)||self.intersects.contains_key(&i){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
collector.collect(self.predict_collision_start(self.time,time_limit,i));
|
||||||
}
|
}
|
||||||
if self.grounded {
|
if self.grounded {
|
||||||
//walk maintenance
|
//walk maintenance
|
||||||
@ -986,56 +1006,109 @@ impl crate::instruction::InstructionEmitter<PhysicsInstruction> for PhysicsState
|
|||||||
impl crate::instruction::InstructionConsumer<PhysicsInstruction> for PhysicsState {
|
impl crate::instruction::InstructionConsumer<PhysicsInstruction> for PhysicsState {
|
||||||
fn process_instruction(&mut self, ins:TimedInstruction<PhysicsInstruction>) {
|
fn process_instruction(&mut self, ins:TimedInstruction<PhysicsInstruction>) {
|
||||||
match &ins.instruction {
|
match &ins.instruction {
|
||||||
PhysicsInstruction::StrafeTick => (),
|
PhysicsInstruction::StrafeTick => (),
|
||||||
PhysicsInstruction::Input(InputInstruction::MoveMouse(_)) => (),
|
PhysicsInstruction::Input(InputInstruction::MoveMouse(_)) => (),
|
||||||
_=>println!("{:?}",ins),
|
_=>println!("{:?}",ins),
|
||||||
}
|
}
|
||||||
//selectively update body
|
//selectively update body
|
||||||
match &ins.instruction {
|
match &ins.instruction {
|
||||||
PhysicsInstruction::Input(InputInstruction::MoveMouse(_)) => (),//dodge time for mouse movement
|
PhysicsInstruction::Input(InputInstruction::MoveMouse(_)) => (),//dodge time for mouse movement
|
||||||
PhysicsInstruction::Input(_)
|
PhysicsInstruction::Input(_)
|
||||||
|PhysicsInstruction::SetSpawnPosition(_)
|
|PhysicsInstruction::SetSpawnPosition(_)
|
||||||
|PhysicsInstruction::ReachWalkTargetVelocity
|
|PhysicsInstruction::ReachWalkTargetVelocity
|
||||||
|PhysicsInstruction::CollisionStart(_)
|
|PhysicsInstruction::CollisionStart(_)
|
||||||
|PhysicsInstruction::CollisionEnd(_)
|
|PhysicsInstruction::CollisionEnd(_)
|
||||||
|PhysicsInstruction::StrafeTick => self.advance_time(ins.time),
|
|PhysicsInstruction::StrafeTick => self.advance_time(ins.time),
|
||||||
}
|
}
|
||||||
match ins.instruction {
|
match ins.instruction {
|
||||||
PhysicsInstruction::SetSpawnPosition(position)=>{
|
PhysicsInstruction::SetSpawnPosition(position)=>{
|
||||||
self.spawn_point=position;
|
self.spawn_point=position;
|
||||||
}
|
}
|
||||||
PhysicsInstruction::CollisionStart(c) => {
|
PhysicsInstruction::CollisionStart(c) => {
|
||||||
//check ground
|
let model=c.model(&self.models).unwrap();
|
||||||
match &c.face {
|
match &model.attributes{
|
||||||
AabbFace::Top => {
|
PhysicsCollisionAttributes::Contact{contacting,general}=>{
|
||||||
//ground
|
match &contacting.surf{
|
||||||
self.grounded=true;
|
Some(surf)=>println!("I'm surfing!"),
|
||||||
},
|
None=>match &c.face {
|
||||||
_ => (),
|
AabbFace::Top => {
|
||||||
}
|
//ground
|
||||||
self.contacts.insert(c);
|
self.grounded=true;
|
||||||
//flatten v
|
},
|
||||||
let mut v=self.body.velocity;
|
_ => (),
|
||||||
self.contact_constrain_velocity(&mut v);
|
},
|
||||||
self.body.velocity=v;
|
}
|
||||||
if self.grounded&&self.style.get_control(StyleModifiers::CONTROL_JUMP,self.controls){
|
match &general.booster{
|
||||||
self.jump();
|
Some(booster)=>self.body.velocity+=booster.velocity,
|
||||||
|
None=>(),
|
||||||
|
}
|
||||||
|
match &general.stage_element{
|
||||||
|
Some(stage_element)=>{
|
||||||
|
if stage_element.force||self.game.stage_id<stage_element.stage_id{
|
||||||
|
self.game.stage_id=stage_element.stage_id;
|
||||||
|
}
|
||||||
|
match stage_element.behaviour{
|
||||||
|
crate::model::StageElementBehaviour::SpawnAt=>(),
|
||||||
|
crate::model::StageElementBehaviour::Trigger
|
||||||
|
|crate::model::StageElementBehaviour::Teleport=>{
|
||||||
|
//TODO make good
|
||||||
|
if let Some(mode)=self.get_mode(stage_element.mode_id){
|
||||||
|
if let Some(&spawn)=mode.get_spawn_model_id(self.game.stage_id){
|
||||||
|
if let Some(model)=self.models.get(spawn as usize){
|
||||||
|
self.body.position=model.transform.transform_point3(glam::Vec3::Y)+glam::Vec3::Y*(self.style.hitbox_halfsize.y+0.1);
|
||||||
|
//manual clear //for c in self.contacts{process_instruction(CollisionEnd(c))}
|
||||||
|
self.contacts.clear();
|
||||||
|
self.intersects.clear();
|
||||||
|
self.body.acceleration=self.style.gravity;
|
||||||
|
self.walk.state=WalkEnum::Reached;
|
||||||
|
self.grounded=false;
|
||||||
|
}else{println!("bad1");}
|
||||||
|
}else{println!("bad2");}
|
||||||
|
}else{println!("bad3");}
|
||||||
|
},
|
||||||
|
crate::model::StageElementBehaviour::Platform=>(),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None=>(),
|
||||||
|
}
|
||||||
|
//check ground
|
||||||
|
self.contacts.insert(c.model,c);
|
||||||
|
//flatten v
|
||||||
|
let mut v=self.body.velocity;
|
||||||
|
self.contact_constrain_velocity(&mut v);
|
||||||
|
self.body.velocity=v;
|
||||||
|
if self.grounded&&self.style.get_control(StyleModifiers::CONTROL_JUMP,self.controls){
|
||||||
|
self.jump();
|
||||||
|
}
|
||||||
|
self.refresh_walk_target();
|
||||||
|
},
|
||||||
|
PhysicsCollisionAttributes::Intersect{intersecting,general}=>{
|
||||||
|
//I think that setting the velocity to 0 was preventing surface contacts from entering an infinite loop
|
||||||
|
self.intersects.insert(c.model,c);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
self.refresh_walk_target();
|
|
||||||
},
|
},
|
||||||
PhysicsInstruction::CollisionEnd(c) => {
|
PhysicsInstruction::CollisionEnd(c) => {
|
||||||
self.contacts.remove(&c);//remove contact before calling contact_constrain_acceleration
|
let model=c.model(&self.models).unwrap();
|
||||||
let mut a=self.style.gravity;
|
match &model.attributes{
|
||||||
self.contact_constrain_acceleration(&mut a);
|
PhysicsCollisionAttributes::Contact{contacting,general}=>{
|
||||||
self.body.acceleration=a;
|
self.contacts.remove(&c.model);//remove contact before calling contact_constrain_acceleration
|
||||||
//check ground
|
let mut a=self.style.gravity;
|
||||||
match &c.face {
|
self.contact_constrain_acceleration(&mut a);
|
||||||
AabbFace::Top => {
|
self.body.acceleration=a;
|
||||||
self.grounded=false;
|
//check ground
|
||||||
},
|
match &c.face {
|
||||||
_ => (),
|
AabbFace::Top => {
|
||||||
}
|
self.grounded=false;
|
||||||
self.refresh_walk_target();
|
},
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
self.refresh_walk_target();
|
||||||
|
},
|
||||||
|
PhysicsCollisionAttributes::Intersect{intersecting,general}=>{
|
||||||
|
self.intersects.remove(&c.model);
|
||||||
|
},
|
||||||
|
}
|
||||||
},
|
},
|
||||||
PhysicsInstruction::StrafeTick => {
|
PhysicsInstruction::StrafeTick => {
|
||||||
let camera_mat=self.camera.simulate_move_rotation_y(self.mouse_interpolation.interpolated_position(self.time).x-self.mouse_interpolation.mouse0.x);
|
let camera_mat=self.camera.simulate_move_rotation_y(self.mouse_interpolation.interpolated_position(self.time).x-self.mouse_interpolation.mouse0.x);
|
||||||
|
@ -30,7 +30,7 @@ fn get_texture_refs(dom:&rbx_dom_weak::WeakDom) -> Vec<rbx_dom_weak::types::Ref>
|
|||||||
//next class
|
//next class
|
||||||
objects
|
objects
|
||||||
}
|
}
|
||||||
fn get_attributes(name:&str,can_collide:bool,velocity:glam::Vec3)->crate::model::CollisionAttributes{
|
fn get_attributes(name:&str,can_collide:bool,velocity:glam::Vec3,force_intersecting:bool)->crate::model::CollisionAttributes{
|
||||||
let mut general=crate::model::GameMechanicAttributes::default();
|
let mut general=crate::model::GameMechanicAttributes::default();
|
||||||
let mut intersecting=crate::model::IntersectingAttributes::default();
|
let mut intersecting=crate::model::IntersectingAttributes::default();
|
||||||
let mut contacting=crate::model::ContactingAttributes::default();
|
let mut contacting=crate::model::ContactingAttributes::default();
|
||||||
@ -46,7 +46,7 @@ fn get_attributes(name:&str,can_collide:bool,velocity:glam::Vec3)->crate::model:
|
|||||||
behaviour:crate::model::StageElementBehaviour::Platform,
|
behaviour:crate::model::StageElementBehaviour::Platform,
|
||||||
}),
|
}),
|
||||||
other=>{
|
other=>{
|
||||||
if let Some(captures)=lazy_regex::regex!(r"^(Force)?(SpawnAt|Trigger|Teleport|Platform)(\d+)$")
|
if let Some(captures)=lazy_regex::regex!(r"^(Force)?(Spawn|SpawnAt|Trigger|Teleport|Platform)(\d+)$")
|
||||||
.captures(other){
|
.captures(other){
|
||||||
general.stage_element=Some(crate::model::GameMechanicStageElement{
|
general.stage_element=Some(crate::model::GameMechanicStageElement{
|
||||||
mode_id:0,
|
mode_id:0,
|
||||||
@ -56,7 +56,7 @@ fn get_attributes(name:&str,can_collide:bool,velocity:glam::Vec3)->crate::model:
|
|||||||
None=>false,
|
None=>false,
|
||||||
},
|
},
|
||||||
behaviour:match &captures[2]{
|
behaviour:match &captures[2]{
|
||||||
"SpawnAt"=>crate::model::StageElementBehaviour::SpawnAt,
|
"Spawn"|"SpawnAt"=>crate::model::StageElementBehaviour::SpawnAt,
|
||||||
"Trigger"=>crate::model::StageElementBehaviour::Trigger,
|
"Trigger"=>crate::model::StageElementBehaviour::Trigger,
|
||||||
"Teleport"=>crate::model::StageElementBehaviour::Teleport,
|
"Teleport"=>crate::model::StageElementBehaviour::Teleport,
|
||||||
"Platform"=>crate::model::StageElementBehaviour::Platform,
|
"Platform"=>crate::model::StageElementBehaviour::Platform,
|
||||||
@ -89,9 +89,21 @@ fn get_attributes(name:&str,can_collide:bool,velocity:glam::Vec3)->crate::model:
|
|||||||
//WormholeIn#
|
//WormholeIn#
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return crate::model::CollisionAttributes::Contact{contacting,general};
|
crate::model::CollisionAttributes::Contact{contacting,general}
|
||||||
|
},
|
||||||
|
false=>if force_intersecting
|
||||||
|
||general.jump_limit.is_some()
|
||||||
|
||general.booster.is_some()
|
||||||
|
||general.zone.is_some()
|
||||||
|
||general.stage_element.is_some()
|
||||||
|
||general.wormhole.is_some()
|
||||||
|
||intersecting.water.is_some()
|
||||||
|
||intersecting.accelerator.is_some()
|
||||||
|
{
|
||||||
|
crate::model::CollisionAttributes::Intersect{intersecting,general}
|
||||||
|
}else{
|
||||||
|
crate::model::CollisionAttributes::Decoration
|
||||||
},
|
},
|
||||||
false=>return crate::model::CollisionAttributes::Decoration,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,6 +231,7 @@ pub fn generate_indexed_models(dom:rbx_dom_weak::WeakDom) -> crate::model::Index
|
|||||||
);
|
);
|
||||||
|
|
||||||
//push TempIndexedAttributes
|
//push TempIndexedAttributes
|
||||||
|
let mut force_intersecting=false;
|
||||||
let mut temp_indexing_attributes=Vec::new();
|
let mut temp_indexing_attributes=Vec::new();
|
||||||
if let Some(attr)=match &object.name[..]{
|
if let Some(attr)=match &object.name[..]{
|
||||||
"MapStart"=>{
|
"MapStart"=>{
|
||||||
@ -227,11 +240,11 @@ pub fn generate_indexed_models(dom:rbx_dom_weak::WeakDom) -> crate::model::Index
|
|||||||
},
|
},
|
||||||
"UnorderedCheckpoint"=>Some(crate::model::TempIndexedAttributes::UnorderedCheckpoint{mode_id:0}),
|
"UnorderedCheckpoint"=>Some(crate::model::TempIndexedAttributes::UnorderedCheckpoint{mode_id:0}),
|
||||||
other=>{
|
other=>{
|
||||||
let regman=lazy_regex::regex!(r"^(BonusStart|Spawn|OrderedCheckpoint)(\d+)$");
|
let regman=lazy_regex::regex!(r"^(BonusStart|Spawn|ForceSpawn|OrderedCheckpoint)(\d+)$");
|
||||||
if let Some(captures) = regman.captures(other) {
|
if let Some(captures) = regman.captures(other) {
|
||||||
match &captures[1]{
|
match &captures[1]{
|
||||||
"BonusStart"=>Some(crate::model::TempIndexedAttributes::Start{mode_id:captures[2].parse::<u32>().unwrap()}),
|
"BonusStart"=>Some(crate::model::TempIndexedAttributes::Start{mode_id:captures[2].parse::<u32>().unwrap()}),
|
||||||
"Spawn"=>Some(crate::model::TempIndexedAttributes::Spawn{mode_id:0,stage_id:captures[2].parse::<u32>().unwrap()}),
|
"Spawn"|"ForceSpawn"=>Some(crate::model::TempIndexedAttributes::Spawn{mode_id:0,stage_id:captures[2].parse::<u32>().unwrap()}),
|
||||||
"OrderedCheckpoint"=>Some(crate::model::TempIndexedAttributes::OrderedCheckpoint{mode_id:0,checkpoint_id:captures[2].parse::<u32>().unwrap()}),
|
"OrderedCheckpoint"=>Some(crate::model::TempIndexedAttributes::OrderedCheckpoint{mode_id:0,checkpoint_id:captures[2].parse::<u32>().unwrap()}),
|
||||||
_=>None,
|
_=>None,
|
||||||
}
|
}
|
||||||
@ -240,6 +253,7 @@ pub fn generate_indexed_models(dom:rbx_dom_weak::WeakDom) -> crate::model::Index
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}{
|
}{
|
||||||
|
force_intersecting=true;
|
||||||
temp_indexing_attributes.push(attr);
|
temp_indexing_attributes.push(attr);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -354,17 +368,17 @@ pub fn generate_indexed_models(dom:rbx_dom_weak::WeakDom) -> crate::model::Index
|
|||||||
primitives::Primitives::Cylinder=>RobloxBasePartDescription::Cylinder,
|
primitives::Primitives::Cylinder=>RobloxBasePartDescription::Cylinder,
|
||||||
//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,//Wedge::Right
|
f0,//Cube::Right->Wedge::Right
|
||||||
if f5.is_some(){f5}else{f1},//Wedge::TopFront
|
if f5.is_some(){f5}else{f1},//Cube::Front|Cube::Top->Wedge::TopFront
|
||||||
f2,//Wedge::Back
|
f2,//Cube::Back->Wedge::Back
|
||||||
f3,//Wedge::Left
|
f3,//Cube::Left->Wedge::Left
|
||||||
f4,//Wedge::Bottom
|
f4,//Cube::Bottom->Wedge::Bottom
|
||||||
]),
|
]),
|
||||||
primitives::Primitives::CornerWedge=>RobloxBasePartDescription::CornerWedge([
|
primitives::Primitives::CornerWedge=>RobloxBasePartDescription::CornerWedge([
|
||||||
f0,//CornerWedge::Right
|
f0,//Cube::Right->CornerWedge::Right
|
||||||
f1,//CornerWedge::Top
|
f1,//Cube::Top->CornerWedge::Top
|
||||||
f4,//CornerWedge::Bottom
|
f4,//Cube::Bottom->CornerWedge::Bottom
|
||||||
f5,//CornerWedge::Front
|
f5,//Cube::Front->CornerWedge::Front
|
||||||
]),
|
]),
|
||||||
};
|
};
|
||||||
//make new model if unit cube has not been created before
|
//make new model if unit cube has not been created before
|
||||||
@ -440,7 +454,7 @@ pub fn generate_indexed_models(dom:rbx_dom_weak::WeakDom) -> crate::model::Index
|
|||||||
indexed_models[model_id].instances.push(crate::model::ModelInstance {
|
indexed_models[model_id].instances.push(crate::model::ModelInstance {
|
||||||
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),
|
||||||
attributes:get_attributes(&object.name,*can_collide,glam::vec3(velocity.x,velocity.y,velocity.z)),
|
attributes:get_attributes(&object.name,*can_collide,glam::vec3(velocity.x,velocity.y,velocity.z),force_intersecting),
|
||||||
temp_indexing:temp_indexing_attributes,
|
temp_indexing:temp_indexing_attributes,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
26
src/main.rs
26
src/main.rs
@ -11,6 +11,7 @@ mod framework;
|
|||||||
mod primitives;
|
mod primitives;
|
||||||
mod instruction;
|
mod instruction;
|
||||||
mod load_roblox;
|
mod load_roblox;
|
||||||
|
mod worker;
|
||||||
|
|
||||||
struct Entity {
|
struct Entity {
|
||||||
index_count: u32,
|
index_count: u32,
|
||||||
@ -104,8 +105,8 @@ impl GlobalState{
|
|||||||
for model_instance in &model.instances{
|
for model_instance in &model.instances{
|
||||||
if let Some(model_physics)=body::ModelPhysics::from_model(model,model_instance){
|
if let Some(model_physics)=body::ModelPhysics::from_model(model,model_instance){
|
||||||
let model_id=self.physics.models.len() as u32;
|
let model_id=self.physics.models.len() as u32;
|
||||||
//snoop it before it gets stolen
|
self.physics.models.push(model_physics);
|
||||||
for attr in model_instance.temp_indexing.iter(){
|
for attr in &model_instance.temp_indexing{
|
||||||
match attr{
|
match attr{
|
||||||
model::TempIndexedAttributes::Start{mode_id}=>starts.push((*mode_id,model_id)),
|
model::TempIndexedAttributes::Start{mode_id}=>starts.push((*mode_id,model_id)),
|
||||||
model::TempIndexedAttributes::Spawn{mode_id,stage_id}=>spawns.push((*mode_id,model_id,*stage_id)),
|
model::TempIndexedAttributes::Spawn{mode_id,stage_id}=>spawns.push((*mode_id,model_id,*stage_id)),
|
||||||
@ -113,8 +114,6 @@ impl GlobalState{
|
|||||||
model::TempIndexedAttributes::UnorderedCheckpoint{mode_id}=>unordered_checkpoints.push((*mode_id,model_id)),
|
model::TempIndexedAttributes::UnorderedCheckpoint{mode_id}=>unordered_checkpoints.push((*mode_id,model_id)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//steal it
|
|
||||||
self.physics.models.push(model_physics);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -147,14 +146,22 @@ impl GlobalState{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let num_modes=self.physics.modes.len();
|
||||||
|
for (mode_id,mode) in eshmep{
|
||||||
|
self.physics.mode_from_mode_id.insert(mode_id,num_modes+mode);
|
||||||
|
}
|
||||||
self.physics.modes.append(&mut modedatas.into_iter().map(|mut tup|{
|
self.physics.modes.append(&mut modedatas.into_iter().map(|mut tup|{
|
||||||
tup.1.sort_by_key(|tup|tup.0);
|
tup.1.sort_by_key(|tup|tup.0);
|
||||||
tup.2.sort_by_key(|tup|tup.0);
|
tup.2.sort_by_key(|tup|tup.0);
|
||||||
|
let mut eshmep1=std::collections::HashMap::new();
|
||||||
|
let mut eshmep2=std::collections::HashMap::new();
|
||||||
model::ModeDescription{
|
model::ModeDescription{
|
||||||
start:tup.0,
|
start:tup.0,
|
||||||
spawns:tup.1.into_iter().map(|tup|tup.1).collect(),
|
spawns:tup.1.into_iter().enumerate().map(|(i,tup)|{eshmep1.insert(tup.0,i);tup.1}).collect(),
|
||||||
ordered_checkpoints:tup.2.into_iter().map(|tup|tup.1).collect(),
|
ordered_checkpoints:tup.2.into_iter().enumerate().map(|(i,tup)|{eshmep2.insert(tup.0,i);tup.1}).collect(),
|
||||||
unordered_checkpoints:tup.3,
|
unordered_checkpoints:tup.3,
|
||||||
|
spawn_from_stage_id:eshmep1,
|
||||||
|
ordered_checkpoint_from_checkpoint_id:eshmep2,
|
||||||
}
|
}
|
||||||
}).collect());
|
}).collect());
|
||||||
println!("Physics Objects: {}",self.physics.models.len());
|
println!("Physics Objects: {}",self.physics.models.len());
|
||||||
@ -228,7 +235,7 @@ impl GlobalState{
|
|||||||
//the models received here are supposed to be tightly packed, i.e. no code needs to check if two models are using the same groups.
|
//the models received here are supposed to be tightly packed, i.e. no code needs to check if two models are using the same groups.
|
||||||
let indexed_models_len=indexed_models.models.len();
|
let indexed_models_len=indexed_models.models.len();
|
||||||
let mut unique_texture_models=Vec::with_capacity(indexed_models_len);
|
let mut unique_texture_models=Vec::with_capacity(indexed_models_len);
|
||||||
for mut model in indexed_models.models.into_iter(){
|
for model in indexed_models.models.into_iter(){
|
||||||
//convert ModelInstance into ModelGraphicsInstance
|
//convert ModelInstance into ModelGraphicsInstance
|
||||||
let instances:Vec<ModelGraphicsInstance>=model.instances.into_iter().filter_map(|instance|{
|
let instances:Vec<ModelGraphicsInstance>=model.instances.into_iter().filter_map(|instance|{
|
||||||
if instance.color.w==0.0{
|
if instance.color.w==0.0{
|
||||||
@ -581,7 +588,8 @@ impl framework::Example for GlobalState {
|
|||||||
time: 0,
|
time: 0,
|
||||||
style:body::StyleModifiers::default(),
|
style:body::StyleModifiers::default(),
|
||||||
grounded: false,
|
grounded: false,
|
||||||
contacts: std::collections::HashSet::new(),
|
contacts: std::collections::HashMap::new(),
|
||||||
|
intersects: std::collections::HashMap::new(),
|
||||||
models: Vec::new(),
|
models: Vec::new(),
|
||||||
walk: body::WalkState::new(),
|
walk: body::WalkState::new(),
|
||||||
camera: body::Camera::from_offset(glam::vec3(0.0,4.5-2.5,0.0),(config.width as f32)/(config.height as f32)),
|
camera: body::Camera::from_offset(glam::vec3(0.0,4.5-2.5,0.0),(config.width as f32)/(config.height as f32)),
|
||||||
@ -590,6 +598,7 @@ impl framework::Example for GlobalState {
|
|||||||
world:body::WorldState{},
|
world:body::WorldState{},
|
||||||
game:body::GameMechanicsState::default(),
|
game:body::GameMechanicsState::default(),
|
||||||
modes:Vec::new(),
|
modes:Vec::new(),
|
||||||
|
mode_from_mode_id:std::collections::HashMap::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
//load textures
|
//load textures
|
||||||
@ -904,6 +913,7 @@ impl framework::Example for GlobalState {
|
|||||||
//if generate_indexed_models succeeds, clear the previous ones
|
//if generate_indexed_models succeeds, clear the previous ones
|
||||||
self.physics.clear();
|
self.physics.clear();
|
||||||
self.graphics.clear();
|
self.graphics.clear();
|
||||||
|
self.physics.game.stage_id=0;
|
||||||
self.generate_model_physics(&indexed_model_instances);
|
self.generate_model_physics(&indexed_model_instances);
|
||||||
self.generate_model_graphics(device,queue,indexed_model_instances);
|
self.generate_model_graphics(device,queue,indexed_model_instances);
|
||||||
//manual reset
|
//manual reset
|
||||||
|
19
src/model.rs
19
src/model.rs
@ -85,7 +85,26 @@ pub struct ModeDescription{
|
|||||||
pub spawns:Vec<u32>,//spawns[spawn_id]=model_id
|
pub spawns:Vec<u32>,//spawns[spawn_id]=model_id
|
||||||
pub ordered_checkpoints:Vec<u32>,//ordered_checkpoints[checkpoint_id]=model_id
|
pub ordered_checkpoints:Vec<u32>,//ordered_checkpoints[checkpoint_id]=model_id
|
||||||
pub unordered_checkpoints:Vec<u32>,//unordered_checkpoints[checkpoint_id]=model_id
|
pub unordered_checkpoints:Vec<u32>,//unordered_checkpoints[checkpoint_id]=model_id
|
||||||
|
pub spawn_from_stage_id:std::collections::HashMap::<u32,usize>,
|
||||||
|
pub ordered_checkpoint_from_checkpoint_id:std::collections::HashMap::<u32,usize>,
|
||||||
}
|
}
|
||||||
|
impl ModeDescription{
|
||||||
|
pub fn get_spawn_model_id(&self,stage_id:u32)->Option<&u32>{
|
||||||
|
if let Some(&spawn)=self.spawn_from_stage_id.get(&stage_id){
|
||||||
|
self.spawns.get(spawn)
|
||||||
|
}else{
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn get_ordered_checkpoint_model_id(&self,checkpoint_id:u32)->Option<&u32>{
|
||||||
|
if let Some(&checkpoint)=self.ordered_checkpoint_from_checkpoint_id.get(&checkpoint_id){
|
||||||
|
self.ordered_checkpoints.get(checkpoint)
|
||||||
|
}else{
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum TempIndexedAttributes{
|
pub enum TempIndexedAttributes{
|
||||||
Start{
|
Start{
|
||||||
mode_id:u32,
|
mode_id:u32,
|
||||||
|
83
src/worker.rs
Normal file
83
src/worker.rs
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
use std::thread;
|
||||||
|
use std::sync::{mpsc,Arc};
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
|
||||||
|
//The goal here is to have a worker thread that parks itself when it runs out of work.
|
||||||
|
//The worker thread publishes the result of its work back to the worker object for every item in the work queue.
|
||||||
|
//The physics (target use case) knows when it has not changed the body, so not updating the value is also an option.
|
||||||
|
|
||||||
|
struct Worker<Task:Send,Value:Clone> {
|
||||||
|
sender: mpsc::Sender<Task>,
|
||||||
|
value:Arc<Mutex<Value>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Task:Send+'static,Value:Clone+Send+'static> Worker<Task,Value> {
|
||||||
|
fn new<F:Fn(Task)->Value+Send+'static>(value:Value,f:F) -> Self {
|
||||||
|
let (sender, receiver) = mpsc::channel::<Task>();
|
||||||
|
let ret=Self {
|
||||||
|
sender,
|
||||||
|
value:Arc::new(Mutex::new(value)),
|
||||||
|
};
|
||||||
|
let value=ret.value.clone();
|
||||||
|
thread::spawn(move || {
|
||||||
|
loop {
|
||||||
|
match receiver.recv() {
|
||||||
|
Ok(task) => {
|
||||||
|
println!("Worker got a task");
|
||||||
|
// Process the task
|
||||||
|
*value.lock()=f(task);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
println!("Worker stopping.",);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send(&self,task:Task)->Result<(), mpsc::SendError<Task>>{
|
||||||
|
self.sender.send(task)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn grab_clone(&self)->Value{
|
||||||
|
self.value.lock().clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]//How to run this test with printing: cargo test --release -- --nocapture
|
||||||
|
fn test_worker() {
|
||||||
|
println!("hiiiii");
|
||||||
|
// Create the worker thread
|
||||||
|
let worker = Worker::new(crate::body::Body::with_pva(glam::Vec3::ZERO,glam::Vec3::ZERO,glam::Vec3::ZERO),
|
||||||
|
|_|crate::body::Body::with_pva(glam::Vec3::ONE,glam::Vec3::ONE,glam::Vec3::ONE)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Send tasks to the worker
|
||||||
|
for i in 0..5 {
|
||||||
|
let task = crate::instruction::TimedInstruction{
|
||||||
|
time:0,
|
||||||
|
instruction:crate::body::PhysicsInstruction::StrafeTick,
|
||||||
|
};
|
||||||
|
worker.send(task).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: Signal the worker to stop (in a real-world scenario)
|
||||||
|
// sender.send("STOP".to_string()).unwrap();
|
||||||
|
|
||||||
|
// Sleep to allow the worker thread to finish processing
|
||||||
|
thread::sleep(std::time::Duration::from_secs(2));
|
||||||
|
|
||||||
|
// Send a new task
|
||||||
|
let task = crate::instruction::TimedInstruction{
|
||||||
|
time:0,
|
||||||
|
instruction:crate::body::PhysicsInstruction::StrafeTick,
|
||||||
|
};
|
||||||
|
worker.send(task).unwrap();
|
||||||
|
|
||||||
|
println!("value={:?}",worker.grab_clone());
|
||||||
|
|
||||||
|
// wait long enough to see print from final task
|
||||||
|
thread::sleep(std::time::Duration::from_secs(1));
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user