diff --git a/minecraft-positions/src/lib.rs b/minecraft-positions/src/lib.rs index e67cc314..e3b603ae 100644 --- a/minecraft-positions/src/lib.rs +++ b/minecraft-positions/src/lib.rs @@ -112,6 +112,10 @@ impl Position { cz: (self.z.floor() as i32).div_euclid(16), } } + + pub fn distance(&self, other: &Position) -> f64 { + ((self.x - other.x).powi(2) + (self.y - other.y).powi(2) + (self.z - other.z).powi(2)).sqrt() + } } impl std::ops::Add for Position { diff --git a/minecraft-protocol/src/components/slots.rs b/minecraft-protocol/src/components/slots.rs index abdaa336..a3a2a484 100644 --- a/minecraft-protocol/src/components/slots.rs +++ b/minecraft-protocol/src/components/slots.rs @@ -21,7 +21,7 @@ pub struct SlotItem { #[cfg_attr(test, derive(PartialEq))] #[minecraft_enum(VarInt)] -#[derive(Debug)] +#[derive(Debug, Clone, Copy)] pub enum Hand { MainHand, OffHand, diff --git a/minecraft-server/src/entities/entity.rs b/minecraft-server/src/entities/entity.rs index 59da82ac..adbe4d8e 100644 --- a/minecraft-server/src/entities/entity.rs +++ b/minecraft-server/src/entities/entity.rs @@ -1,5 +1,6 @@ use super::*; +#[derive(Clone)] #[MinecraftEntity( inheritable, descendants { AbstractArrow..., Boat..., Display, FallingBlock, LlamaSpit, Painting, DragonFireball, Fireball..., FireworkRocket, SmallFireball, Interaction..., ItemEntity, ItemFrame..., LivingEntity... EndCrystal, EvokerFangs, WitherSkull, AreaEffectCloud, FishingHook, EyeOfEnder, ThrownItemProjectile... }, @@ -29,9 +30,7 @@ pub struct Entity { } impl Handler { - pub async fn init(self, server_msg_rcvr: BroadcastReceiver) { - self.insert_task("newton", tokio::spawn(newton_task(self.clone(), server_msg_rcvr))).await; - } + pub async fn init(self, _server_msg_rcvr: BroadcastReceiver) {} } impl Default for Entity { diff --git a/minecraft-server/src/entities/living_entity.rs b/minecraft-server/src/entities/living_entity.rs index d0d06d33..df8cb4d4 100644 --- a/minecraft-server/src/entities/living_entity.rs +++ b/minecraft-server/src/entities/living_entity.rs @@ -1,5 +1,6 @@ use super::*; +#[derive(Clone)] #[MinecraftEntity( inheritable, ancestors { Entity }, diff --git a/minecraft-server/src/entities/mobs/mod.rs b/minecraft-server/src/entities/mobs/mod.rs index e350db03..c2e6d531 100644 --- a/minecraft-server/src/entities/mobs/mod.rs +++ b/minecraft-server/src/entities/mobs/mod.rs @@ -13,7 +13,7 @@ pub use flying::*; mod bat; pub use bat::*; -#[derive(Default)] +#[derive(Default, Clone)] #[MinecraftEntity( inheritable, ancestors { LivingEntity, Entity }, @@ -36,7 +36,7 @@ pub struct AmbientCreature { pub mob: Mob, } -#[derive(Default)] +#[derive(Default, Clone)] #[MinecraftEntity( inheritable, ancestors { Mob, LivingEntity, Entity }, diff --git a/minecraft-server/src/entities/mod.rs b/minecraft-server/src/entities/mod.rs index 909251c0..6670520d 100644 --- a/minecraft-server/src/entities/mod.rs +++ b/minecraft-server/src/entities/mod.rs @@ -47,6 +47,7 @@ pub use minecraft_protocol::{ nbt::NbtTag, packets::UUID }; +pub use crate::world::EntityChangeSet; pub use crate::prelude::*; use std::{pin::Pin, future::Future}; @@ -111,13 +112,13 @@ impl Handler where AnyEntity: TryAsEntityRef { self.world.observe_entity(self.eid, observer).await } - pub async fn mutate(&self, mutator: impl FnOnce(&mut T) -> (R, EntityChanges)) -> Option { + pub async fn mutate(&self, mutator: impl FnOnce(&mut T) -> R) -> Option { self.world.mutate_entity(self.eid, move |entity| { mutator(entity.try_as_entity_mut().expect("Called mutate on the wrong entity")) }).await } - pub async fn mutate_any(&self, mutator: impl FnOnce(&mut AnyEntity) -> (R, EntityChanges)) -> Option { + pub async fn mutate_any(&self, mutator: impl FnOnce(&mut AnyEntity) -> R) -> Option { self.world.mutate_entity(self.eid, mutator).await } } @@ -265,6 +266,10 @@ impl AnyEntity { self.try_as_entity_ref() } + pub async fn init_task(&self) -> Option { + EntityTask::init(self).await + } + pub fn to_network(&self) -> Option { use minecraft_protocol::ids::entities::Entity::*; match self { diff --git a/minecraft-server/src/entities/monsters/mod.rs b/minecraft-server/src/entities/monsters/mod.rs index 80ea6cdc..1409e3c5 100644 --- a/minecraft-server/src/entities/monsters/mod.rs +++ b/minecraft-server/src/entities/monsters/mod.rs @@ -33,7 +33,7 @@ pub use zombies::*; mod enderman; pub use enderman::*; -#[derive(Default)] +#[derive(Default, Clone)] #[MinecraftEntity( inheritable, ancestors { PathfinderMob, Mob, LivingEntity, Entity }, diff --git a/minecraft-server/src/entities/monsters/zombies.rs b/minecraft-server/src/entities/monsters/zombies.rs index 65e8ad17..f491318e 100644 --- a/minecraft-server/src/entities/monsters/zombies.rs +++ b/minecraft-server/src/entities/monsters/zombies.rs @@ -1,15 +1,10 @@ -use minecraft_protocol::network; - use super::*; -#[derive(Default)] +#[derive(Default, Clone)] #[MinecraftEntity( inheritable, ancestors { Monster, PathfinderMob, Mob, LivingEntity, Entity }, descendants { ZombieVillager, Husk, Drowned, ZombifiedPiglin }, - defines { - Entity.init(self, server_msg_rcvr: BroadcastReceiver); - } )] pub struct Zombie { pub monster: Monster, @@ -18,68 +13,160 @@ pub struct Zombie { pub is_becoming_drowned: bool, } -impl Handler { - pub async fn init(self, server_msg_rcvr: BroadcastReceiver) { - self.insert_task("newton", tokio::spawn(newton_task(self.clone(), server_msg_rcvr.resubscribe()))).await; - self.insert_task("zombie-ai", tokio::spawn(zombie_ai_task(self.clone(), server_msg_rcvr))).await; - } +// TODO: Attributes should be stored in a nicer way +// https://minecraft.wiki/w/Attribute +const ZOMBIE_BASE_FOLLOW_RANGE: f64 = 35.0; +const ZOMBIE_BASE_MOVEMENT_SPEED: f64 = 0.23; +const ZOMBIE_SEARCH_COOLDOWN: u64 = 20; + +pub struct ZombieTask { + newton_task: NewtonTask, + target: Option, + last_search_tick: u64, } -pub async fn sleep_ticks(server_msg_rcvr: &mut BroadcastReceiver, t: usize) { - let mut i = 0; - while i < t { - let Ok(msg) = server_msg_rcvr.recv().await else {continue}; - if matches!(&msg, &ServerMessage::Tick(_)) { i += 1; } +impl ZombieTask { + pub async fn init(zombie: &Zombie) -> Option { + let anyentity: AnyEntity = zombie.to_owned().into(); + let Some(newton_task) = NewtonTask::init(&anyentity).await else { return None; }; + Some(ZombieTask { + newton_task, + target: None, + last_search_tick: 0, + }) } -} -const ZOOMBIE_SPEED: f64 = 0.2; // Arbitrary value + /// Sets the target to the closest player in range. + /// + /// Returns the position of the zombie and the position of the target as an optimization, just so that we don't have to get them again. + async fn acquire_target(&mut self, h: &Handler) -> Option<(Position, Position)> { + // Get the range of chunks to search + let self_position = h.observe(|e| e.get_entity().position.clone()).await?; + let mut lowest = self_position.clone(); + lowest.x -= ZOMBIE_BASE_FOLLOW_RANGE.floor(); + lowest.z -= ZOMBIE_BASE_FOLLOW_RANGE.floor(); + let mut highest = self_position.clone(); + highest.x += ZOMBIE_BASE_FOLLOW_RANGE.ceil(); + highest.z += ZOMBIE_BASE_FOLLOW_RANGE.ceil(); + let lowest_chunk = lowest.chunk_column(); + let highest_chunk = highest.chunk_column(); + + // List all players in area + let mut player_positions = HashMap::new(); + for cx in lowest_chunk.cx..=highest_chunk.cx { + for cz in lowest_chunk.cz..=highest_chunk.cz { + let chunk_position = ChunkColumnPosition { cx, cz }; + h.world.observe_entities(chunk_position, |entity, eid| -> Option<()> { + TryAsEntityRef::::try_as_entity_ref(entity).map(|player| { + player_positions.insert(eid, player.get_entity().position.clone()); + }); + None + }).await; + } + } -pub async fn zombie_ai_task(h: Handler, mut server_msg_rcvr: BroadcastReceiver) where AnyEntity: TryAsEntityRef { - loop { - sleep_ticks(&mut server_msg_rcvr, 1).await; + // Return if no players are found + if player_positions.is_empty() { + return None; + } - let mut self_position = h.observe(|e| e.get_entity().position.clone()).await.unwrap(); - let chunk = self_position.chunk_column(); - let player_positions = h.world.observe_entities(chunk, |entity| { - let network_entity = entity.to_network().unwrap(); - TryAsEntityRef::::try_as_entity_ref(entity).map(|player| { - (player.get_entity().position.clone(), network_entity) + // Get their distances + let mut player_distances = Vec::with_capacity(player_positions.len()); + for (eid, position) in &player_positions { + player_distances.push((*eid, position.distance(&self_position))); + } + player_distances.sort_by(|(_, d1), (_, d2)| d1.partial_cmp(d2).unwrap()); + + // Get the closest player that's in range + let (target_eid, target_dist) = player_distances[0]; + if target_dist > ZOMBIE_BASE_FOLLOW_RANGE as f64 { + return None; + } + self.target = Some(target_eid); + + // TODO: ensure there is a line of sight + + player_positions.remove(&target_eid).map(|target_position| (self_position, target_position)) + } + + /// Returns the position of the target if any. + async fn get_target_position(&self, h: &Handler) -> Option { + let target_eid = self.target?; + h.world.observe_entity(target_eid, |entity| { + TryAsEntityRef::::try_as_entity_ref(entity).map(|player| { + player.position.clone() }) - }).await; - - let Some((target_position, network_entity)) = player_positions.get(0) else { sleep_ticks(&mut server_msg_rcvr, 100).await; continue }; - let target_object = CollisionShape { - x1: target_position.x - network_entity.width() as f64 / 2.0, - y1: target_position.y, - z1: target_position.z - network_entity.width() as f64 / 2.0, - x2: target_position.x + network_entity.width() as f64 / 2.0, - y2: target_position.y + network_entity.height() as f64, - z2: target_position.z + network_entity.width() as f64 / 2.0, + }).await.flatten() + } + + /// Returns the position of the zombie. + async fn get_self_position(&self, h: &Handler) -> Option { + h.observe(|e| e.get_entity().position.clone()).await + } + + /// Returns the movement towards the target that can be applied without colliding with the world. + async fn get_movement(&mut self, h: &Handler, self_position: &Position, target_position: &Position) -> Translation { + // Create a movement vector + let mut translation = Translation { + x: target_position.x - self_position.x, + y: target_position.y - self_position.y, + z: target_position.z - self_position.z, }; + let norm = translation.norm(); + if norm > ZOMBIE_BASE_FOLLOW_RANGE { + self.target = None; + return Translation::zero(); + } + if norm > ZOMBIE_BASE_MOVEMENT_SPEED { + translation.set_norm(ZOMBIE_BASE_MOVEMENT_SPEED); + } - for _ in 0..50 { - let mut translation = Translation { - x: target_position.x - self_position.x, - y: target_position.y - self_position.y, - z: target_position.z - self_position.z, - }; - translation.set_norm(ZOOMBIE_SPEED); - - let authorized_translation = h.world.try_move(&target_object, &translation).await; - - let new_pos = h.mutate(|e| { - e.get_entity_mut().position += authorized_translation; - (e.get_entity().position.clone(), EntityChanges::position()) - }).await; - self_position = match new_pos { - Some(pos) => pos, - None => break, + // Create a collision shape + let collision_shape = CollisionShape { + x1: self_position.x - 0.5, + y1: self_position.y, + z1: self_position.z - 0.5, + x2: self_position.x + 0.5, + y2: self_position.y + 1.95, + z2: self_position.z + 0.5, + }; + + // Restrict the movement considering world collisions + h.world.try_move(&collision_shape, &translation).await + } + + pub async fn tick(&mut self, h: Handler, tick_id: u64, entity_change_set: &EntityChangeSet) { + // Acquire target if none + let mut positions = None; + if self.target.is_none() && self.last_search_tick + ZOMBIE_SEARCH_COOLDOWN < tick_id { + positions = self.acquire_target(&h).await; + self.last_search_tick = tick_id; + } + + // Get target position if not already acquired + if self.target.is_some() && positions.is_none() { + let target_position = self.get_target_position(&h).await; + let self_position = self.get_self_position(&h).await; + positions = match (target_position, self_position) { + (Some(target_position), Some(self_position)) => Some((self_position, target_position)), + _ => None, }; + } - sleep_ticks(&mut server_msg_rcvr, 1).await; // TODO: do while + // Get the movement to apply + if let Some((self_position, target_position)) = positions { + let movement = self.get_movement(&h, &self_position, &target_position).await; + let (yaw, pitch) = movement.yaw_pitch(); + h.mutate(|e| { + e.get_entity_mut().position += movement; + e.get_entity_mut().yaw = yaw; + e.get_entity_mut().pitch = pitch; + e.get_living_entity_mut().head_yaw = yaw; // TODO: Make pitch and yaw work on zombies + }).await; } - + + // Apply gravity and velocity + self.newton_task.tick(h.into(), entity_change_set).await; } } diff --git a/minecraft-server/src/entities/player.rs b/minecraft-server/src/entities/player.rs index acf209b8..be3df261 100644 --- a/minecraft-server/src/entities/player.rs +++ b/minecraft-server/src/entities/player.rs @@ -80,7 +80,8 @@ impl Player { let eid = world.spawn_entity::(AnyEntity::Player(player)).await; let handler = Handler::assume(eid, world); - handler.clone().insert_task("player", tokio::spawn(handle_player(handler, uuid, stream, packet_receiver, server_msg_rcvr, change_receiver))).await; + + tokio::spawn(handle_player(handler, uuid, stream, packet_receiver, server_msg_rcvr, change_receiver)); eid } @@ -92,7 +93,7 @@ impl Handler { let old_center_chunk = player.center_chunk.clone(); let new_center_chunk = player.get_entity().position.chunk(); player.center_chunk = new_center_chunk.clone(); - ((old_center_chunk, new_center_chunk, player.render_distance), EntityChanges::other()) + (old_center_chunk, new_center_chunk, player.render_distance) }).await else {return}; // Tell the client which chunk he is in @@ -112,7 +113,7 @@ impl Handler { // Select chunks to load (max 50) and unload let Some((loaded_chunks_after, newly_loaded_chunks, unloaded_chunks, uuid)) = self.mutate(|player| { - if loaded_chunks_after == player.loaded_chunks { return (None, EntityChanges::nothing()) }; + if loaded_chunks_after == player.loaded_chunks { return None }; let mut newly_loaded_chunks: Vec<_> = loaded_chunks_after.difference(&player.loaded_chunks).cloned().collect(); let unloaded_chunks: Vec<_> = player.loaded_chunks.difference(&loaded_chunks_after).cloned().collect(); for skipped in newly_loaded_chunks.iter().skip(50) { @@ -121,7 +122,7 @@ impl Handler { newly_loaded_chunks.truncate(50); let uuid = player.info.uuid; player.loaded_chunks = loaded_chunks_after.clone(); - (Some((loaded_chunks_after, newly_loaded_chunks, unloaded_chunks, uuid)), EntityChanges::other()) + Some((loaded_chunks_after, newly_loaded_chunks, unloaded_chunks, uuid)) }).await.flatten() else { return }; // Tell the world about the changes @@ -176,7 +177,7 @@ impl Handler { let packet = packet.serialize_minecraft_packet().unwrap(); let packets_sent = self.mutate(|player| { player.packets_sent += 1; - (player.packets_sent, EntityChanges::other()) + player.packets_sent }).await.unwrap_or(0); if packets_sent > 500 { warn!("Many packets sent ({packets_sent})"); @@ -233,7 +234,7 @@ impl Handler { }, WorldChange::EntityMetadata { eid, metadata } => todo!(), WorldChange::EntityPosition { eid, position } => { - let Some(prev_position) = self.mutate(|player| ((player.entity_prev_positions.insert(eid, position.clone())), EntityChanges::other())).await else {return}; + let Some(prev_position) = self.mutate(|player| player.entity_prev_positions.insert(eid, position.clone())).await else {return}; match prev_position { Some(prev_position) => { self.send_packet(PlayClientbound::UpdateEntityPosition { @@ -293,7 +294,7 @@ impl Handler { } } - async fn on_packet<'a>(mut self, packet: PlayServerbound<'a>) { + async fn on_packet(self, packet: PlayServerbound<'_>) { use PlayServerbound::*; match packet { SetPlayerPosition { x, y, z, on_ground } => { @@ -362,6 +363,9 @@ impl Handler { RequestPing { payload } => { self.send_packet(PlayClientbound::Ping { id: payload as i32 }).await; } + KeepAlive { keep_alive_id } => { + self.send_packet(PlayClientbound::KeepAlive { keep_alive_id }).await; + } packet => warn!("Unsupported packet received: {packet:?}"), } } diff --git a/minecraft-server/src/entities/tasks/mod.rs b/minecraft-server/src/entities/tasks/mod.rs index bc48155f..35e91cc6 100644 --- a/minecraft-server/src/entities/tasks/mod.rs +++ b/minecraft-server/src/entities/tasks/mod.rs @@ -2,3 +2,29 @@ pub use super::*; mod newton; pub use newton::*; + +//pub trait EntityTask where AnyEntity: TryAsEntityRef<::InnerEntity> { +// type InnerEntity; +// +// async fn init(h: Handler) -> Option>; +// async fn tick(&mut self, h: Handler); +//} + +pub enum EntityTask { + Zombie(ZombieTask), +} + +impl EntityTask { + pub async fn init(entity: &AnyEntity) -> Option { + match entity { + AnyEntity::Zombie(zombie) => ZombieTask::init(zombie).await.map(EntityTask::Zombie), + _ => None, + } + } + + pub async fn tick(&mut self, h: Handler, tick_id: u64, entity_change_set: &EntityChangeSet) { + match self { + EntityTask::Zombie(zombie_task) => zombie_task.tick(h.assume_other(), tick_id, entity_change_set).await, + } + } +} diff --git a/minecraft-server/src/entities/tasks/newton.rs b/minecraft-server/src/entities/tasks/newton.rs index 51b2f536..be7fef24 100644 --- a/minecraft-server/src/entities/tasks/newton.rs +++ b/minecraft-server/src/entities/tasks/newton.rs @@ -2,22 +2,43 @@ use crate::CollisionShape; use super::*; -pub async fn newton_task(h: Handler, mut server_msg_rcvr: BroadcastReceiver) where AnyEntity: TryAsEntityRef { - let Some(network_entity) = h.observe_any(|any_entity| any_entity.to_network()).await else { return; }; - - let (width, height) = match network_entity { - Some(network_entity) => (network_entity.width() as f64, network_entity.height() as f64), - None => { - warn!("Entity {} has no network entity", h.eid); - return; - } - }; +/// This task applies gravity and velocity to an entity. +/// It has minimal performance impact. +/// +/// It can be used by other larger tasks. +/// In that case, other tasks should stick to modifying `x`, `z`, `vx`, `vz` and `vy`. +/// This task shall be called at the end of their tick. +/// See the [ZombieTask] for an example. +pub struct NewtonTask { + width: f64, + height: f64, + on_ground: bool, +} - loop { - let Ok(msg) = server_msg_rcvr.recv().await else {continue}; +impl NewtonTask { + pub async fn init(entity: &AnyEntity) -> Option { + let network_entity = entity.to_network(); + + let (width, height) = match network_entity { + Some(network_entity) => (network_entity.width() as f64, network_entity.height() as f64), + None => { + warn!("Entity has no network entity"); + return None; + } + }; + + Some(NewtonTask { + width, + height, + on_ground: false, + }) + } - if !matches!(&msg, &ServerMessage::Tick(_)) { - continue; + pub async fn tick(&mut self, h: Handler, entity_change_set: &EntityChangeSet) { + // If it was on ground before and hasn't moved since, skip the turn + // TODO: detect if the ground is destroyed + if self.on_ground && !entity_change_set.get(&h.eid).copied().unwrap_or_default().position_changed() { + return; } // Get data from entity @@ -27,33 +48,19 @@ pub async fn newton_task(h: Handler, mut server_msg_rcvr }).await else { return; }; // Apply velocity and collisions - let mut changes = EntityChanges::nothing(); - let mut new_velocity = velocity.clone(); - new_velocity.y -= 9.81/20.0; + velocity.y -= 9.81/20.0; let bounding_box = CollisionShape { - x1: position.x - width/2.0, + x1: position.x - self.width/2.0, y1: position.y, - z1: position.z - width/2.0, - x2: position.x + width/2.0, - y2: position.y + height, - z2: position.z + width/2.0, + z1: position.z - self.width/2.0, + x2: position.x + self.width/2.0, + y2: position.y + self.height, + z2: position.z + self.width/2.0, }; - let new_velocity = h.world.try_move(&bounding_box, &new_velocity).await; - if velocity.x != new_velocity.x { - velocity.x = 0.0; - changes += EntityChanges::velocity(); - } - if velocity.y != new_velocity.y { - velocity.y = 0.0; - changes += EntityChanges::velocity(); - } - if velocity.z != new_velocity.z { - velocity.z = 0.0; - changes += EntityChanges::velocity(); - } - if !new_velocity.is_zero() { - changes += EntityChanges::position(); - position += new_velocity; + let new_velocity = h.world.try_move(&bounding_box, &velocity).await; + self.on_ground = velocity.y < 0.0 && new_velocity.y >= 0.0; + if !velocity.is_zero() { + position += new_velocity.clone(); } // TODO(feat): Apply air resistance to x and z velocity @@ -61,14 +68,10 @@ pub async fn newton_task(h: Handler, mut server_msg_rcvr // Mutate entity // TODO(correctness): Before modifying entity values, we should ensure the original values we based the changes on are still the same - if changes.nothing_changed() { - continue; - } h.mutate(|entity| { let entity = entity.get_entity_mut(); - entity.velocity = velocity; + entity.velocity = new_velocity; entity.position = position; - ((), changes) }).await; } } diff --git a/minecraft-server/src/server_behavior.rs b/minecraft-server/src/server_behavior.rs index c7672e51..22a750f5 100644 --- a/minecraft-server/src/server_behavior.rs +++ b/minecraft-server/src/server_behavior.rs @@ -6,7 +6,7 @@ use crate::prelude::*; #[derive(Clone, Debug)] pub enum ServerMessage { /// Message indicating a new tick has started - Tick(usize), + Tick(u64), } pub struct ServerBehavior { @@ -21,11 +21,13 @@ impl ServerBehavior { let world = Box::leak(Box::new(World::new(receiver.resubscribe()))); // Send ticks to player handlers + let world2: &World = world; tokio::spawn(async move { - let mut tick_id = 0; + let mut tick_id: u64 = 0; let mut tick = tokio::time::interval(Duration::from_millis(50)); loop { tick.tick().await; + world2.tick(tick_id).await; let _ = sender.send(ServerMessage::Tick(tick_id)); tick_id += 1; } diff --git a/minecraft-server/src/world/change.rs b/minecraft-server/src/world/change.rs index a14f736d..d80e9598 100644 --- a/minecraft-server/src/world/change.rs +++ b/minecraft-server/src/world/change.rs @@ -38,13 +38,10 @@ pub enum WorldChange { }, } +#[derive(Clone, Copy)] pub struct EntityChanges(u8); impl EntityChanges { - pub const fn other() -> EntityChanges { - EntityChanges(0) - } - pub const fn nothing() -> EntityChanges { EntityChanges(0) } @@ -61,7 +58,7 @@ impl EntityChanges { EntityChanges(1 << 2) } - pub const fn metadata() -> EntityChanges { + pub const fn other() -> EntityChanges { EntityChanges(1 << 3) } @@ -81,7 +78,7 @@ impl EntityChanges { self.0 & (1 << 2) != 0 } - pub const fn metadata_changed(&self) -> bool { + pub const fn other_changed(&self) -> bool { self.0 & (1 << 3) != 0 } } @@ -99,3 +96,9 @@ impl std::ops::AddAssign for EntityChanges { self.0 |= rhs.0; } } + +impl Default for EntityChanges { + fn default() -> EntityChanges { + EntityChanges::nothing() + } +} diff --git a/minecraft-server/src/world/collisions.rs b/minecraft-server/src/world/collisions.rs index 33ed52e5..51b8273f 100644 --- a/minecraft-server/src/world/collisions.rs +++ b/minecraft-server/src/world/collisions.rs @@ -268,13 +268,26 @@ impl<'a> Iterator for PointIter<'a> { } /// Vector describing a movement -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Default, Clone, PartialEq)] pub struct Translation { pub x: f64, pub y: f64, pub z: f64, } +impl Translation { + pub fn zero() -> Self { + Self::default() + } + + pub fn yaw_pitch(&self) -> (f32, f32) { + let r = (self.x * self.x + self.y * self.y + self.z * self.z).sqrt(); + let pitch = -(self.y / r).asin() / std::f64::consts::PI * 180.0; + let yaw = -(self.x / self.z).atan() / std::f64::consts::PI * 180.0; + (yaw as f32, pitch as f32) + } +} + pub struct TranslationFragmentIterator<'a> { translation: &'a Translation, position: &'a CollisionShape, diff --git a/minecraft-server/src/world/ecs.rs b/minecraft-server/src/world/ecs.rs index eb20c8b0..96cc9ec1 100644 --- a/minecraft-server/src/world/ecs.rs +++ b/minecraft-server/src/world/ecs.rs @@ -3,18 +3,16 @@ use crate::*; use minecraft_protocol::packets::UUID; use tokio::sync::RwLock; -pub type EntityTask = Pin + Send + Sync + 'static>>; -pub type EntityTaskHandle = tokio::task::JoinHandle<()>; - pub struct Entities { eid_counter: std::sync::atomic::AtomicU32, uuid_counter: std::sync::atomic::AtomicU64, - pub entities: RwLock>, + tasks: RwLock>, + entities: RwLock>, + change_set: RwLock, /// A hashmap of chunk positions to get a list of entities in a chunk - pub chunks: RwLock>>, - pub uuids: RwLock>, - pub entity_tasks: RwLock>>, + chunks: RwLock>>, + uuids: RwLock>, } impl Entities { @@ -22,10 +20,11 @@ impl Entities { Entities { eid_counter: std::sync::atomic::AtomicU32::new(0), uuid_counter: std::sync::atomic::AtomicU64::new(0), + tasks: RwLock::new(HashMap::new()), entities: RwLock::new(HashMap::new()), + change_set: RwLock::new(HashMap::new()), chunks: RwLock::new(HashMap::new()), uuids: RwLock::new(HashMap::new()), - entity_tasks: RwLock::new(HashMap::new()), } } @@ -36,14 +35,14 @@ impl Entities { /// Observe entities in a chunk through a closure /// That closure will be applied to each entity, and the results will be returned in a vector - pub(super) async fn observe_entities(&self, chunk: ChunkColumnPosition, mut observer: impl FnMut(&AnyEntity) -> Option) -> Vec { + pub(super) async fn observe_entities(&self, chunk: ChunkColumnPosition, mut observer: impl FnMut(&AnyEntity, Eid) -> Option) -> Vec { let entities = self.entities.read().await; let chunks = self.chunks.read().await; let Some(eids) = chunks.get(&chunk) else {return Vec::new()}; let mut results = Vec::with_capacity(eids.len()); for eid in eids { if let Some(entity) = entities.get(eid) { - if let Some(r) = observer(entity) { + if let Some(r) = observer(entity, *eid) { results.push(r); } } @@ -51,14 +50,26 @@ impl Entities { results } + // TODO don't return [EntityChanges] /// Mutate an entity through a closure - pub(super) async fn mutate_entity(&self, eid: Eid, mutator: impl FnOnce(&mut AnyEntity) -> (R, EntityChanges)) -> Option<(R, EntityChanges)> { + pub(super) async fn mutate_entity(&self, eid: Eid, mutator: impl FnOnce(&mut AnyEntity) -> R) -> Option<(R, EntityChanges)> { let mut entities = self.entities.write().await; if let Some(entity) = entities.get_mut(&eid) { let prev_position = entity.as_entity().position.clone(); + let prev_velocity = entity.as_entity().velocity.clone(); + let prev_pitch = entity.as_entity().pitch; + let prev_yaw = entity.as_entity().yaw; let r = mutator(entity); + let mut changes = EntityChanges::other(); + if prev_velocity != entity.as_entity().velocity { + changes += EntityChanges::velocity(); + } + if prev_pitch != entity.as_entity().pitch || prev_yaw != entity.as_entity().yaw { + changes += EntityChanges::pitch(); + } if prev_position != entity.as_entity().position { + changes += EntityChanges::position(); let old_chunk = prev_position.chunk_column(); let new_chunk = entity.as_entity().position.chunk_column(); drop(entities); @@ -66,24 +77,33 @@ impl Entities { chunks.entry(old_chunk).and_modify(|set| { set.remove(&eid); }); // TODO: ensure it gets removed chunks.entry(new_chunk).or_insert(HashSet::new()).insert(eid); } - Some(r) + *self.change_set.write().await.entry(eid).or_default() += changes; + Some((r, changes)) } else { None } } + // TODO: Since we lock tasks it makes it impossible for an entity task to itself call this function + // It would be resolved if we had a temporary task buffer that would be added only on Ecs::tick pub(super) async fn spawn_entity(&self, entity: AnyEntity, world: &'static World, receiver: BroadcastReceiver) -> (Eid, UUID) where AnyEntity: TryAsEntityRef, Handler: EntityExt { + let task = entity.init_task().await; let eid = self.eid_counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst); let uuid = self.uuid_counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst) as u128; + let mut tasks = self.tasks.write().await; let mut entities = self.entities.write().await; let mut chunks = self.chunks.write().await; let mut uuids = self.uuids.write().await; chunks.entry(entity.as_entity().position.chunk_column()).or_insert(HashSet::new()).insert(eid); entities.insert(eid, entity); + if let Some(task) = task { + tasks.insert(eid, task); + } uuids.insert(uuid, eid); drop(entities); + drop(tasks); drop(chunks); drop(uuids); let h = Handler::::assume(eid, world); @@ -91,14 +111,6 @@ impl Entities { (eid, uuid) } - pub(super) async fn insert_entity_task(&self, eid: Eid, name: &'static str, handle: EntityTaskHandle) { - let mut entity_tasks = self.entity_tasks.write().await; - let old = entity_tasks.entry(eid).or_insert(HashMap::new()).insert(name, handle); - if let Some(old) = old { - old.abort(); - } - } - /// Remove an entity pub(super) async fn remove_entity(&self, eid: Eid) -> Option { let entity = self.entities.write().await.remove(&eid); @@ -107,13 +119,15 @@ impl Entities { chunks.retain(|_,v| !v.is_empty()); drop(chunks); self.uuids.write().await.retain(|_,v| *v != eid); - self.entity_tasks.write().await.remove(&eid); entity } -} -impl Handler where AnyEntity: TryAsEntityRef { - pub async fn insert_task(&self, name: &'static str, handle: EntityTaskHandle) { - self.world.entities.insert_entity_task(self.eid, name, handle).await; + pub(super) async fn tick(&self, tick_id: u64, world: &'static World) { + let entity_change_set = std::mem::take(&mut *self.change_set.write().await); + let mut tasks = self.tasks.write().await; + for (eid, task) in tasks.iter_mut() { + let h = Handler::::assume(*eid, world); + task.tick(h, tick_id, &entity_change_set).await; + } } } diff --git a/minecraft-server/src/world/mod.rs b/minecraft-server/src/world/mod.rs index 3a2ed2dd..5d0a9f3a 100644 --- a/minecraft-server/src/world/mod.rs +++ b/minecraft-server/src/world/mod.rs @@ -11,6 +11,8 @@ use ecs::*; mod collisions; pub use collisions::*; +pub type EntityChangeSet = HashMap; + /// World is the union of the map and entities. /// World handles loaded chunks and entities. /// It is responsible for notifying players of changes in the world. @@ -106,12 +108,12 @@ impl World { self.entities.observe_entity(eid, observer).await } - pub async fn observe_entities(&self, chunk: ChunkColumnPosition, observer: impl FnMut(&AnyEntity) -> Option) -> Vec { + pub async fn observe_entities(&self, chunk: ChunkColumnPosition, observer: impl FnMut(&AnyEntity, Eid) -> Option) -> Vec { self.entities.observe_entities(chunk, observer).await } // TODO: add version that doesn't notify modified entity - pub async fn mutate_entity(&self, eid: Eid, mutator: impl FnOnce(&mut AnyEntity) -> (R, EntityChanges)) -> Option { + pub async fn mutate_entity(&self, eid: Eid, mutator: impl FnOnce(&mut AnyEntity) -> R) -> Option { // TODO: change events match self.entities.mutate_entity(eid, mutator).await { Some((r, changes)) => { @@ -139,9 +141,6 @@ impl World { head_yaw, }).await; } - if changes.metadata_changed() { - todo!() - } Some(r) }, None => None, @@ -158,6 +157,11 @@ impl World { } } } + + pub async fn tick(&'static self, tick_id: u64) { + self.entities.tick(tick_id, self).await; + // TODO: tick world + } } #[cfg(test)]