diff --git a/Cargo.toml b/Cargo.toml index 05255c27..a85afad9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ members = [ "src/lib/utils/profiling", "src/lib/world", "src/lib/world_gen", - "src/lib/utils/threadpool", + "src/lib/utils/threadpool", "src/lib/entities", ] #================== Lints ==================# @@ -87,6 +87,7 @@ debug = true ferrumc-anvil = { path = "src/lib/adapters/anvil" } ferrumc-config = { path = "src/lib/utils/config" } ferrumc-core = { path = "src/lib/core" } +ferrumc-entities = { path = "src/lib/entities" } ferrumc-general-purpose = { path = "src/lib/utils/general_purpose" } ferrumc-logging = { path = "src/lib/utils/logging" } ferrumc-macros = { path = "src/lib/derive_macros" } @@ -193,7 +194,10 @@ noise = "0.9.0" ctrlc = "3.4.7" num_cpus = "1.17.0" typename = "0.1.2" + +# Game Logic bevy_ecs = { version = "0.16.1", features = ["multi_threaded", "trace"] } +glam = "0.30.3" # I/O memmap2 = "0.9.5" diff --git a/src/bin/Cargo.toml b/src/bin/Cargo.toml index 5ecc5713..ab24ea64 100644 --- a/src/bin/Cargo.toml +++ b/src/bin/Cargo.toml @@ -18,6 +18,7 @@ ferrumc-plugins = { workspace = true } ferrumc-storage = { workspace = true } ferrumc-utils = { workspace = true } ferrumc-config = { workspace = true } +ferrumc-entities = { workspace = true } ferrumc-profiling = { workspace = true } ferrumc-logging = { workspace = true } ferrumc-world = { workspace = true } diff --git a/src/bin/src/packet_handlers/play_packets/chat_message.rs b/src/bin/src/packet_handlers/play_packets/chat_message.rs new file mode 100644 index 00000000..666394d5 --- /dev/null +++ b/src/bin/src/packet_handlers/play_packets/chat_message.rs @@ -0,0 +1,30 @@ +use bevy_ecs::prelude::{EventWriter, Res}; +use bevy_ecs::system::Query; +use ferrumc_core::transform::position::Position; +use ferrumc_entities::events::SpawnEntityEvent; +use ferrumc_entities::bundles::ZOMBIE_ID; +use ferrumc_core::entities::entity_kind::EntityKind; +use ferrumc_net::ChatMessageReceiver; +use tracing::info; + +pub fn handle( + events: Res, + pos_query: Query<&Position>, + mut ev_spawn_entity: EventWriter, +) { + for (packet, entity_id) in events.0.try_iter() { + info!("[CHAT] Received message: {}", packet.message); + if matches!(packet.message.as_str(), "zombie") { + info!("[CHAT] Zombie command received, spawning zombie!"); + + let Ok(pos) = pos_query.get(entity_id) else { + tracing::error!("Failed to get position for entity {:?}", entity_id); + continue; + }; + ev_spawn_entity.write(SpawnEntityEvent { + entity_kind: EntityKind::new(ZOMBIE_ID), + position: pos.clone(), + }); + } + } +} diff --git a/src/bin/src/packet_handlers/play_packets/mod.rs b/src/bin/src/packet_handlers/play_packets/mod.rs index 60fed72a..57b554a8 100644 --- a/src/bin/src/packet_handlers/play_packets/mod.rs +++ b/src/bin/src/packet_handlers/play_packets/mod.rs @@ -1,6 +1,7 @@ use bevy_ecs::schedule::Schedule; -mod chunk_batch_ack; +pub mod chat_message; +pub mod chunk_batch_ack; mod confirm_player_teleport; mod keep_alive; mod place_block; @@ -15,6 +16,7 @@ mod swing_arm; pub fn register_packet_handlers(schedule: &mut Schedule) { // Added separately so if we mess up the signature of one of the systems we can know exactly // which one + schedule.add_systems(chat_message::handle); schedule.add_systems(chunk_batch_ack::handle); schedule.add_systems(confirm_player_teleport::handle); schedule.add_systems(keep_alive::handle); diff --git a/src/bin/src/register_events.rs b/src/bin/src/register_events.rs index d1c920d3..eb14a1ea 100644 --- a/src/bin/src/register_events.rs +++ b/src/bin/src/register_events.rs @@ -3,6 +3,7 @@ use bevy_ecs::prelude::World; use ferrumc_core::chunks::cross_chunk_boundary_event::CrossChunkBoundaryEvent; use ferrumc_core::conn::conn_kill_event::ConnectionKillEvent; use ferrumc_core::conn::force_player_recount_event::ForcePlayerRecountEvent; +use ferrumc_entities::events::SpawnEntityEvent; use ferrumc_net::packets::packet_events::TransformEvent; pub fn register_events(world: &mut World) { @@ -10,4 +11,7 @@ pub fn register_events(world: &mut World) { EventRegistry::register_event::(world); EventRegistry::register_event::(world); EventRegistry::register_event::(world); + + // idk if they should be added here, but just for testing purposes: + EventRegistry::register_event::(world); } diff --git a/src/bin/src/systems/mod.rs b/src/bin/src/systems/mod.rs index 8a29f771..a131d4d1 100644 --- a/src/bin/src/systems/mod.rs +++ b/src/bin/src/systems/mod.rs @@ -4,6 +4,7 @@ mod keep_alive_system; pub mod new_connections; mod player_count_update; pub mod send_chunks; +pub mod spawn_entities; mod world_sync; pub fn register_game_systems(schedule: &mut bevy_ecs::schedule::Schedule) { @@ -13,6 +14,9 @@ pub fn register_game_systems(schedule: &mut bevy_ecs::schedule::Schedule) { schedule.add_systems(player_count_update::player_count_updater); schedule.add_systems(world_sync::sync_world); + schedule.add_systems(spawn_entities::spawn_entity::handle_spawn_entity); + schedule.add_systems(spawn_entities::on_new_entity::broadcast_new_entities); + // Should always be last schedule.add_systems(connection_killer::connection_killer); } diff --git a/src/bin/src/systems/spawn_entities/mod.rs b/src/bin/src/systems/spawn_entities/mod.rs new file mode 100644 index 00000000..c99f5a00 --- /dev/null +++ b/src/bin/src/systems/spawn_entities/mod.rs @@ -0,0 +1,2 @@ +pub mod spawn_entity; +pub mod on_new_entity; diff --git a/src/bin/src/systems/spawn_entities/on_new_entity.rs b/src/bin/src/systems/spawn_entities/on_new_entity.rs new file mode 100644 index 00000000..60d77c52 --- /dev/null +++ b/src/bin/src/systems/spawn_entities/on_new_entity.rs @@ -0,0 +1,39 @@ +use bevy_ecs::entity::Entity; +use bevy_ecs::query::Added; +use bevy_ecs::system::Query; +use ferrumc_core::entities::entity_kind::EntityKind; +use ferrumc_core::transform::position::Position; +use ferrumc_core::transform::rotation::Rotation; +use ferrumc_net::connection::StreamWriter; +use ferrumc_net::packets::outgoing::spawn_entity::SpawnEntityPacket; + +pub fn broadcast_new_entities( + query: Query<(Entity, &EntityKind), Added>, + players_query: Query<(Entity, &mut StreamWriter)>, + transforms: Query<(&Position, &Rotation, &EntityKind)>, +) { + for (entity, kind) in query.iter() { + tracing::info!("New entity spawned: Entity {:?}, Kind {:?}", entity, kind); + + let (pos, rot, kind) = transforms + .get(entity) + .unwrap_or_else(|_| panic!("Missing transform for {:?}", entity)); + + let packet = SpawnEntityPacket::entity(entity, pos, rot, kind); + + if let Ok(packet) = packet { + for (player_entity, writer) in players_query.iter() { + tracing::debug!("Sending SpawnEntityPacket to player: {:?}", player_entity); + if let Err(e) = writer.send_packet(packet.clone()) { + tracing::error!( + "Failed to send SpawnEntityPacket to player {:?}: {}", + player_entity, + e + ); + } + } + } else { + tracing::error!("Failed to create SpawnEntityPacket for entity {:?}", entity); + } + } +} diff --git a/src/bin/src/systems/spawn_entities/spawn_entity.rs b/src/bin/src/systems/spawn_entities/spawn_entity.rs new file mode 100644 index 00000000..37c8deea --- /dev/null +++ b/src/bin/src/systems/spawn_entities/spawn_entity.rs @@ -0,0 +1,19 @@ +use bevy_ecs::prelude::*; +use ferrumc_entities::events::SpawnEntityEvent; +use ferrumc_entities::factory::EntityFactory; + +pub fn handle_spawn_entity( + mut commands: Commands, + mut events: EventReader, +) { + for ev in events.read() { + if let Some(entity_id) = EntityFactory::spawn_entity( + &mut commands, + ev.entity_kind, + ev.position.clone(), + ) { + tracing::info!("Spawned entity {:?} of kind {:?} at {:?}", + entity_id, ev.entity_kind, ev.position); + } + } +} diff --git a/src/bin/src/systems/world_sync.rs b/src/bin/src/systems/world_sync.rs index 0b4af559..d271cf53 100644 --- a/src/bin/src/systems/world_sync.rs +++ b/src/bin/src/systems/world_sync.rs @@ -9,7 +9,7 @@ pub fn sync_world(state: Res, mut last_synced: ResMut= 15 { - tracing::info!("Syncing world..."); + tracing::trace!("Syncing world..."); state.0.world.sync().expect("Failed to sync world"); // Update the last synced time diff --git a/src/lib/core/Cargo.toml b/src/lib/core/Cargo.toml index 6e483ad3..73d8e9d4 100644 --- a/src/lib/core/Cargo.toml +++ b/src/lib/core/Cargo.toml @@ -10,6 +10,8 @@ bevy_ecs = { workspace = true } tracing = { workspace = true } typename = { workspace = true } ferrumc-net-codec = { workspace = true } +glam = { workspace = true} +ferrumc-macros = { workspace = true } [dev-dependencies] criterion = { workspace = true } diff --git a/src/lib/core/src/collisions/bounding_box.rs b/src/lib/core/src/collisions/bounding_box.rs new file mode 100644 index 00000000..f820874c --- /dev/null +++ b/src/lib/core/src/collisions/bounding_box.rs @@ -0,0 +1,15 @@ +use bevy_ecs::prelude::Component; +use glam::Vec3; + +#[derive(Component)] +pub struct BoundingBox { + pub half_extents: Vec3, // (0.3, 0.9, 0.3) for zombie-ish size +} + +impl BoundingBox { + pub fn new(half_extents: impl Into) -> Self { + BoundingBox { + half_extents: half_extents.into(), + } + } +} diff --git a/src/lib/core/src/collisions/mod.rs b/src/lib/core/src/collisions/mod.rs index c919e66b..4c707c54 100644 --- a/src/lib/core/src/collisions/mod.rs +++ b/src/lib/core/src/collisions/mod.rs @@ -1 +1,2 @@ +pub mod bounding_box; pub mod bounds; diff --git a/src/lib/core/src/entities/entity_kind.rs b/src/lib/core/src/entities/entity_kind.rs new file mode 100644 index 00000000..42e35c9a --- /dev/null +++ b/src/lib/core/src/entities/entity_kind.rs @@ -0,0 +1,25 @@ +use bevy_ecs::prelude::Component; + +#[derive(Component, Debug, Clone, Copy)] +pub struct EntityKind { + /// The id of the entity kind. (Found in the registry under minecraft:entity_type) + r#type: u64, +} + +impl EntityKind { + /// Creates a new `EntityKind` with the given type id. + pub fn new(r#type: u64) -> Self { + Self { r#type } + } + + /// Returns the type id of the entity kind. + pub fn get_id(&self) -> u64 { + self.r#type + } +} + +impl From for EntityKind { + fn from(r#type: u64) -> Self { + Self::new(r#type) + } +} diff --git a/src/lib/core/src/entities/health.rs b/src/lib/core/src/entities/health.rs new file mode 100644 index 00000000..a2233dfe --- /dev/null +++ b/src/lib/core/src/entities/health.rs @@ -0,0 +1,18 @@ +use bevy_ecs::prelude::Component; +use typename::TypeName; + +#[derive(TypeName, Debug, Component)] +pub struct Health { + pub current: f32, + pub max: f32, +} + +impl Health { + pub fn new(current: f32, max: f32) -> Self { + Self { current, max } + } + /// New health with the same current and max value + pub fn new_max(max: f32) -> Self { + Self { current: max, max } + } +} diff --git a/src/lib/core/src/entities/mod.rs b/src/lib/core/src/entities/mod.rs new file mode 100644 index 00000000..f54cbe82 --- /dev/null +++ b/src/lib/core/src/entities/mod.rs @@ -0,0 +1,2 @@ +pub mod entity_kind; +pub mod health; diff --git a/src/lib/core/src/lib.rs b/src/lib/core/src/lib.rs index bd447958..bf66b841 100644 --- a/src/lib/core/src/lib.rs +++ b/src/lib/core/src/lib.rs @@ -4,6 +4,7 @@ pub mod errors; pub mod chunks; pub mod collisions; pub mod conn; +pub mod entities; pub mod identity; pub mod state; pub mod transform; diff --git a/src/lib/core/src/transform/mod.rs b/src/lib/core/src/transform/mod.rs index effd8b99..2b0e70e0 100644 --- a/src/lib/core/src/transform/mod.rs +++ b/src/lib/core/src/transform/mod.rs @@ -1,3 +1,24 @@ +use bevy_ecs::prelude::Bundle; + pub mod grounded; pub mod position; pub mod rotation; + +#[derive(Bundle)] +pub struct Transform { + pub position: position::Position, + pub rotation: rotation::Rotation, + pub grounded: grounded::OnGround, +} +impl Transform { + pub fn new( + position: impl Into, + rotation: impl Into, + ) -> Self { + Transform { + position: position.into(), + rotation: rotation.into(), + grounded: grounded::OnGround::default(), + } + } +} diff --git a/src/lib/core/src/transform/position.rs b/src/lib/core/src/transform/position.rs index 7605f66e..43c16f2e 100644 --- a/src/lib/core/src/transform/position.rs +++ b/src/lib/core/src/transform/position.rs @@ -3,7 +3,7 @@ use ferrumc_net_codec::net_types::network_position::NetworkPosition; use std::fmt::{Debug, Display, Formatter}; use typename::TypeName; -#[derive(TypeName, Component)] +#[derive(TypeName, Component, Clone)] pub struct Position { pub x: f64, pub y: f64, diff --git a/src/lib/entities/Cargo.toml b/src/lib/entities/Cargo.toml new file mode 100644 index 00000000..ec707300 --- /dev/null +++ b/src/lib/entities/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "ferrumc-entities" +version = "0.1.0" +edition = "2024" + +[dependencies] +bevy_ecs = { workspace = true } + +ferrumc-core = { workspace = true } +ferrumc-macros = { workspace = true } +glam = { workspace = true } + +[lints] +workspace = true diff --git a/src/lib/entities/src/bundles.rs b/src/lib/entities/src/bundles.rs new file mode 100644 index 00000000..8a171066 --- /dev/null +++ b/src/lib/entities/src/bundles.rs @@ -0,0 +1,44 @@ +use crate::components::{AiComponent, Hostile, Movable, Zombie}; +use crate::spawner::SpawnBundleExt; +use bevy_ecs::bundle::Bundle; +use ferrumc_core::collisions::bounding_box::BoundingBox; +use ferrumc_core::entities::entity_kind::EntityKind; +use ferrumc_core::entities::health::Health; +use ferrumc_core::transform::Transform; +use ferrumc_core::transform::position::Position; +use ferrumc_macros::get_registry_entry; + +#[derive(Bundle)] +pub struct ZombieBundle { + pub zombie: Zombie, + pub entity_kind: EntityKind, + pub transform: Transform, + pub health: Health, + pub bounding_box: BoundingBox, + pub ai: AiComponent, + pub movable: Movable, + pub hostile: Hostile, +} + +pub const ZOMBIE_ID: u64 = get_registry_entry!("minecraft:entity_type.entries.minecraft:zombie"); +impl Default for ZombieBundle { + fn default() -> Self { + ZombieBundle { + zombie: Zombie, + entity_kind: EntityKind::new(ZOMBIE_ID), + transform: Transform::new((0.0, 64.0, 0.0), (0.0, 0.0)), + health: Health::new_max(20.0), + bounding_box: BoundingBox::new((0.3, 0.9, 0.3)), + ai: AiComponent::default(), + movable: Movable { speed: 0.25 }, // Slow zombie movement + hostile: Hostile { damage: 2.0, range: 1.5 }, // Zombie specific stats + } + } +} + +impl SpawnBundleExt for ZombieBundle { + fn with_position(mut self, position: Position) -> Self { + self.transform.position = position; + self + } +} diff --git a/src/lib/entities/src/components.rs b/src/lib/entities/src/components.rs new file mode 100644 index 00000000..17721a21 --- /dev/null +++ b/src/lib/entities/src/components.rs @@ -0,0 +1,57 @@ +use bevy_ecs::prelude::*; + +/// Marker type for zombie +#[derive(Component)] +pub struct Zombie; + +/// Generic AI component for entities that can have AI behavior +#[derive(Component)] +pub struct AiComponent { + pub target: Option, + pub state: AiState, +} + +#[derive(Debug, Clone, Copy)] +pub enum AiState { + Idle, + Chasing, + Attacking, + Fleeing, +} + +impl Default for AiComponent { + fn default() -> Self { + Self { + target: None, + state: AiState::Idle, + } + } +} + +/// Component for entities that can move +#[derive(Component)] +pub struct Movable { + pub speed: f32, +} + +impl Default for Movable { + fn default() -> Self { + Self { speed: 1.0 } + } +} + +/// Component for hostile entities +#[derive(Component)] +pub struct Hostile { + pub damage: f32, + pub range: f32, +} + +impl Default for Hostile { + fn default() -> Self { + Self { + damage: 1.0, + range: 2.0, + } + } +} diff --git a/src/lib/entities/src/events.rs b/src/lib/entities/src/events.rs new file mode 100644 index 00000000..d5adec54 --- /dev/null +++ b/src/lib/entities/src/events.rs @@ -0,0 +1,9 @@ +use bevy_ecs::prelude::Event; +use ferrumc_core::transform::position::Position; +use ferrumc_core::entities::entity_kind::EntityKind; + +#[derive(Clone, Event)] +pub struct SpawnEntityEvent { + pub entity_kind: EntityKind, + pub position: Position, +} diff --git a/src/lib/entities/src/factory.rs b/src/lib/entities/src/factory.rs new file mode 100644 index 00000000..14400f2c --- /dev/null +++ b/src/lib/entities/src/factory.rs @@ -0,0 +1,28 @@ +use bevy_ecs::prelude::*; +use ferrumc_core::entities::entity_kind::EntityKind; +use ferrumc_core::transform::position::Position; +use crate::bundles::*; +use crate::spawner::SpawnBundleExt; + +/// Entity factory for spawning different types of entities +#[derive(Default)] +pub struct EntityFactory; + +impl EntityFactory { + /// Spawn an entity of the given kind at the specified position + pub fn spawn_entity( + commands: &mut Commands, + entity_kind: EntityKind, + position: Position, + ) -> Option { + match entity_kind.get_id() { + ZOMBIE_ID => { + Some(commands.spawn(ZombieBundle::default().with_position(position)).id()) + } + // Add more entity types here as they're implemented + _ => { + None + } + } + } +} diff --git a/src/lib/entities/src/lib.rs b/src/lib/entities/src/lib.rs new file mode 100644 index 00000000..6b034b25 --- /dev/null +++ b/src/lib/entities/src/lib.rs @@ -0,0 +1,5 @@ +pub mod bundles; +pub mod components; +pub mod events; +pub mod spawner; +pub mod factory; diff --git a/src/lib/entities/src/spawner.rs b/src/lib/entities/src/spawner.rs new file mode 100644 index 00000000..f9fc4996 --- /dev/null +++ b/src/lib/entities/src/spawner.rs @@ -0,0 +1,8 @@ +use bevy_ecs::prelude::*; +use ferrumc_core::transform::position::Position; + +/// Extension trait for bundles to set a position +pub trait SpawnBundleExt: Sized { + /// Sets the `Position` on the bundle's `transform` component + fn with_position(self, position: Position) -> Self; +} diff --git a/src/lib/net/crates/codec/src/decode/errors.rs b/src/lib/net/crates/codec/src/decode/errors.rs index 472aee9d..eb1acb3b 100644 --- a/src/lib/net/crates/codec/src/decode/errors.rs +++ b/src/lib/net/crates/codec/src/decode/errors.rs @@ -5,6 +5,13 @@ pub enum NetDecodeError { #[error("IO error: {0}")] IoError(#[from] std::io::Error), + #[error("Invalid length: expected {expected}, got {actual}")] + InvalidLength { + expected: usize, + actual: usize, + field: String, + }, + #[error("Invalid UTF-8: {0}")] Utf8Error(#[from] std::string::FromUtf8Error), diff --git a/src/lib/net/crates/codec/src/decode/primitives.rs b/src/lib/net/crates/codec/src/decode/primitives.rs index bb9ca220..31a5ec61 100644 --- a/src/lib/net/crates/codec/src/decode/primitives.rs +++ b/src/lib/net/crates/codec/src/decode/primitives.rs @@ -3,6 +3,7 @@ use crate::net_types::var_int::VarInt; use std::collections::HashMap; use std::hash::Hash; use std::io::Read; +use std::mem::MaybeUninit; use tokio::io::AsyncRead; use tokio::io::AsyncReadExt; @@ -144,6 +145,44 @@ where } } +impl NetDecode for [T; N] +where + T: NetDecode, +{ + fn decode(reader: &mut R, opts: &NetDecodeOpts) -> NetDecodeResult { + // 1) allocate uninitialized array + let mut uninit: MaybeUninit<[T; N]> = MaybeUninit::uninit(); + // 2) get a *mut T to the first element + let ptr = uninit.as_mut_ptr() as *mut T; + // 3) fill it slot by slot + for i in 0..N { + let val = T::decode(reader, opts)?; + unsafe { + ptr.add(i).write(val); + } + } + // 4) all slots are init’d, so we can assume init + let array = unsafe { uninit.assume_init() }; + Ok(array) + } + + async fn decode_async( + reader: &mut R, + opts: &NetDecodeOpts, + ) -> NetDecodeResult { + let mut uninit: MaybeUninit<[T; N]> = MaybeUninit::uninit(); + let ptr = uninit.as_mut_ptr() as *mut T; + for i in 0..N { + let val = T::decode_async(reader, opts).await?; + unsafe { + ptr.add(i).write(val); + } + } + let array = unsafe { uninit.assume_init() }; + Ok(array) + } +} + /// This isn't actually a type in the Minecraft Protocol. This is just for saving data/ or for general use. /// It was created for saving/reading chunks! impl NetDecode for HashMap diff --git a/src/lib/net/src/packets/incoming/chat_message.rs b/src/lib/net/src/packets/incoming/chat_message.rs new file mode 100644 index 00000000..ca1d4b99 --- /dev/null +++ b/src/lib/net/src/packets/incoming/chat_message.rs @@ -0,0 +1,137 @@ +use ferrumc_macros::{packet, NetDecode}; +use ferrumc_net_codec::decode::errors::NetDecodeError; +use ferrumc_net_codec::decode::{NetDecode, NetDecodeOpts, NetDecodeResult}; +use ferrumc_net_codec::net_types::{var_int::VarInt}; +use std::io::Read; +use tokio::io::AsyncRead; +use tokio::io::AsyncReadExt; + +// According to packets.json, serverbound play chat packet is ID 7. +// The wiki.vg page for 1.21 protocol also lists "Serverbound Chat Message" as 0x07 in Play state. +// https://wiki.vg/Protocol#Chat_Message_.28serverbound.29 +// Fields: Message (String), Timestamp (Long), Salt (Long), Signature (Optional ByteArray), Message Count (VarInt), Acknowledged (BitSet) + +// For a basic MVP, we'll just parse the message string. +// More fields can be added later as needed. + +#[derive(NetDecode, Debug)] +#[packet(packet_id = "chat", state = "play")] +#[allow(unused)] +pub struct ChatMessage { + pub message: String, + timestamp: i64, + /// The salt used to verify the signature hash. + salt: i64, + signature: ChatSignature, + message_count: VarInt, + acknowledged: ChatAckBitSet, + checksum: u8, +} + +#[derive(Debug)] +pub struct ChatSignature { + ///The signature used to verify the chat message's authentication. When present, always 256 bytes and not length-prefixed. + pub signature: Option<[u8; 256]>, +} + +impl NetDecode for ChatSignature { + fn decode(reader: &mut R, opts: &NetDecodeOpts) -> NetDecodeResult { + // Whether the next field is present. + let has_signature = ::decode(reader, opts)?; + if !has_signature { + return Ok(ChatSignature { signature: None }); + } + + let mut signature = [0; 256]; + reader.read_exact(&mut signature)?; + + Ok(ChatSignature { + signature: Some(signature), + }) + } + + async fn decode_async( + reader: &mut R, + opts: &NetDecodeOpts, + ) -> NetDecodeResult { + let has_signature = ::decode_async(reader, opts).await?; + if !has_signature { + return Ok(ChatSignature { signature: None }); + } + + let mut signature = [0; 256]; + reader.read_exact(&mut signature).await?; + + Ok(ChatSignature { + signature: Some(signature), + }) + } +} + +#[derive(Debug)] +#[allow(unused)] +pub struct ChatAckBitSet { + /// Fixed BitSet (20). + // + // Bit sets of type Fixed BitSet (n) have a fixed length of n bits, encoded as ceil(n / 8) bytes. Note that this is different from BitSet, which uses longs. + // Field Name Field Type Meaning + // Data Byte Array (n) A packed representation of the bit set as created by BitSet.toByteArray, padded with zeroes at the end to fit the specified length. + // + // The ith bit is set when (Data[i / 8] & (1 << (i % 8))) != 0, where i starts at 0. This encoding is not equivalent to the long array in BitSet. + bits: Vec, +} + +impl NetDecode for ChatAckBitSet { + fn decode(reader: &mut R, opts: &NetDecodeOpts) -> NetDecodeResult { + let length = VarInt::decode(reader, opts)?.0 as usize; + if length % 8 != 0 { + return Err(NetDecodeError::InvalidLength { + expected: length, + actual: 0, + field: "ChatAckBitSet".to_string(), + }); + } + + let mut data = vec![0u8; length / 8]; + reader.read_exact(&mut data)?; + + let mut bits = Vec::new(); + for chunk in data.chunks(8) { + let mut value = 0u64; + for (i, byte) in chunk.iter().enumerate() { + value |= (*byte as u64) << (i * 8); + } + bits.push(value); + } + + Ok(ChatAckBitSet { bits }) + } + + async fn decode_async( + reader: &mut R, + opts: &NetDecodeOpts, + ) -> NetDecodeResult { + let length = VarInt::decode_async(reader, opts).await?.0 as usize; + if length % 8 != 0 { + return Err(NetDecodeError::InvalidLength { + expected: length, + actual: 0, + field: "ChatAckBitSet".to_string(), + }); + } + + let mut data = vec![0u8; length / 8]; + reader.read_exact(&mut data).await?; + + let mut bits = Vec::new(); + for chunk in data.chunks(8) { + let mut value = 0u64; + for (i, byte) in chunk.iter().enumerate() { + value |= (*byte as u64) << (i * 8); + } + bits.push(value); + } + + Ok(ChatAckBitSet { bits }) + } +} diff --git a/src/lib/net/src/packets/incoming/mod.rs b/src/lib/net/src/packets/incoming/mod.rs index adbd4679..57aeb42b 100644 --- a/src/lib/net/src/packets/incoming/mod.rs +++ b/src/lib/net/src/packets/incoming/mod.rs @@ -1,4 +1,5 @@ pub mod ack_finish_configuration; +pub mod chat_message; pub mod client_information; pub mod handshake; pub mod login_acknowledged; diff --git a/src/lib/net/src/packets/outgoing/spawn_entity.rs b/src/lib/net/src/packets/outgoing/spawn_entity.rs index 2b2adb37..c5f77269 100644 --- a/src/lib/net/src/packets/outgoing/spawn_entity.rs +++ b/src/lib/net/src/packets/outgoing/spawn_entity.rs @@ -1,6 +1,7 @@ use crate::errors::NetError; use bevy_ecs::prelude::{Entity, Query}; +use ferrumc_core::entities::entity_kind::EntityKind; use ferrumc_core::identity::player_identity::PlayerIdentity; use ferrumc_core::transform::position::Position; use ferrumc_core::transform::rotation::Rotation; @@ -9,7 +10,7 @@ use ferrumc_net_codec::net_types::angle::NetAngle; use ferrumc_net_codec::net_types::var_int::VarInt; use std::io::Write; -#[derive(NetEncode)] +#[derive(NetEncode, Clone)] #[packet(packet_id = "add_entity", state = "play")] pub struct SpawnEntityPacket { entity_id: VarInt, @@ -55,4 +56,30 @@ impl SpawnEntityPacket { velocity_z: 0, }) } + + pub fn entity( + entity_id: Entity, + pos: &Position, + rot: &Rotation, + entity_kind: &EntityKind, + ) -> Result { + // generate a uuid based on the entity ID + let entity_uuid = entity_id.index() as u128; // Placeholder, UUIDs are not used for non-player entities + + Ok(Self { + entity_id: VarInt::new(entity_id.index() as i32), + entity_uuid, + r#type: VarInt::new(entity_kind.get_id() as i32), + x: pos.x, + y: pos.y, + z: pos.z, + pitch: NetAngle::from_degrees(rot.pitch as f64), + yaw: NetAngle::from_degrees(rot.yaw as f64), + head_yaw: NetAngle::from_degrees(rot.yaw as f64), + data: VarInt::new(0), // Placeholder for entity data + velocity_x: 0, + velocity_y: 0, + velocity_z: 0, + }) + } }