diff --git a/Cargo.toml b/Cargo.toml index c2158761..43380b43 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,9 @@ inherits = 'release' lto = 'fat' panic = 'abort' +[profile.dev] +incremental = true + [workspace] members = [ 'crates/bvh-region', diff --git a/crates/geometry/src/ray.rs b/crates/geometry/src/ray.rs index 1dc9b326..4b7b7d00 100644 --- a/crates/geometry/src/ray.rs +++ b/crates/geometry/src/ray.rs @@ -46,16 +46,20 @@ impl Ray { /// Returns an iterator over the grid cells ([`IVec3`]) that the ray passes through. pub fn voxel_traversal(&self, bounds_min: IVec3, bounds_max: IVec3) -> VoxelTraversal { // Convert ray origin to grid coordinates and handle negative coordinates correctly - #[allow(clippy::cast_possible_truncation)] - let current_pos = if self.origin.x < 0.0 || self.origin.y < 0.0 || self.origin.z < 0.0 { - IVec3::new( - self.origin.x.floor() as i32, - self.origin.y.floor() as i32, - self.origin.z.floor() as i32, - ) - } else { - self.origin.as_ivec3() - }; + let current_pos = self.origin.as_ivec3(); + + // Ensure that the traversal includes the current block + let min_block = IVec3::new( + current_pos.x.min(bounds_min.x), + current_pos.y.min(bounds_min.y), + current_pos.z.min(bounds_min.z), + ); + + let max_block = IVec3::new( + current_pos.x.max(bounds_max.x), + current_pos.y.max(bounds_max.y), + current_pos.z.max(bounds_max.z), + ); // Calculate step direction for each axis let step = IVec3::new( @@ -140,8 +144,8 @@ impl Ray { step, t_max, t_delta, - bounds_min, - bounds_max, + bounds_min: min_block, + bounds_max: max_block, } } } diff --git a/crates/hyperion/src/egress/sync_entity_state.rs b/crates/hyperion/src/egress/sync_entity_state.rs index bf801636..8b2cbf76 100644 --- a/crates/hyperion/src/egress/sync_entity_state.rs +++ b/crates/hyperion/src/egress/sync_entity_state.rs @@ -6,14 +6,12 @@ use glam::Vec3; use hyperion_inventory::PlayerInventory; use hyperion_utils::EntityExt; use tracing::{error, info_span}; -use valence_protocol::{ByteAngle, RawBytes, VarInt, packets::play}; +use valence_protocol::{RawBytes, VarInt, packets::play}; use crate::{ Prev, net::{Compose, ConnectionId}, - simulation::{ - Pitch, Position, Velocity, Xp, Yaw, animation::ActiveAnimation, metadata::MetadataChanges, - }, + simulation::{Position, Velocity, Xp, animation::ActiveAnimation, metadata::MetadataChanges}, system_registry::{SYNC_ENTITY_POSITION, SystemId}, util::TracingExt, }; @@ -224,96 +222,59 @@ impl Module for EntityStateSyncModule { }, ); + // Add a new system specifically for projectiles (arrows) system!( - "entity_velocity_sync", - world, - &Compose($), - &Velocity, - ) - .multi_threaded() - .kind::() - .tracing_each_entity( - info_span!("entity_velocity_sync"), - move |entity, (compose, velocity)| { - let run = || { - let entity_id = VarInt(entity.minecraft_id()); - let world = entity.world(); - - if velocity.velocity != Vec3::ZERO { - let pkt = play::EntityVelocityUpdateS2c { - entity_id, - velocity: (*velocity).try_into()?, - }; - - compose.broadcast(&pkt, system_id).send(&world)?; - } - - anyhow::Ok(()) - }; - - if let Err(e) = run() { - error!("failed to run velocity sync: {e}"); - } - }, - ); - - system!( - "entity_state_sync", + "projectile_sync", world, &Compose($), &Position, - &Yaw, - &Pitch, - ?&ConnectionId, + &(Prev, Position), + &mut Velocity, ) .multi_threaded() - .kind::() + .kind::() .tracing_each_entity( - info_span!("entity_state_sync"), - move |entity, (compose, position, yaw, pitch, io)| { - let run = || { - let entity_id = VarInt(entity.minecraft_id()); - - let io = io.copied(); - - let world = entity.world(); + info_span!("projectile_sync"), + move |entity, (compose, position, previous_position, velocity)| { + let entity_id = VarInt(entity.minecraft_id()); + let world = entity.world(); + let chunk_pos = position.to_chunk(); - let chunk_pos = position.to_chunk(); + let position_delta = **position - **previous_position; + let needs_teleport = position_delta.abs().max_element() >= 8.0; + let changed_position = **position != **previous_position; - let pkt = play::EntityPositionS2c { + if changed_position && !needs_teleport { + let pkt = play::MoveRelativeS2c { entity_id, - position: position.as_dvec3(), - yaw: ByteAngle::from_degrees(**yaw), - pitch: ByteAngle::from_degrees(**pitch), + #[allow(clippy::cast_possible_truncation)] + delta: (position_delta * 4096.0).to_array().map(|x| x as i16), on_ground: false, }; compose .broadcast_local(&pkt, chunk_pos, system_id) - .exclude(io) - .send(&world)?; + .send(&world) + .unwrap(); + } - // todo: unsure if we always want to set this - let pkt = play::EntitySetHeadYawS2c { + // Sync velocity if non-zero + if velocity.velocity != Vec3::ZERO { + let pkt = play::EntityVelocityUpdateS2c { entity_id, - head_yaw: ByteAngle::from_degrees(**yaw), + velocity: (*velocity).try_into().unwrap_or_else(|_| { + Velocity::ZERO + .try_into() + .expect("failed to convert velocity to i16") + }), }; - compose - .broadcast(&pkt, system_id) - .exclude(io) - .send(&world)?; - - anyhow::Ok(()) - }; - if let Err(e) = run() { - error!("failed to run sync_position: {e}"); + compose.broadcast(&pkt, system_id).send(&world).unwrap(); + // velocity.velocity = Vec3::ZERO; } }, ); track_previous::(world); - track_previous::(world); - track_previous::(world); } } diff --git a/crates/hyperion/src/simulation/blocks/mod.rs b/crates/hyperion/src/simulation/blocks/mod.rs index 49e3ade3..f13b5e75 100644 --- a/crates/hyperion/src/simulation/blocks/mod.rs +++ b/crates/hyperion/src/simulation/blocks/mod.rs @@ -58,6 +58,7 @@ pub enum TrySetBlockDeltaError { pub struct RayCollision { pub distance: f32, pub location: IVec3, + pub normal: Vec3, pub block: BlockState, } @@ -133,12 +134,16 @@ impl Blocks { continue; }; + let collision_point = ray.origin() + ray.direction() * min_dist.into_inner(); + let collision_normal = (collision_point - origin).normalize(); + match &min { Some(current_min) => { if min_dist.into_inner() < current_min.distance { min = Some(RayCollision { distance: min_dist.into_inner(), location: cell, + normal: collision_normal, block, }); } @@ -147,6 +152,7 @@ impl Blocks { min = Some(RayCollision { distance: min_dist.into_inner(), location: cell, + normal: collision_normal, block, }); } diff --git a/crates/hyperion/src/simulation/mod.rs b/crates/hyperion/src/simulation/mod.rs index 59570fac..f736acac 100644 --- a/crates/hyperion/src/simulation/mod.rs +++ b/crates/hyperion/src/simulation/mod.rs @@ -1,5 +1,7 @@ use std::{borrow::Borrow, collections::HashMap, hash::Hash, num::TryFromIntError, sync::Arc}; +use anyhow::Context; +use blocks::Blocks; use bytemuck::{Pod, Zeroable}; use derive_more::{Constructor, Deref, DerefMut, Display, From}; use flecs_ecs::prelude::*; @@ -9,7 +11,7 @@ use hyperion_utils::EntityExt; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use skin::PlayerSkin; -use tracing::debug; +use tracing::{debug, error}; use uuid; use valence_generated::block::BlockState; use valence_protocol::{ByteAngle, VarInt, packets::play}; @@ -556,8 +558,8 @@ impl Velocity { velocity: Vec3::ZERO, }; - #[allow(unused)] - const fn new(x: f32, y: f32, z: f32) -> Self { + #[must_use] + pub const fn new(x: f32, y: f32, z: f32) -> Self { Self { velocity: Vec3::new(x, y, z), } @@ -568,17 +570,16 @@ impl TryFrom<&Velocity> for valence_protocol::Velocity { type Error = TryFromIntError; fn try_from(value: &Velocity) -> Result { - let nums = value + let max_velocity = 75.0; + let clamped_velocity = value .velocity - .to_array() - .try_map(|a| { - #[expect( - clippy::cast_possible_truncation, - reason = "https://blog.rust-lang.org/2020/07/16/Rust-1.45.0.html#:~:text=as%20would%20perform%20a%20%22saturating%20cast%22 as is saturating." - )] - let num = (a * 8000.0) as i32; - i16::try_from(num) - })?; + .clamp(Vec3::splat(-max_velocity), Vec3::splat(max_velocity)); + + let nums = clamped_velocity.to_array().try_map(|a| { + #[allow(clippy::cast_possible_truncation)] + let num = (a * 8000.0) as i32; + i16::try_from(num) + })?; Ok(Self(nums)) } @@ -677,12 +678,14 @@ impl Module for SimModule { .with::() .with_enum_wildcard::() .each_entity(|entity, (compose, uuid, position, pitch, yaw, velocity)| { - debug!("spawned entity"); let minecraft_id = entity.minecraft_id(); let world = entity.world(); - entity.get::<&EntityKind>(|kind| { - let kind = *kind as i32; + let spawn_entity = move |kind: EntityKind| -> anyhow::Result<()> { + let kind = kind as i32; + + let velocity = valence_protocol::Velocity::try_from(velocity) + .context("failed to convert velocity")?; let packet = play::EntitySpawnS2c { entity_id: VarInt(minecraft_id), @@ -691,15 +694,25 @@ impl Module for SimModule { position: position.as_dvec3(), pitch: ByteAngle::from_degrees(**pitch), yaw: ByteAngle::from_degrees(**yaw), - head_yaw: ByteAngle(0), // todo: - data: VarInt::default(), // todo: - velocity: velocity.try_into().unwrap(), + head_yaw: ByteAngle::from_degrees(0.0), // todo: + data: VarInt::default(), // todo: + velocity, }; compose .broadcast(&packet, SystemId(0)) .send(&world) .unwrap(); + + Ok(()) + }; + + debug!("spawned entity"); + + entity.get::<&EntityKind>(|kind| { + if let Err(e) = spawn_entity(*kind) { + error!("failed to spawn entity: {e}"); + } }); }); @@ -717,6 +730,62 @@ impl Module for SimModule { _ => {} }); }); + + system!( + "update_projectile_positions", + world, + &mut Position, + &mut Yaw, + &mut Pitch, + &mut Velocity, + ) + .kind::() + .with_enum_wildcard::() + .each_entity(|entity, (position, yaw, pitch, velocity)| { + if velocity.velocity != Vec3::ZERO { + // Update position based on velocity with delta time + position.x += velocity.velocity.x; + position.y += velocity.velocity.y; + position.z += velocity.velocity.z; + + // re calculate yaw and pitch based on velocity + let (new_yaw, new_pitch) = get_rotation_from_velocity(velocity.velocity); + *yaw = Yaw::new(new_yaw); + *pitch = Pitch::new(new_pitch); + + let ray = entity.get::<(&Position, &Yaw, &Pitch)>(|(position, yaw, pitch)| { + let center = **position; + + let direction = get_direction_from_rotation(**yaw, **pitch); + + geometry::ray::Ray::new(center, direction) + }); + + entity.world().get::<&mut Blocks>(|blocks| { + // calculate distance limit based on velocity + let distance_limit = velocity.velocity.length(); + let Some(collision) = blocks.first_collision(ray, distance_limit) else { + velocity.velocity.x *= 0.99; + velocity.velocity.z *= 0.99; + + velocity.velocity.y -= 0.005; + return; + }; + debug!("distance_limit = {}", distance_limit); + + debug!("collision = {collision:?}"); + + velocity.velocity = Vec3::ZERO; + + // Set arrow position to the collision location + **position = collision.normal; + + blocks + .set_block(collision.location, BlockState::DIRT) + .unwrap(); + }); + } + }); } } @@ -725,3 +794,23 @@ pub struct Spawn; #[derive(Component)] pub struct Visible; + +#[must_use] +pub fn get_rotation_from_velocity(velocity: Vec3) -> (f32, f32) { + let yaw = (-velocity.x).atan2(velocity.z).to_degrees(); // Correct yaw calculation + let pitch = (-velocity.y).atan2(velocity.length()).to_degrees(); // Correct pitch calculation + (yaw, pitch) +} + +#[must_use] +pub fn get_direction_from_rotation(yaw: f32, pitch: f32) -> Vec3 { + // Convert angles from degrees to radians + let yaw_rad = yaw.to_radians(); + let pitch_rad = pitch.to_radians(); + + Vec3::new( + -pitch_rad.cos() * yaw_rad.sin(), // x = -cos(pitch) * sin(yaw) + -pitch_rad.sin(), // y = -sin(pitch) + pitch_rad.cos() * yaw_rad.cos(), // z = cos(pitch) * cos(yaw) + ) +} diff --git a/events/tag/src/command.rs b/events/tag/src/command.rs index 1f736705..df01c08b 100644 --- a/events/tag/src/command.rs +++ b/events/tag/src/command.rs @@ -3,7 +3,8 @@ use hyperion_clap::{MinecraftCommand, hyperion_command::CommandRegistry}; use crate::command::{ class::ClassCommand, fly::FlyCommand, gui::GuiCommand, raycast::RaycastCommand, - replace::ReplaceCommand, spawn::SpawnCommand, speed::SpeedCommand, xp::XpCommand, + replace::ReplaceCommand, shoot::ShootCommand, spawn::SpawnCommand, speed::SpeedCommand, + xp::XpCommand, }; mod class; @@ -11,6 +12,7 @@ mod fly; mod gui; mod raycast; mod replace; +mod shoot; mod spawn; mod speed; mod xp; @@ -20,6 +22,7 @@ pub fn register(registry: &mut CommandRegistry, world: &World) { FlyCommand::register(registry, world); RaycastCommand::register(registry, world); ReplaceCommand::register(registry, world); + ShootCommand::register(registry, world); SpeedCommand::register(registry, world); XpCommand::register(registry, world); SpawnCommand::register(registry, world); diff --git a/events/tag/src/command/shoot.rs b/events/tag/src/command/shoot.rs new file mode 100644 index 00000000..be9cd8f6 --- /dev/null +++ b/events/tag/src/command/shoot.rs @@ -0,0 +1,52 @@ +use clap::Parser; +use flecs_ecs::core::{Entity, EntityViewGet, World}; +use hyperion::{ + glam::Vec3, + simulation::{Pitch, Position, Spawn, Uuid, Velocity, Yaw, entity_kind::EntityKind}, +}; +use hyperion_clap::{CommandPermission, MinecraftCommand}; +use tracing::debug; + +#[derive(Parser, CommandPermission, Debug)] +#[command(name = "shoot")] +#[command_permission(group = "Normal")] +pub struct ShootCommand { + #[arg(help = "Initial velocity of the arrow")] + velocity: f32, +} + +impl MinecraftCommand for ShootCommand { + fn execute(self, world: &World, caller: Entity) { + const EYE_HEIGHT: f32 = 1.62; + const BASE_VELOCITY: f32 = 3.0; // Base velocity multiplier for arrows + + caller + .entity_view(world) + .get::<(&Position, &Yaw, &Pitch)>(|(pos, yaw, pitch)| { + // Calculate direction vector from player's rotation + let direction = super::raycast::get_direction_from_rotation(**yaw, **pitch); + + // Spawn arrow slightly in front of player to avoid self-collision + let spawn_pos = Vec3::new(pos.x, pos.y + EYE_HEIGHT, pos.z) + direction * 0.5; + + // Calculate velocity with base multiplier + let velocity = direction * (self.velocity * BASE_VELOCITY); + + debug!( + "Arrow velocity: ({}, {}, {})", + velocity.x, velocity.y, velocity.z + ); + + // Create arrow entity with velocity + world + .entity() + .add_enum(EntityKind::Arrow) + .set(Uuid::new_v4()) + .set(Position::new(spawn_pos.x, spawn_pos.y, spawn_pos.z)) + .set(Velocity::new(velocity.x, velocity.y, velocity.z)) + .set(Yaw::new(**yaw)) + .set(Pitch::new(**pitch)) + .enqueue(Spawn); + }); + } +}