diff --git a/.etc/example-config.toml b/.etc/example-config.toml index cc43e3fc..a0cef8e2 100644 --- a/.etc/example-config.toml +++ b/.etc/example-config.toml @@ -36,3 +36,5 @@ map_size = 1_000 cache_ttl = 60 # How big the cache can be in kb. cache_capacity = 20_000 + +whitelist = false \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 69f49006..cea4a6b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,7 +93,7 @@ ferrumc-core = { path = "src/lib/core" } ferrumc-default-commands = { path = "src/lib/default_commands" } ferrumc-commands = { path = "src/lib/commands" } ferrumc-ecs = { path = "src/lib/ecs" } -ferrumc-events = { path = "src/lib/events" } +ferrumc-events = { path = "src/lib/events"} ferrumc-general-purpose = { path = "src/lib/utils/general_purpose" } ferrumc-logging = { path = "src/lib/utils/logging" } ferrumc-macros = { path = "src/lib/derive_macros" } @@ -120,6 +120,7 @@ async-trait = "0.1.82" # Logging tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } +tracing-appender = "0.2.3" log = "0.4.22" console-subscriber = "0.4.1" diff --git a/README.md b/README.md index 441bce29..ba5ff22a 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ our [Discord server](https://discord.gg/qT5J8EMjwk) for help or to discuss the p Server list
  • -

    🚄 Extremely fast and adaptable update speeds

    +

    🚄 Extremely fast

    Mind boggling
  • @@ -50,7 +50,7 @@ our [Discord server](https://discord.gg/qT5J8EMjwk) for help or to discuss the p Low memory usage
  • -

    🗂️ Customizable configuration

    +

    🗂️ Straightforward Configuration

    Configuration
  • @@ -58,7 +58,7 @@ our [Discord server](https://discord.gg/qT5J8EMjwk) for help or to discuss the p Configuration
  • -

    🌐 Compatible with vanilla Minecraft clients (Currently only 1.21.1)

    +

    🌐 Compatible with vanilla Minecraft clients (Version 1.21.1)

  • 💪 Powerful Entity Component System to handle high entity loads

    @@ -72,7 +72,7 @@ our [Discord server](https://discord.gg/qT5J8EMjwk) for help or to discuss the p

    📝 Custom made network, NBT and Anvil encoding systems to allow for minimal I/O lag

  • -

    💾 Multiple database options to finetune the server to your needs

    +

    💾 Crazy fast K/V database

    32 render distance* Chunk Loading DEMO
  • @@ -82,19 +82,19 @@ our [Discord server](https://discord.gg/qT5J8EMjwk) for help or to discuss the p @@ -148,9 +148,23 @@ cargo build --release ## 🖥️ Usage +```plaintext +Usage: ferrumc.exe [OPTIONS] [COMMAND] + +Commands: +setup Sets up the config +import Import the world data +run Start the server (default, if no command is given) +help Print this message or the help of the given subcommand(s) + +Options: +--log [default: debug] [possible values: trace, debug, info, warn, error] +-h, --help Print help +``` + 1. Move the FerrumC binary (`ferrumc.exe` or `ferrumc` depending on the OS) to your desired server directory 2. Open a terminal in that directory -3. (Optional) Generate a config file: `./ferrumc --setup` +3. (Optional) Generate a config file: `./ferrumc setup` - Edit the generated `config.toml` file to customize your server settings 4. Import an existing world: Either copy your world files to the server directory or specify the path to the world files in the `config.toml` file. This should be the root directory of your world files, containing the `region` directory @@ -218,10 +232,9 @@ with the vanilla server, but we do plan on implementing some sort of terrain gen ### Will there be plugins? And how? -We do very much plan to have a plugin system and as of right now, our plan is to leverage the -JVM to allow for plugins to be written in Kotlin, Java, or any other JVM language. We are also considering other -languages -such as Rust, JavaScript and possibly other native languages, but that is a fair way off for now. +We do very much plan to have a plugin system and as of right now we are planning to use +some kind of ffi (foreign function interface) to allow for plugins to be written in other languages. +Not confirmed yet. ### What does 'FerrumC' mean? @@ -240,4 +253,11 @@ This project is licensed under the MIT License - see the [LICENSE.md](LICENSE) f Star History Chart - \ No newline at end of file + + +## 📊 Stats + +[![Timeline graph](https://images.repography.com/59032276/ferrumc-rs/ferrumc/recent-activity/J6CgGhzs6y3LXRuADz1QpSUriBC3ix9DXnPUbbljruA/O-qGFiSVQmksFEaX7mVQ4jY3lppUTK2xUw4CpqZ3oUk_timeline.svg)](https://github.com/ferrumc-rs/ferrumc/commits) +[![Issue status graph](https://images.repography.com/59032276/ferrumc-rs/ferrumc/recent-activity/J6CgGhzs6y3LXRuADz1QpSUriBC3ix9DXnPUbbljruA/O-qGFiSVQmksFEaX7mVQ4jY3lppUTK2xUw4CpqZ3oUk_issues.svg)](https://github.com/ferrumc-rs/ferrumc/issues) +[![Pull request status graph](https://images.repography.com/59032276/ferrumc-rs/ferrumc/recent-activity/J6CgGhzs6y3LXRuADz1QpSUriBC3ix9DXnPUbbljruA/O-qGFiSVQmksFEaX7mVQ4jY3lppUTK2xUw4CpqZ3oUk_prs.svg)](https://github.com/ferrumc-rs/ferrumc/pulls) +[![Top contributors](https://images.repography.com/59032276/ferrumc-rs/ferrumc/recent-activity/J6CgGhzs6y3LXRuADz1QpSUriBC3ix9DXnPUbbljruA/O-qGFiSVQmksFEaX7mVQ4jY3lppUTK2xUw4CpqZ3oUk_users.svg)](https://github.com/ferrumc-rs/ferrumc/graphs/contributors) diff --git a/scripts/new_packet.py b/scripts/new_packet.py new file mode 100644 index 00000000..0f493608 --- /dev/null +++ b/scripts/new_packet.py @@ -0,0 +1,64 @@ +import os.path + +incoming_template = """ +use crate::packets::IncomingPacket; +use crate::NetResult; +use ferrumc_macros::{packet, NetDecode}; +use ferrumc_state::ServerState; +use std::sync::Arc; + +#[derive(NetDecode)] +#[packet(packet_id = ++id++, state = "play")] +pub struct ++name++ { +} + +impl IncomingPacket for ++name++ { + async fn handle(self, conn_id: usize, state: Arc) -> NetResult<()> { + todo!() + } +} +""" + +outgoing_template = """ +use ferrumc_macros::{packet, NetEncode};\ +use std::io::Write; + +#[derive(NetEncode)] +#[packet(packet_id = ++id++)] +pub struct ++name++ {} +""" + + +def to_snake_case(string) -> str: + return string.lower().replace(" ", "_") + + +def to_camel_case(string) -> str: + return string.title().replace(" ", "") + + +packet_type_input = input("Incoming or outgoing packet? (i/o): ") +packet_type = "" +if packet_type_input == "i": + packet_type = "incoming" +elif packet_type_input == "o": + packet_type = "outgoing" +else: + print("Invalid input") + exit() + +packet_name = input("Packet name: ") +packets_dir = os.path.join(os.path.join(os.path.dirname(__file__), ".."), "src/lib/net/src/packets") + +packet_id = input("Packet ID (formatted like 0x01): ") +packet_id = packet_id[:-2] + packet_id[-2:].upper() + +with open(f"{packets_dir}/{packet_type}/{to_snake_case(packet_name)}.rs", "x") as f: + if packet_type == "incoming": + f.write(incoming_template.replace("++name++", to_camel_case(packet_name)).replace("++id++", packet_id)) + with open(f"{packets_dir}/incoming/mod.rs", "a") as modfile: + modfile.write(f"\npub mod {to_snake_case(packet_name)};") + else: + f.write(outgoing_template.replace("++name++", to_camel_case(packet_name)).replace("++id++", packet_id)) + with open(f"{packets_dir}/outgoing/mod.rs", "a") as modfile: + modfile.write(f"\npub mod {to_snake_case(packet_name)};") diff --git a/src/bin/Cargo.toml b/src/bin/Cargo.toml index 0d2d9c3f..1b8040fe 100644 --- a/src/bin/Cargo.toml +++ b/src/bin/Cargo.toml @@ -30,15 +30,14 @@ ferrumc-commands = { workspace = true } ferrumc-default-commands = { workspace = true } ferrumc-text = { workspace = true } -ctor = { workspace = true } parking_lot = { workspace = true, features = ["deadlock_detection"] } tracing = { workspace = true } tokio = { workspace = true } -rayon = { workspace = true } futures = { workspace = true } -serde_json = { workspace = true } async-trait = { workspace = true } clap = { workspace = true, features = ["derive"] } +flate2 = { workspace = true } +ctor = { workspace = true } [[bin]] diff --git a/src/bin/src/main.rs b/src/bin/src/main.rs index 1321ada1..8c7ab4c7 100644 --- a/src/bin/src/main.rs +++ b/src/bin/src/main.rs @@ -16,7 +16,7 @@ use ferrumc_world::World; use std::hash::{Hash, Hasher}; use std::sync::Arc; use systems::definition; -use tracing::{error, info}; +use tracing::{error, info, trace}; pub(crate) mod errors; use crate::cli::{CLIArgs, Command, ImportArgs}; @@ -37,11 +37,11 @@ async fn main() { let mut hasher = std::collections::hash_map::DefaultHasher::new(); std::any::TypeId::of::().hash(&mut hasher); let digest = hasher.finish(); - println!("ChunkReceiver: {:X}", digest); + trace!("ChunkReceiver: {:X}", digest); let mut hasher = std::collections::hash_map::DefaultHasher::new(); std::any::TypeId::of::().hash(&mut hasher); let digest = hasher.finish(); - println!("StreamWriter: {:X}", digest); + trace!("StreamWriter: {:X}", digest); } match cli_args.command { diff --git a/src/bin/src/packet_handlers/animations.rs b/src/bin/src/packet_handlers/animations.rs new file mode 100644 index 00000000..61ff53eb --- /dev/null +++ b/src/bin/src/packet_handlers/animations.rs @@ -0,0 +1,21 @@ +use ferrumc_macros::event_handler; +use ferrumc_net::errors::NetError; +use ferrumc_net::packets::outgoing::entity_animation::EntityAnimationEvent; +use ferrumc_net::utils::broadcast::{broadcast, BroadcastOptions}; +use ferrumc_state::GlobalState; + +#[event_handler] +async fn entity_animation( + event: EntityAnimationEvent, + state: GlobalState, +) -> Result { + //TODO change this global broadcast to a broadcast that affects only players in the view distance + // of the player doing it, but as long as we still cant see other players, this will be fine. + broadcast( + &event.packet, + &state, + BroadcastOptions::default().except([event.entity]), + ) + .await?; + Ok(event) +} diff --git a/src/bin/src/packet_handlers/login_process.rs b/src/bin/src/packet_handlers/login_process.rs index 4c6a9ab5..f992eb33 100644 --- a/src/bin/src/packet_handlers/login_process.rs +++ b/src/bin/src/packet_handlers/login_process.rs @@ -6,6 +6,7 @@ use ferrumc_core::transform::grounded::OnGround; use ferrumc_core::transform::position::Position; use ferrumc_core::transform::rotation::Rotation; use ferrumc_ecs::components::storage::ComponentRefMut; +use ferrumc_ecs::entities::Entity; use ferrumc_macros::event_handler; use ferrumc_net::connection::{ConnectionState, StreamWriter}; use ferrumc_net::errors::NetError; @@ -21,13 +22,19 @@ use ferrumc_net::packets::outgoing::keep_alive::OutgoingKeepAlivePacket; use ferrumc_net::packets::outgoing::login_disconnect::LoginDisconnectPacket; use ferrumc_net::packets::outgoing::login_play::LoginPlayPacket; use ferrumc_net::packets::outgoing::login_success::LoginSuccessPacket; +use ferrumc_net::packets::outgoing::player_info_update::PlayerInfoUpdatePacket; use ferrumc_net::packets::outgoing::registry_data::get_registry_packets; use ferrumc_net::packets::outgoing::set_center_chunk::SetCenterChunk; use ferrumc_net::packets::outgoing::set_default_spawn_position::SetDefaultSpawnPositionPacket; use ferrumc_net::packets::outgoing::set_render_distance::SetRenderDistance; +use ferrumc_net::packets::outgoing::spawn_entity::SpawnEntityPacket; use ferrumc_net::packets::outgoing::synchronize_player_position::SynchronizePlayerPositionPacket; +use ferrumc_net::utils::broadcast::{broadcast, get_all_play_players, BroadcastOptions}; +use ferrumc_net::NetResult; use ferrumc_net_codec::encode::NetEncodeOpts; use ferrumc_state::GlobalState; +use futures::StreamExt; +use std::time::Instant; use tracing::{debug, trace}; #[event_handler] @@ -47,7 +54,7 @@ async fn handle_login_start( login_start_event.conn_id, PlayerIdentity::new(username.to_string(), uuid), )? - .add_component::(login_start_event.conn_id, ChunkReceiver::default())?; + /*.add_component::(login_start_event.conn_id, ChunkReceiver::default())?*/; //Send a Login Success Response to further the login sequence let mut writer = state @@ -146,76 +153,78 @@ async fn handle_ack_finish_configuration( state: GlobalState, ) -> Result { trace!("Handling Ack Finish Configuration event"); - - let conn_id = ack_finish_configuration_event.conn_id; - - let mut conn_state = state.universe.get_mut::(conn_id)?; - - *conn_state = ConnectionState::Play; - - // add components to the entity after the connection state has been set to play. - // to avoid wasting resources on entities that are fetching stuff like server status etc. - state - .universe - .add_component::(conn_id, Position::default())? - .add_component::(conn_id, Rotation::default())? - .add_component::(conn_id, OnGround::default())?; - - let mut writer = state.universe.get_mut::(conn_id)?; - - writer // 21 - .send_packet(&LoginPlayPacket::new(conn_id), &NetEncodeOpts::WithLength) - .await?; - writer // 29 - .send_packet( - &SynchronizePlayerPositionPacket::default(), // The coordinates here should be used for the center chunk. - &NetEncodeOpts::WithLength, - ) - .await?; - writer // 37 - .send_packet( - &SetDefaultSpawnPositionPacket::default(), // Player specific, aka. home, bed, where it would respawn. - &NetEncodeOpts::WithLength, - ) - .await?; - writer // 38 - .send_packet( - &GameEventPacket::start_waiting_for_level_chunks(), - &NetEncodeOpts::WithLength, - ) - .await?; - writer // 41 - .send_packet( - &SetCenterChunk::new(0, 0), // TODO - Dependent on the player spawn position. - &NetEncodeOpts::WithLength, - ) - .await?; - writer // other - .send_packet( - &SetRenderDistance::new(5), // TODO - &NetEncodeOpts::WithLength, - ) - .await?; - trace!( - "Sending command graph: {:#?}", - ferrumc_commands::infrastructure::get_graph() - ); - writer - .send_packet(&CommandsPacket::create(), &NetEncodeOpts::WithLength) - .await?; - - let pos = state.universe.get_mut::(conn_id)?; - let mut chunk_recv = state.universe.get_mut::(conn_id)?; - chunk_recv.last_chunk = Some((pos.x as i32, pos.z as i32, String::from("overworld"))); - chunk_recv.calculate_chunks().await; - - send_keep_alive(conn_id, state, &mut writer).await?; + let entity_id = ack_finish_configuration_event.conn_id; + { + let mut conn_state = state.universe.get_mut::(entity_id)?; + + *conn_state = ConnectionState::Play; + + // add components to the entity after the connection state has been set to play. + // to avoid wasting resources on entities that are fetching stuff like server status etc. + state + .universe + .add_component::(entity_id, Position::default())? + .add_component::(entity_id, Rotation::default())? + .add_component::(entity_id, OnGround::default())? + .add_component::(entity_id, ChunkReceiver::default())?; + + let mut writer = state.universe.get_mut::(entity_id)?; + + writer // 21 + .send_packet(&LoginPlayPacket::new(entity_id), &NetEncodeOpts::WithLength) + .await?; + writer // 29 + .send_packet( + &SynchronizePlayerPositionPacket::default(), // The coordinates here should be used for the center chunk. + &NetEncodeOpts::WithLength, + ) + .await?; + writer // 37 + .send_packet( + &SetDefaultSpawnPositionPacket::default(), // Player specific, aka. home, bed, where it would respawn. + &NetEncodeOpts::WithLength, + ) + .await?; + writer // 38 + .send_packet( + &GameEventPacket::start_waiting_for_level_chunks(), + &NetEncodeOpts::WithLength, + ) + .await?; + writer // 41 + .send_packet( + &SetCenterChunk::new(0, 0), // TODO - Dependent on the player spawn position. + &NetEncodeOpts::WithLength, + ) + .await?; + writer // other + .send_packet( + &SetRenderDistance::new(5), // TODO + &NetEncodeOpts::WithLength, + ) + .await?; + + trace!( + "Sending command graph: {:#?}", + ferrumc_commands::infrastructure::get_graph() + ); + writer + .send_packet(&CommandsPacket::create(), &NetEncodeOpts::WithLength) + .await?; + + send_keep_alive(entity_id, &state, &mut writer).await?; + + let pos = state.universe.get_mut::(entity_id)?; + let mut chunk_recv = state.universe.get_mut::(entity_id)?; + chunk_recv.last_chunk = Some((pos.x as i32, pos.z as i32, String::from("overworld"))); + chunk_recv.calculate_chunks().await; + } Ok(ack_finish_configuration_event) } async fn send_keep_alive( conn_id: usize, - state: GlobalState, + state: &GlobalState, writer: &mut ComponentRefMut<'_, StreamWriter>, ) -> Result<(), NetError> { let keep_alive_packet = OutgoingKeepAlivePacket::default(); @@ -234,3 +243,63 @@ async fn send_keep_alive( Ok(()) } + +async fn player_info_update_packets(entity_id: Entity, state: &GlobalState) -> NetResult<()> { + // Broadcasts a player info update packet to all players. + { + let packet = PlayerInfoUpdatePacket::new_player_join_packet(entity_id, state); + + let start = Instant::now(); + broadcast( + &packet, + state, + BroadcastOptions::default().except([entity_id]), + ) + .await?; + trace!( + "Broadcasting player info update took: {:?}", + start.elapsed() + ); + } + + // Tell the player about all the other players that are already connected. + { + let packet = PlayerInfoUpdatePacket::existing_player_info_packet(entity_id, state); + + let start = Instant::now(); + let mut writer = state.universe.get_mut::(entity_id)?; + writer + .send_packet(&packet, &NetEncodeOpts::WithLength) + .await?; + debug!("Sending player info update took: {:?}", start.elapsed()); + } + + Ok(()) +} + +async fn broadcast_spawn_entity_packet(entity_id: Entity, state: &GlobalState) -> NetResult<()> { + let packet = SpawnEntityPacket::player(entity_id, state)?; + + let start = Instant::now(); + broadcast( + &packet, + state, + BroadcastOptions::default().except([entity_id]), + ) + .await?; + trace!("Broadcasting spawn entity took: {:?}", start.elapsed()); + + let writer = state.universe.get_mut::(entity_id)?; + futures::stream::iter(get_all_play_players(state)) + .fold(writer, |mut writer, entity| async move { + if let Ok(packet) = SpawnEntityPacket::player(entity, state) { + let _ = writer + .send_packet(&packet, &NetEncodeOpts::WithLength) + .await; + } + writer + }) + .await; + + Ok(()) +} diff --git a/src/bin/src/packet_handlers/mod.rs b/src/bin/src/packet_handlers/mod.rs index 59affd21..2f50c206 100644 --- a/src/bin/src/packet_handlers/mod.rs +++ b/src/bin/src/packet_handlers/mod.rs @@ -1,6 +1,8 @@ mod chat_message; mod commands; +mod animations; mod handshake; mod login_process; +mod player; +mod player_leave; mod tick_handler; -mod transform; diff --git a/src/bin/src/packet_handlers/player/do_action.rs b/src/bin/src/packet_handlers/player/do_action.rs new file mode 100644 index 00000000..de89aac9 --- /dev/null +++ b/src/bin/src/packet_handlers/player/do_action.rs @@ -0,0 +1,38 @@ +use ferrumc_macros::event_handler; +use ferrumc_net::errors::NetError; +use ferrumc_net::packets::incoming::player_command::{PlayerCommandAction, PlayerDoActionEvent}; +use ferrumc_net::packets::outgoing::entity_metadata::{EntityMetadata, EntityMetadataPacket}; +use ferrumc_net::utils::broadcast::broadcast; +use ferrumc_state::GlobalState; +use tracing::trace; + +#[event_handler] +async fn handle_player_do_action( + event: PlayerDoActionEvent, + state: GlobalState, +) -> Result { + trace!("player just did: {:?}", event.action); + + match event.action { + PlayerCommandAction::StartSneaking => { + let packet = EntityMetadataPacket::new( + event.entity_id, + [ + EntityMetadata::entity_sneaking_visual(), + EntityMetadata::entity_sneaking_pressed(), + ], + ); + + broadcast(&packet, &state, Default::default()).await?; + } + PlayerCommandAction::StopSneaking => { + let packet = + EntityMetadataPacket::new(event.entity_id, [EntityMetadata::entity_standing()]); + + broadcast(&packet, &state, Default::default()).await?; + } + _ => {} + } + + Ok(event) +} diff --git a/src/bin/src/packet_handlers/player/head_rot.rs b/src/bin/src/packet_handlers/player/head_rot.rs new file mode 100644 index 00000000..a5b60f34 --- /dev/null +++ b/src/bin/src/packet_handlers/player/head_rot.rs @@ -0,0 +1,33 @@ +use ferrumc_core::transform::rotation::Rotation; +use ferrumc_macros::event_handler; +use ferrumc_net::errors::NetError; +use ferrumc_net::packets::outgoing::set_head_rotation::SetHeadRotationPacket; +use ferrumc_net::packets::packet_events::TransformEvent; +use ferrumc_net::utils::broadcast::{broadcast, BroadcastOptions}; +use ferrumc_net::utils::ecs_helpers::EntityExt; +use ferrumc_net_codec::net_types::angle::NetAngle; +use ferrumc_state::GlobalState; + +#[event_handler(priority = "normal")] +async fn handle_player_move( + event: TransformEvent, + state: GlobalState, +) -> Result { + let entity = event.conn_id; + + // let pos = entity.get::(&state)?; + let rot = entity.get::(&state)?; + // let grounded = entity.get::(&state)?; + + // let teleport_packet = TeleportEntityPacket::new(entity, &pos, &rot, grounded.0); + let head_rot_packet = + SetHeadRotationPacket::new(entity as i32, NetAngle::from_degrees(rot.yaw as f64)); + + let start = std::time::Instant::now(); + // broadcast(&teleport_packet, &state, BroadcastOptions::default().all()).await?; + broadcast(&head_rot_packet, &state, BroadcastOptions::default().all()).await?; + + tracing::trace!("broadcasting entity move took {:?}", start.elapsed()); + + Ok(event) +} diff --git a/src/bin/src/packet_handlers/player/mod.rs b/src/bin/src/packet_handlers/player/mod.rs new file mode 100644 index 00000000..0b3c4247 --- /dev/null +++ b/src/bin/src/packet_handlers/player/mod.rs @@ -0,0 +1,3 @@ +pub mod do_action; +pub mod head_rot; +pub mod update_player_position; diff --git a/src/bin/src/packet_handlers/player/update_player_position.rs b/src/bin/src/packet_handlers/player/update_player_position.rs new file mode 100644 index 00000000..9fb4fe47 --- /dev/null +++ b/src/bin/src/packet_handlers/player/update_player_position.rs @@ -0,0 +1,153 @@ +use ferrumc_core::chunks::chunk_receiver::ChunkReceiver; +use ferrumc_core::transform::grounded::OnGround; +use ferrumc_core::transform::position::Position; +use ferrumc_core::transform::rotation::Rotation; +use ferrumc_ecs::entities::Entity; +use ferrumc_macros::{event_handler, NetEncode}; +use ferrumc_net::errors::NetError; +use ferrumc_net::packets::outgoing::teleport_entity::TeleportEntityPacket; +use ferrumc_net::packets::outgoing::update_entity_position::UpdateEntityPositionPacket; +use ferrumc_net::packets::outgoing::update_entity_position_and_rotation::UpdateEntityPositionAndRotationPacket; +use ferrumc_net::packets::outgoing::update_entity_rotation::UpdateEntityRotationPacket; +use ferrumc_net::packets::packet_events::TransformEvent; +use ferrumc_net::utils::broadcast::{broadcast, BroadcastOptions}; +use ferrumc_net::utils::ecs_helpers::EntityExt; +use ferrumc_net::NetResult; +use ferrumc_state::GlobalState; +use tracing::trace; + +#[event_handler(priority = "fastest")] +async fn handle_player_move( + event: TransformEvent, + state: GlobalState, +) -> Result { + let conn_id = event.conn_id; + + let mut delta_pos = None::<(i16, i16, i16)>; + let mut new_rot = None::; + + if let Some(ref new_position) = event.position { + trace!("Getting chunk_recv 1 for player move"); + { + let mut chunk_recv = state.universe.get_mut::(conn_id)?; + trace!("Got chunk_recv 1 for player move"); + if let Some(last_chunk) = &chunk_recv.last_chunk { + let new_chunk = ( + new_position.x as i32 / 16, + new_position.z as i32 / 16, + String::from("overworld"), + ); + if *last_chunk != new_chunk { + chunk_recv.last_chunk = Some(new_chunk); + chunk_recv.calculate_chunks().await; + } + } else { + chunk_recv.last_chunk = Some(( + new_position.x as i32 / 16, + new_position.z as i32 / 16, + String::from("overworld"), + )); + chunk_recv.calculate_chunks().await; + } + } + + trace!("Getting position 1 for player move"); + let mut position = conn_id.get_mut::(&state)?; + trace!("Got position 1 for player move"); + + delta_pos = Some(( + ((new_position.x * 4096.0) - (position.x * 4096.0)) as i16, + ((new_position.y * 4096.0) - (position.y * 4096.0)) as i16, + ((new_position.z * 4096.0) - (position.z * 4096.0)) as i16, + )); + + *position = Position::new(new_position.x, new_position.y, new_position.z); + } + + if let Some(ref new_rotation) = event.rotation { + trace!("Getting rotation 1 for player move"); + let mut rotation = conn_id.get_mut::(&state)?; + trace!("Got rotation 1 for player move"); + + let new_rotation = Rotation::new(new_rotation.yaw, new_rotation.pitch); + new_rot = Some(new_rotation); + + *rotation = new_rotation; + } + + if let Some(new_grounded) = event.on_ground { + trace!("Getting on_ground 1 for player move"); + let mut on_ground = conn_id.get_mut::(&state)?; + trace!("Got on_ground 1 for player move"); + + *on_ground = OnGround(new_grounded); + } + + update_pos_for_all(conn_id, delta_pos, new_rot, &state).await?; + + Ok(event) +} + +#[derive(NetEncode)] +enum BroadcastMovementPacket { + UpdateEntityPosition(UpdateEntityPositionPacket), + UpdateEntityPositionAndRotation(UpdateEntityPositionAndRotationPacket), + UpdateEntityRotation(UpdateEntityRotationPacket), + TeleportEntity(TeleportEntityPacket), +} + +async fn update_pos_for_all( + entity_id: Entity, + delta_pos: Option<(i16, i16, i16)>, + new_rot: Option, + state: &GlobalState, +) -> NetResult<()> { + let is_grounded = entity_id.get::(state)?.0; + + // If any delta of (x|y|z) exceeds 7.5, then it's "not recommended" to use this packet + // As docs say: "If the movement exceeds these limits, Teleport Entity should be sent instead." + // "should"???? + const MAX_DELTA: i16 = (7.5 * 4096f32) as i16; + let delta_exceeds_threshold = match delta_pos { + Some((delta_x, delta_y, delta_z)) => { + delta_x.abs() > MAX_DELTA || delta_y.abs() > MAX_DELTA || delta_z.abs() > MAX_DELTA + } + None => false, + }; + + let packet: BroadcastMovementPacket = if delta_exceeds_threshold { + let pos = entity_id.get::(state)?; + let rot = entity_id.get::(state)?; + let grounded = entity_id.get::(state)?.0; + + BroadcastMovementPacket::TeleportEntity(TeleportEntityPacket::new( + entity_id, &pos, &rot, grounded, + )) + } else { + match (delta_pos, new_rot) { + (Some(delta_pos), Some(new_rot)) => { + BroadcastMovementPacket::UpdateEntityPositionAndRotation( + UpdateEntityPositionAndRotationPacket::new( + entity_id, + delta_pos, + &new_rot, + is_grounded, + ), + ) + } + (Some(delta_pos), None) => BroadcastMovementPacket::UpdateEntityPosition( + UpdateEntityPositionPacket::new(entity_id, delta_pos, is_grounded), + ), + (None, Some(new_rot)) => BroadcastMovementPacket::UpdateEntityRotation( + UpdateEntityRotationPacket::new(entity_id, &new_rot, is_grounded), + ), + _ => { + return Ok(()); + } + } + }; + + broadcast(&packet, state, BroadcastOptions::default().all()).await?; + + Ok(()) +} diff --git a/src/bin/src/packet_handlers/player_leave.rs b/src/bin/src/packet_handlers/player_leave.rs new file mode 100644 index 00000000..8acf2442 --- /dev/null +++ b/src/bin/src/packet_handlers/player_leave.rs @@ -0,0 +1,28 @@ +use ferrumc_macros::event_handler; +use ferrumc_net::connection::PlayerDisconnectEvent; +use ferrumc_net::errors::NetError; +use ferrumc_net::packets::outgoing::remove_entities::RemoveEntitiesPacket; +use ferrumc_net::utils::broadcast::{broadcast, BroadcastOptions}; +use ferrumc_state::GlobalState; +use tracing::info; + +#[event_handler] +async fn handle_player_disconnect( + event: PlayerDisconnectEvent, + state: GlobalState, +) -> Result { + let entity_id = event.entity_id; + + info!("Player disconnected: {:?}", entity_id); + + let remove_entity_packet = RemoveEntitiesPacket::from_entities([entity_id]); + + broadcast( + &remove_entity_packet, + &state, + BroadcastOptions::default().all(), + ) + .await?; + + Ok(event) +} diff --git a/src/bin/src/packet_handlers/transform/mod.rs b/src/bin/src/packet_handlers/transform/mod.rs deleted file mode 100644 index 3448d5b1..00000000 --- a/src/bin/src/packet_handlers/transform/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod update_player_position; diff --git a/src/bin/src/packet_handlers/transform/update_player_position.rs b/src/bin/src/packet_handlers/transform/update_player_position.rs deleted file mode 100644 index e262e267..00000000 --- a/src/bin/src/packet_handlers/transform/update_player_position.rs +++ /dev/null @@ -1,66 +0,0 @@ -use ferrumc_core::chunks::chunk_receiver::ChunkReceiver; -use ferrumc_core::transform::grounded::OnGround; -use ferrumc_core::transform::position::Position; -use ferrumc_core::transform::rotation::Rotation; -use ferrumc_macros::event_handler; -use ferrumc_net::errors::NetError; -use ferrumc_net::packets::packet_events::TransformEvent; -use ferrumc_net::utils::ecs_helpers::EntityExt; -use ferrumc_state::GlobalState; -use tracing::trace; - -#[event_handler] -async fn handle_player_move( - event: TransformEvent, - state: GlobalState, -) -> Result { - let conn_id = event.conn_id; - if let Some(ref new_position) = event.position { - trace!("Getting chunk_recv 1 for player move"); - { - let mut chunk_recv = state.universe.get_mut::(conn_id)?; - trace!("Got chunk_recv 1 for player move"); - if let Some(last_chunk) = &chunk_recv.last_chunk { - let new_chunk = ( - new_position.x as i32 / 16, - new_position.z as i32 / 16, - String::from("overworld"), - ); - if *last_chunk != new_chunk { - chunk_recv.last_chunk = Some(new_chunk); - chunk_recv.calculate_chunks().await; - } - } else { - chunk_recv.last_chunk = Some(( - new_position.x as i32 / 16, - new_position.z as i32 / 16, - String::from("overworld"), - )); - chunk_recv.calculate_chunks().await; - } - } - - trace!("Getting position 1 for player move"); - let mut position = conn_id.get_mut::(&state)?; - trace!("Got position 1 for player move"); - *position = Position::new(new_position.x, new_position.y, new_position.z); - } - - if let Some(ref new_rotation) = event.rotation { - trace!("Getting rotation 1 for player move"); - let mut rotation = conn_id.get_mut::(&state)?; - trace!("Got rotation 1 for player move"); - - *rotation = Rotation::new(new_rotation.yaw, new_rotation.pitch); - } - - if let Some(new_grounded) = event.on_ground { - trace!("Getting on_ground 1 for player move"); - let mut on_ground = conn_id.get_mut::(&state)?; - trace!("Got on_ground 1 for player move"); - - *on_ground = OnGround(new_grounded); - } - - Ok(event) -} diff --git a/src/bin/src/systems/chunk_fetcher.rs b/src/bin/src/systems/chunk_fetcher.rs index 47741926..601a34c4 100644 --- a/src/bin/src/systems/chunk_fetcher.rs +++ b/src/bin/src/systems/chunk_fetcher.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; use std::sync::atomic::AtomicBool; use std::sync::Arc; use tokio::task::JoinSet; -use tracing::{error, info, trace}; +use tracing::{debug, info, trace}; pub struct ChunkFetcher { stop: AtomicBool, @@ -70,11 +70,11 @@ impl System for ChunkFetcher { match result { Ok(task_res) => { if let Err(e) = task_res { - error!("Error fetching chunk: {:?}", e); + debug!("Error fetching chunk: {:?}", e); } } Err(e) => { - error!("Error fetching chunk: {:?}", e); + debug!("Error fetching chunk: {:?}", e); } } } diff --git a/src/bin/src/systems/ticking_system.rs b/src/bin/src/systems/ticking_system.rs index ffe6f21f..6e72394f 100644 --- a/src/bin/src/systems/ticking_system.rs +++ b/src/bin/src/systems/ticking_system.rs @@ -7,7 +7,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Duration; use tokio::time::Instant; -use tracing::{debug, info}; +use tracing::{debug, info, trace}; pub struct TickingSystem; static KILLED: AtomicBool = AtomicBool::new(false); @@ -19,12 +19,18 @@ impl System for TickingSystem { let mut tick = 0; while !KILLED.load(Ordering::Relaxed) { let required_end = Instant::now() + Duration::from_millis(50); - // TODO handle error - let res = TickEvent::trigger(TickEvent::new(tick), state.clone()).await; + let res = { + let start = Instant::now(); + let res = TickEvent::trigger(TickEvent::new(tick), state.clone()).await; + trace!("Tick took {:?}", Instant::now() - start); + + res + }; if res.is_err() { debug!("error handling tick event: {:?}", res); } let now = Instant::now(); + if required_end > now { tokio::time::sleep(required_end - now).await; } else { diff --git a/src/lib/core/src/transform/grounded.rs b/src/lib/core/src/transform/grounded.rs index 1a0f9253..5079f15a 100644 --- a/src/lib/core/src/transform/grounded.rs +++ b/src/lib/core/src/transform/grounded.rs @@ -1,2 +1,14 @@ #[derive(Debug, Default)] pub struct OnGround(pub bool); + +impl From for OnGround { + fn from(on_ground: bool) -> Self { + Self(on_ground) + } +} + +impl From for bool { + fn from(on_ground: OnGround) -> Self { + on_ground.0 + } +} diff --git a/src/lib/derive_macros/src/net/decode.rs b/src/lib/derive_macros/src/net/decode.rs index 5a964eb9..b3407f27 100644 --- a/src/lib/derive_macros/src/net/decode.rs +++ b/src/lib/derive_macros/src/net/decode.rs @@ -1,4 +1,4 @@ -use crate::helpers::{get_derive_attributes, StructInfo}; +use crate::helpers::{extract_struct_info, get_derive_attributes, StructInfo}; use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, DeriveInput, LitStr}; @@ -6,125 +6,124 @@ use syn::{parse_macro_input, DeriveInput, LitStr}; pub(crate) fn derive(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); + // Collect attributes relevant to our `net(...)` usage: let net_attributes = get_derive_attributes(&input, "net"); let repr_attr = get_derive_attributes(&input, "repr"); - // check the type of repr attribute - let repr_attr = { - let mut repr_type = None; - repr_attr.iter().for_each(|attr| { - attr.parse_nested_meta(|meta| { - let Some(ident) = meta.path.get_ident() else { - return Ok(()); - }; - - repr_type = Some(ident.to_string()); + // Attempt to parse the `#[repr(...)]` attribute if it exists. + let repr_type = { + let mut repr_t = None; + for attr in &repr_attr { + attr.parse_nested_meta(|meta| { + if let Some(ident) = meta.path.get_ident() { + repr_t = Some(ident.to_string()); + } Ok(()) }) .unwrap(); - }); - - repr_type.map(|val| syn::parse_str::(&val).expect("Failed to parse repr type")) + } + repr_t.map(|val| syn::parse_str::(&val).expect("Failed to parse repr type")) }; - // check if any attribute that has "#[net(u8_cast)]" + // Look for `#[net(type_cast = "X", type_cast_handler = "Y")]` usage for enum casting. let (type_cast, type_cast_handler) = { - let mut type_cast = None; - let mut type_cast_handler = None; - net_attributes.iter().for_each(|attr| { + let mut cast = None; + let mut cast_handler = None; + for attr in &net_attributes { attr.parse_nested_meta(|meta| { - let Some(ident) = meta.path.get_ident() else { - return Ok(()); - }; - - match ident.to_string().as_str() { - "type_cast" => { - let value = meta.value().expect("value failed"); - let value = value.parse::().expect("parse failed"); - let n = value.value(); - type_cast = Some(n); - } - "type_cast_handler" => { - let value = meta.value().expect("value failed"); - let value = value.parse::().expect("parse failed"); - let n = value.value(); - type_cast_handler = Some(n); - } - &_ => { - return Ok(()); + if let Some(ident) = meta.path.get_ident() { + match ident.to_string().as_str() { + "type_cast" => { + let value = meta.value().expect("Missing type_cast value"); + let value = value.parse::().expect("Failed to parse type_cast"); + cast = Some(value.value()); + } + "type_cast_handler" => { + let value = meta.value().expect("Missing type_cast_handler value"); + let value = value + .parse::() + .expect("Failed to parse type_cast_handler"); + cast_handler = Some(value.value()); + } + _ => {} } } - Ok(()) }) .unwrap(); - }); - - (type_cast, type_cast_handler) + } + (cast, cast_handler) }; - // So for enums we can simply read the type and then cast it directly. + // If `type_cast` is present, we assume this is an enum. We'll decode by reading + // the specified type, then casting into the enum. if let Some(type_cast) = type_cast { - let Some(repr_attr) = repr_attr else { - panic!( - "NetDecode with type_cast enabled requires a repr attribute. Example: #[repr(u8)]" - ); + let Some(repr_ident) = repr_type else { + panic!("NetDecode with type_cast requires a repr attribute. Example: #[repr(u8)]"); }; - // in netdecode, read a type of type_cast and then if type_cast_handler exists, use it to do `type_cast_handler(type_cast)` - - let type_cast = syn::parse_str::(&type_cast).expect("Failed to parse type_cast"); + let type_cast_ty = + syn::parse_str::(&type_cast).expect("Failed to parse type_cast as a type"); let StructInfo { - struct_name: name, + struct_name: enum_name, impl_generics, ty_generics, where_clause, - lifetime: _lifetime, .. - } = crate::helpers::extract_struct_info(&input, None); + } = extract_struct_info(&input, None); - let type_cast_handler = match type_cast_handler { - None => { - quote! { value } - } - Some(handler) => { - let handler = syn::parse_str::(&handler) + let cast_handler_expr = match type_cast_handler { + None => quote!(value), + Some(handler_str) => { + let handler_expr = syn::parse_str::(&handler_str) .expect("Failed to parse type_cast_handler"); - quote! { #handler } + quote!(#handler_expr) } }; + // Build match arms for each variant's discriminant (explicit or implicit). let enum_arms = if let syn::Data::Enum(data) = &input.data { - let mut next_discriminant = 0; + let mut next_disc = 0; data.variants .iter() .map(|variant| { - let variant_name = &variant.ident; - let discriminant = if let Some((_, expr)) = &variant.discriminant { - // Use the explicit discriminant - quote! { #expr } + let variant_ident = &variant.ident; + // If the variant has a discriminant (e.g., `Variant = 5`), use that. + // Otherwise, use the running `next_disc`. + let disc_expr = if let Some((_, disc)) = &variant.discriminant { + quote! { #disc } } else { - // Use the next implicit discriminant - let disc = quote! { #next_discriminant }; - next_discriminant += 1; - disc + let disc_token = quote! { #next_disc }; + next_disc += 1; + disc_token }; quote! { - #discriminant => Ok(#name::#variant_name), + #disc_expr => Ok(#enum_name::#variant_ident), } }) .collect::>() } else { - panic!("NetDecode with type_cast enabled can only be derived for enums."); + panic!("`#[net(type_cast = ...)]` is only valid on enums."); }; let expanded = quote! { - impl #impl_generics ferrumc_net_codec::decode::NetDecode for #name #ty_generics #where_clause { - fn decode(reader: &mut R, opts: &ferrumc_net_codec::decode::NetDecodeOpts) -> ferrumc_net_codec::decode::NetDecodeResult { - let value = <#type_cast as ferrumc_net_codec::decode::NetDecode>::decode(reader, opts)?; - let value = #type_cast_handler; - let value = value as #repr_attr; + impl #impl_generics ferrumc_net_codec::decode::NetDecode + for #enum_name #ty_generics + #where_clause + { + fn decode( + reader: &mut R, + opts: &ferrumc_net_codec::decode::NetDecodeOpts + ) -> ferrumc_net_codec::decode::NetDecodeResult { + // Decode the initial numeric value + let value = <#type_cast_ty as ferrumc_net_codec::decode::NetDecode>::decode(reader, opts)?; + // Possibly transform via the handler + let value = #cast_handler_expr; + // Cast to the repr type + let value = value as #repr_ident; + + // Match against the known variant discriminants match (value as i32) { #(#enum_arms)* _ => Err(ferrumc_net_codec::decode::errors::NetDecodeError::InvalidEnumVariant), @@ -132,41 +131,126 @@ pub(crate) fn derive(input: TokenStream) -> TokenStream { } } }; - return TokenStream::from(expanded); } - let fields = if let syn::Data::Struct(data) = &input.data { - &data.fields - } else { - panic!("NetDecode can only be derived for structs or enums with u8_cast enabled."); - }; - - let decode_fields = fields.iter().map(|field| { - let field_name = field.ident.as_ref().unwrap(); - let field_ty = &field.ty; - quote! { - #field_name: <#field_ty as ferrumc_net_codec::decode::NetDecode>::decode(reader, opts)?, - } - }); - + // Otherwise, handle struct decoding. We'll check if each field has an optional trigger. let StructInfo { struct_name, impl_generics, ty_generics, where_clause, - lifetime: _lifetime, .. - } = crate::helpers::extract_struct_info(&input, None); + } = extract_struct_info(&input, None); - let expanded = quote! { - // impl ferrumc_net_codec::decode::NetDecode for #name { - impl #impl_generics ferrumc_net_codec::decode::NetDecode for #struct_name #ty_generics #where_clause { - fn decode(reader: &mut R, opts: &ferrumc_net_codec::decode::NetDecodeOpts) -> ferrumc_net_codec::decode::NetDecodeResult { - Ok(Self { - #(#decode_fields)* + let fields = match &input.data { + syn::Data::Struct(data) => &data.fields, + _ => panic!("NetDecode can only be derived for structs or for enums with `u8_cast`."), + }; + + // Generate per-field decode statements. We'll build them in order, storing + // them in local variables named the same as the field, so the subsequent fields + // can use them in the optional triggers if needed. + let mut decode_statements = Vec::new(); + let mut field_names = Vec::new(); + + for field in fields { + let field_name = field + .ident + .clone() + .expect("Unnamed fields are not currently supported"); + let field_ty = &field.ty; + + // Check for optional trigger attribute: `#[net(optional_trigger = "...expr...")]` + // or something like `#[net(optional_trigger = { some_field == true })]`. + let mut optional_trigger_expr: Option = None; + + // Check the `net(...)` attributes on this field + for attr in &field.attrs { + if attr.path().is_ident("net") { + // e.g., #[net(optional_trigger = { some_field == true })] + + attr.parse_nested_meta(|meta| { + if let Some(ident) = meta.path.get_ident() { + if ident.to_string().as_str() == "optional_trigger" { + meta.parse_nested_meta(|meta| { + if let Some(expr) = meta.path.get_ident() { + let val = syn::parse_str::(&expr.to_string()) + .expect("Failed to parse optional_trigger expression"); + + optional_trigger_expr = Some(val); + } else { + panic!("Expected an expression for optional_trigger"); + } + + Ok(()) + }) + .expect("Failed to parse optional_trigger expression"); + } + } + Ok(()) + }) + .unwrap(); + } + } + + // Generate decoding code depending on whether there's an optional trigger + if let Some(expr) = optional_trigger_expr { + // For an optional field, we decode it only if `expr` is true at runtime. + // We'll store the result in a local variable `field_name` which will be an Option. + // Then at the end, we can build the struct using those local variables. + decode_statements.push(quote! { + let #field_name = { + if #expr { + Some(<#field_ty as ferrumc_net_codec::decode::NetDecode>::decode(reader, opts)?) + } else { + None + } + }; + }); + } else { + // Check if the field is an Option and handle it accordingly. + let is_optional = { + let ty_str = quote! { #field_ty }.to_string(); + ty_str.contains("Option<") + }; + + if is_optional { + decode_statements.push(quote! { + compile_error!("Optional fields must have an `optional_trigger` attribute\n\ + Example: #[net(optional_trigger = { some_field == true })]"); }) } + + // Normal (non-optional) field decode: + decode_statements.push(quote! { + let #field_name = <#field_ty as ferrumc_net_codec::decode::NetDecode>::decode(reader, opts)?; + }); + } + + field_names.push(field_name); + } + + // After decoding everything into local variables, construct the struct. + let build_struct = quote! { + Ok(Self { + #(#field_names),* + }) + }; + + let expanded = quote! { + impl #impl_generics ferrumc_net_codec::decode::NetDecode + for #struct_name #ty_generics + #where_clause + { + fn decode( + reader: &mut R, + opts: &ferrumc_net_codec::decode::NetDecodeOpts + ) -> ferrumc_net_codec::decode::NetDecodeResult { + #(#decode_statements)* + + #build_struct + } } }; diff --git a/src/lib/ecs/Cargo.toml b/src/lib/ecs/Cargo.toml index eb8ac550..b33e4980 100644 --- a/src/lib/ecs/Cargo.toml +++ b/src/lib/ecs/Cargo.toml @@ -9,4 +9,7 @@ thiserror = { workspace = true } dashmap = { workspace = true } parking_lot = { workspace = true } rayon = { workspace = true } -tracing = { workspace = true } \ No newline at end of file +tracing = { workspace = true } + +[dev-dependencies] +criterion = { workspace = true} \ No newline at end of file diff --git a/src/lib/ecs/benches/bench.rs b/src/lib/ecs/benches/bench.rs new file mode 100644 index 00000000..dc498388 --- /dev/null +++ b/src/lib/ecs/benches/bench.rs @@ -0,0 +1,100 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use ferrumc_ecs::Universe; + +#[allow(dead_code)] +struct Position { + x: f32, + y: f32, +} + +#[allow(dead_code)] +struct Velocity { + x: f32, + y: f32, +} + +fn create_entity(universe: &Universe) { + // entity is 0 here; + universe + .builder() + .with(Position { x: 0.0, y: 0.0 }) + .unwrap() + .build(); +} + +fn get_position_immut(universe: &Universe) { + let position = universe.get::(0).unwrap(); + assert_eq!(position.x, 0.0); + assert_eq!(position.y, 0.0); +} + +fn get_position_mut(universe: &Universe) { + let position = universe.get_mut::(0).unwrap(); + assert_eq!(position.x, 0.0); + assert_eq!(position.y, 0.0); +} + +fn _create_1000_entities_with_pos_and_vel(universe: &Universe) { + for i in 0..1000 { + let builder = universe + .builder() + .with(Position { + x: i as f32, + y: i as f32, + }) + .unwrap(); + if i % 2 == 0 { + builder + .with(Velocity { + x: i as f32, + y: i as f32, + }) + .unwrap(); + } + } +} + +fn query_10k_entities(universe: &Universe) { + let query = universe.query::<(&Position, &Velocity)>(); + for (_, (position, velocity)) in query { + assert_eq!(position.x, velocity.x); + assert_eq!(position.y, velocity.y); + } +} + +fn criterion_benchmark(c: &mut Criterion) { + let mut world = Universe::new(); + c.benchmark_group("entity") + .bench_function("create_entity", |b| { + b.iter(|| { + create_entity(black_box(&world)); + }); + // Create a new world after bench is done. + world = Universe::new(); + world + .builder() + .with(Position { x: 0.0, y: 0.0 }) + .unwrap() + .build(); + }) + .bench_function("get immut", |b| { + b.iter(|| { + get_position_immut(black_box(&world)); + }); + }) + .bench_function("get mut", |b| { + b.iter(|| { + get_position_mut(black_box(&world)); + }); + }) + .bench_function("query 10k entities", |b| { + let universe = Universe::new(); + _create_1000_entities_with_pos_and_vel(&universe); + b.iter(|| { + query_10k_entities(black_box(&world)); + }); + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/src/lib/ecs/src/components/mod.rs b/src/lib/ecs/src/components/mod.rs index 92093503..7fcf1e24 100644 --- a/src/lib/ecs/src/components/mod.rs +++ b/src/lib/ecs/src/components/mod.rs @@ -4,7 +4,9 @@ use crate::ECSResult; use dashmap::DashMap; use parking_lot::RwLock; use std::any::TypeId; +#[cfg(debug_assertions)] use std::hash::{Hash, Hasher}; +#[cfg(debug_assertions)] use tracing::trace; pub mod storage; diff --git a/src/lib/events/Cargo.toml b/src/lib/events/Cargo.toml index 2c1c7109..8461d9a3 100644 --- a/src/lib/events/Cargo.toml +++ b/src/lib/events/Cargo.toml @@ -10,3 +10,4 @@ ctor = { workspace = true} thiserror = { workspace = true } futures = { workspace = true } dashmap = { workspace = true } +tracing = { workspace = true } \ No newline at end of file diff --git a/src/lib/events/src/infrastructure.rs b/src/lib/events/src/infrastructure.rs index 25f612ce..16774b21 100644 --- a/src/lib/events/src/infrastructure.rs +++ b/src/lib/events/src/infrastructure.rs @@ -73,6 +73,9 @@ pub trait Event: Sized + Send + Sync + 'static { /// /// Returns `Ok(())` if the execution succeeded. `Err(EventsError)` ifa listener failed. async fn trigger(event: Self::Data, state: Self::State) -> Result<(), Self::Error> { + #[cfg(debug_assertions)] + let start = std::time::Instant::now(); + let listeners = EVENTS_LISTENERS .get(Self::name()) .expect("Failed to find event listeners. Impossible;"); @@ -96,65 +99,12 @@ pub trait Event: Sized + Send + Sync + 'static { }) .await?; + #[cfg(debug_assertions)] + tracing::trace!("Event {} took {:?}", Self::name(), start.elapsed()); + Ok(()) } - /*/// Trigger the execution of an event with concurrency support - /// - /// If the event structure supports cloning. This method can be used to execute - /// listeners of the same priority concurrently (using tokio::task). This imply a - /// cloning cost at each listener execution. See `Event::trigger` for a more - /// efficient but more linear approach. - /// - /// # Mutability policy - /// - /// The listeners having the same priority being runned concurrently, there are no - /// guarantees in the order of mutation of the event data. - /// - /// It is recommended to ensure listeners of the same priority exclusively update fields - /// in the event data that are untouched by other listeners of the same group. - async fn trigger_concurrently(event: Self::Data) -> Result<(), Self::Error> - where - Self::Data: Clone, - { - let read_guard = &EVENTS_LISTENERS; - let listeners = read_guard.get(Self::name()).unwrap(); - - // Convert listeners iterator into Stream - let mut stream = stream::iter(listeners.iter()); - - let mut priority_join_set = Vec::new(); - let mut current_priority = 0; - - while let Some(Some(listener)) = stream - .next() - .await - .map(|l| l.downcast_ref::>()) - { - if listener.priority == current_priority { - priority_join_set.push(tokio::spawn((listener.listener)(event.clone()))); - } else { - // Await over all listeners launched - let joined = future::join_all(priority_join_set.iter_mut()).await; - - // If one listener fail we return the first error - if let Some(err) = joined - .into_iter() - .filter_map(|res| res.expect("No task should ever panic. Impossible;").err()) - .next() - { - return Err(err); - } - - // Update priority to the new listener(s) - current_priority = listener.priority; - priority_join_set.push(tokio::spawn((listener.listener)(event.clone()))); - } - } - - Ok(()) - } - */ /// Register a new event listener for this event fn register(listener: AsyncEventListener, priority: u8) { // Create the event listener structure diff --git a/src/lib/net/crates/codec/src/decode/primitives.rs b/src/lib/net/crates/codec/src/decode/primitives.rs index a7b6c672..4075e058 100644 --- a/src/lib/net/crates/codec/src/decode/primitives.rs +++ b/src/lib/net/crates/codec/src/decode/primitives.rs @@ -35,6 +35,7 @@ impl_for_primitives!( u32 | i32, u64 | i64, u128 | i128, + usize | isize, f32, f64 ); diff --git a/src/lib/net/crates/codec/src/encode/primitives.rs b/src/lib/net/crates/codec/src/encode/primitives.rs index b90ffc79..86931626 100644 --- a/src/lib/net/crates/codec/src/encode/primitives.rs +++ b/src/lib/net/crates/codec/src/encode/primitives.rs @@ -40,6 +40,7 @@ impl_for_primitives!( u32 | i32, u64 | i64, u128 | i128, + usize | isize, f32, f64 ); diff --git a/src/lib/net/crates/codec/src/net_types/angle.rs b/src/lib/net/crates/codec/src/net_types/angle.rs new file mode 100644 index 00000000..2cae5512 --- /dev/null +++ b/src/lib/net/crates/codec/src/net_types/angle.rs @@ -0,0 +1,94 @@ +use crate::encode::{NetEncode, NetEncodeOpts, NetEncodeResult}; +use std::f64::consts::PI; +use std::io::Write; +use tokio::io::{AsyncWrite, AsyncWriteExt}; + +/// Represents a rotation angle in steps of 1/256 of a full turn +/// Stored as a single byte (0-255) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct NetAngle(pub u8); + +impl NetAngle { + /// Creates a new Angle from a byte value + pub fn new(value: u8) -> Self { + NetAngle(value) + } + + /// Creates an Angle from degrees + pub fn from_degrees(deg: f64) -> Self { + // This ensures negative angles won't break the `as u8` cast + let wrapped = deg.rem_euclid(360.0); + let steps = (wrapped * 256.0 / 360.0).round() as u8; + NetAngle(steps) + } + + /// Creates an Angle from radians + pub fn from_radians(radians: f64) -> Self { + let normalized = radians % (2.0 * PI); + let steps = (normalized * 256.0 / (2.0 * PI)).round() as u8; + NetAngle(steps) + } + + /// Converts the angle to degrees + pub fn to_degrees(&self) -> f64 { + (self.0 as f64) * 360.0 / 256.0 + } + + /// Converts the angle to radians + pub fn to_radians(&self) -> f64 { + (self.0 as f64) * 2.0 * PI / 256.0 + } + + /// Returns the raw byte value + pub fn as_byte(&self) -> u8 { + self.0 + } +} + +impl From for NetAngle { + fn from(value: u8) -> Self { + NetAngle(value) + } +} + +impl From for u8 { + fn from(angle: NetAngle) -> Self { + angle.0 + } +} + +impl NetEncode for NetAngle { + fn encode(&self, writer: &mut W, _: &NetEncodeOpts) -> NetEncodeResult<()> { + writer.write_all(&[self.0])?; + Ok(()) + } + + async fn encode_async( + &self, + writer: &mut W, + _: &NetEncodeOpts, + ) -> NetEncodeResult<()> { + writer.write_all(&[self.0]).await?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_angle_conversions() { + let angle = NetAngle::from_degrees(90.0); + assert!((angle.to_degrees() - 90.0).abs() < f64::EPSILON); + + let angle = NetAngle::from_radians(PI / 2.0); + assert!((angle.to_radians() - PI / 2.0).abs() < f64::EPSILON); + } + + #[test] + fn test_angle_wraparound() { + let angle1 = NetAngle::from_degrees(370.0); + let angle2 = NetAngle::from_degrees(10.0); + assert_eq!(angle1, angle2); + } +} diff --git a/src/lib/net/crates/codec/src/net_types/length_prefixed_vec.rs b/src/lib/net/crates/codec/src/net_types/length_prefixed_vec.rs index c7f0eec1..f229b0b8 100644 --- a/src/lib/net/crates/codec/src/net_types/length_prefixed_vec.rs +++ b/src/lib/net/crates/codec/src/net_types/length_prefixed_vec.rs @@ -10,6 +10,15 @@ pub struct LengthPrefixedVec { pub data: Vec, } +impl Default for LengthPrefixedVec { + fn default() -> Self { + Self { + length: VarInt::new(0), + data: Vec::new(), + } + } +} + impl LengthPrefixedVec { pub fn new(data: Vec) -> Self { Self { diff --git a/src/lib/net/crates/codec/src/net_types/mod.rs b/src/lib/net/crates/codec/src/net_types/mod.rs index ac384077..90f90a73 100644 --- a/src/lib/net/crates/codec/src/net_types/mod.rs +++ b/src/lib/net/crates/codec/src/net_types/mod.rs @@ -1,3 +1,4 @@ +pub mod angle; pub mod bitset; pub mod length_prefixed_vec; pub mod network_position; diff --git a/src/lib/net/crates/codec/src/net_types/network_position.rs b/src/lib/net/crates/codec/src/net_types/network_position.rs index dd138782..4a745b0a 100644 --- a/src/lib/net/crates/codec/src/net_types/network_position.rs +++ b/src/lib/net/crates/codec/src/net_types/network_position.rs @@ -40,6 +40,7 @@ impl NetEncode for NetworkPosition { _: &NetEncodeOpts, ) -> NetEncodeResult<()> { use tokio::io::AsyncWriteExt; + writer .write_all(self.as_u64().to_be_bytes().as_ref()) .await?; diff --git a/src/lib/net/src/connection.rs b/src/lib/net/src/connection.rs index 63f190b4..56ff0daf 100644 --- a/src/lib/net/src/connection.rs +++ b/src/lib/net/src/connection.rs @@ -1,6 +1,8 @@ use crate::packets::incoming::packet_skeleton::PacketSkeleton; use crate::utils::state::terminate_connection; use crate::{handle_packet, NetResult}; +use ferrumc_events::infrastructure::Event; +use ferrumc_macros::Event; use ferrumc_net_codec::encode::NetEncode; use ferrumc_net_codec::encode::NetEncodeOpts; use ferrumc_state::ServerState; @@ -174,6 +176,11 @@ pub async fn handle_connection(state: Arc, tcp_stream: TcpStream) - debug!("Connection closed for entity: {:?}", entity); + // Broadcast the leave server event + let _ = + PlayerDisconnectEvent::trigger(PlayerDisconnectEvent { entity_id: entity }, state.clone()) + .await; + // Remove all components from the entity // Wait until anything that might be using the entity is done @@ -186,6 +193,11 @@ pub async fn handle_connection(state: Arc, tcp_stream: TcpStream) - Ok(()) } +#[derive(Event)] +pub struct PlayerDisconnectEvent { + pub entity_id: usize, +} + /// Since parking_lot is single-threaded, we use spawn_blocking to remove all components from the entity asynchronously (on another thread). async fn remove_all_components_blocking(state: Arc, entity: usize) -> NetResult<()> { let res = diff --git a/src/lib/net/src/packets/incoming/mod.rs b/src/lib/net/src/packets/incoming/mod.rs index 609f328f..b37fb6b3 100644 --- a/src/lib/net/src/packets/incoming/mod.rs +++ b/src/lib/net/src/packets/incoming/mod.rs @@ -17,3 +17,5 @@ pub mod set_player_rotation; pub mod chat_message; pub mod command; +pub mod player_command; +pub mod swing_arm; diff --git a/src/lib/net/src/packets/incoming/player_command.rs b/src/lib/net/src/packets/incoming/player_command.rs new file mode 100644 index 00000000..e0cb73ee --- /dev/null +++ b/src/lib/net/src/packets/incoming/player_command.rs @@ -0,0 +1,57 @@ +use crate::packets::IncomingPacket; +use crate::NetResult; +use ferrumc_ecs::entities::Entity; +use ferrumc_events::infrastructure::Event; +use ferrumc_macros::{packet, Event, NetDecode}; +use ferrumc_net_codec::net_types::var_int::VarInt; +use ferrumc_state::ServerState; +use std::sync::Arc; + +// Mojang surely has SOME naming schemes.. commands?? +#[derive(NetDecode)] +#[packet(packet_id = 0x25, state = "play")] +pub struct PlayerCommandPacket { + entity_id: VarInt, + // Originally: Action Id = VarInt Enum + action: PlayerCommandAction, + jump_boost: VarInt, +} + +#[derive(Debug, NetDecode)] +#[net(type_cast = "VarInt", type_cast_handler = "value.val as u8")] +#[repr(u8)] +pub enum PlayerCommandAction { + StartSneaking = 0, + StopSneaking = 1, + LeaveBed = 2, + StartSprinting = 3, + StopSprinting = 4, + StartJumpWithHorse = 5, + StopJumpWithHorse = 6, + OpenVehicleInventory = 7, + StartFlyingWithElytra = 8, +} + +impl IncomingPacket for PlayerCommandPacket { + async fn handle(self, _: Entity, state: Arc) -> NetResult<()> { + PlayerDoActionEvent::trigger(PlayerDoActionEvent::from(self), state).await?; + Ok(()) + } +} + +#[derive(Debug, Event)] +pub struct PlayerDoActionEvent { + pub entity_id: Entity, + pub action: PlayerCommandAction, + pub jump_boost: i32, +} + +impl From for PlayerDoActionEvent { + fn from(packet: PlayerCommandPacket) -> Self { + Self { + entity_id: packet.entity_id.val as Entity, + action: packet.action, + jump_boost: packet.jump_boost.val, + } + } +} diff --git a/src/lib/net/src/packets/incoming/swing_arm.rs b/src/lib/net/src/packets/incoming/swing_arm.rs new file mode 100644 index 00000000..315e6abc --- /dev/null +++ b/src/lib/net/src/packets/incoming/swing_arm.rs @@ -0,0 +1,30 @@ +use crate::packets::outgoing::entity_animation::EntityAnimationEvent; +use crate::packets::IncomingPacket; +use crate::NetResult; +use ferrumc_ecs::entities::Entity; +use ferrumc_events::infrastructure::Event; +use ferrumc_macros::{packet, NetDecode}; +use ferrumc_net_codec::net_types::var_int::VarInt; +use ferrumc_state::ServerState; +use std::sync::Arc; + +#[derive(NetDecode)] +#[packet(packet_id = 0x36, state = "play")] +pub struct SwingArmPacket { + hand: VarInt, +} + +impl IncomingPacket for SwingArmPacket { + async fn handle(self, conn_id: Entity, state: Arc) -> NetResult<()> { + let animation = { + if self.hand == 0 { + 0 + } else { + 3 + } + }; + let event = EntityAnimationEvent::new(conn_id, animation); + EntityAnimationEvent::trigger(event, state).await?; + Ok(()) + } +} diff --git a/src/lib/net/src/packets/outgoing/chunk_and_light_data.rs b/src/lib/net/src/packets/outgoing/chunk_and_light_data.rs index 39ca09c7..345cba66 100644 --- a/src/lib/net/src/packets/outgoing/chunk_and_light_data.rs +++ b/src/lib/net/src/packets/outgoing/chunk_and_light_data.rs @@ -7,7 +7,7 @@ use ferrumc_net_codec::net_types::var_int::VarInt; use ferrumc_world::chunk_format::{Chunk, Heightmaps}; use std::io::{Cursor, Write}; use std::ops::Not; -use tracing::warn; +use tracing::{trace, warn}; const SECTIONS: usize = 24; // Number of sections, adjust for your Y range (-64 to 319) @@ -117,9 +117,11 @@ impl ChunkAndLightData { // If there is no palette entry, write a 0 (air) and log a warning None => { VarInt::new(0).write(&mut data)?; - warn!( + trace!( "No palette entry found for section at {}, {}, {}", - chunk.x, section.y, chunk.z + chunk.x, + section.y, + chunk.z ); } } diff --git a/src/lib/net/src/packets/outgoing/entity_animation.rs b/src/lib/net/src/packets/outgoing/entity_animation.rs new file mode 100644 index 00000000..5728ff22 --- /dev/null +++ b/src/lib/net/src/packets/outgoing/entity_animation.rs @@ -0,0 +1,37 @@ +use ferrumc_ecs::entities::Entity; +use ferrumc_macros::{packet, Event, NetEncode}; +use ferrumc_net_codec::net_types::var_int::VarInt; +use std::io::Write; + +#[derive(NetEncode)] +#[packet(packet_id = 0x03)] +pub struct EntityAnimationPacket { + pub eid: VarInt, + pub animation: u8, +} + +#[derive(Event)] +pub struct EntityAnimationEvent { + pub entity: Entity, + pub animation: u8, + pub packet: EntityAnimationPacket, +} + +impl EntityAnimationPacket { + pub fn new(eid: Entity, animation: u8) -> Self { + Self { + eid: VarInt::new(eid as i32), + animation, + } + } +} + +impl EntityAnimationEvent { + pub fn new(eid: Entity, animation: u8) -> Self { + Self { + entity: eid, + animation, + packet: EntityAnimationPacket::new(eid, animation), + } + } +} diff --git a/src/lib/net/src/packets/outgoing/entity_metadata.rs b/src/lib/net/src/packets/outgoing/entity_metadata.rs new file mode 100644 index 00000000..a2735ba5 --- /dev/null +++ b/src/lib/net/src/packets/outgoing/entity_metadata.rs @@ -0,0 +1,296 @@ +// https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Entity_metadata#Entity_Metadata_Format +use crate::packets::outgoing::entity_metadata::entity_state::{EntityState, EntityStateMask}; +use crate::packets::outgoing::entity_metadata::index_type::EntityMetadataIndexType; +use crate::packets::outgoing::entity_metadata::value::EntityMetadataValue; +use ferrumc_ecs::entities::Entity; +use ferrumc_macros::{packet, NetEncode}; +use ferrumc_net_codec::encode::{NetEncode, NetEncodeOpts, NetEncodeResult}; +use ferrumc_net_codec::net_types::var_int::VarInt; +use std::io::Write; +use tokio::io::AsyncWrite; + +/// Packet for sending entity metadata updates to clients +#[derive(NetEncode)] +#[packet(packet_id = 0x58)] +pub struct EntityMetadataPacket { + entity_id: VarInt, + metadata: Vec, + terminator: u8, // Always 0xFF to indicate end of metadata (Simple is Best) +} + +impl EntityMetadataPacket { + /// Creates a new metadata packet for the specified entity + /// + /// # Arguments + /// * `entity_id` - The entity ID to update metadata for + /// * `metadata` - Iterator of metadata entries to send + /// + /// # Example + /// ```ignored + /// let entity_id = ...; + /// let metadata = vec![ + /// EntityMetadata::entity_sneaking_pressed(), + /// EntityMetadata::entity_sneaking_visual(), + /// EntityMetadata::entity_standing() + /// ]; + /// let packet = EntityMetadataPacket::new(entity_id, metadata); + /// ``` + pub fn new(entity_id: Entity, metadata: T) -> Self + where + T: IntoIterator, + { + Self { + entity_id: VarInt::new(entity_id as i32), + metadata: metadata.into_iter().collect(), + terminator: 0xFF, + } + } +} + +/// Single metadata entry containing an index, type and value +#[derive(NetEncode)] +pub struct EntityMetadata { + index: u8, + index_type: EntityMetadataIndexType, + value: EntityMetadataValue, +} + +pub mod constructors { + use super::*; + use crate::packets::outgoing::entity_metadata::extra_data_types::EntityPose; + + impl EntityMetadata { + fn new(index_type: EntityMetadataIndexType, value: EntityMetadataValue) -> Self { + EntityMetadata { + index: value.index(), + index_type, + value, + } + } + /// To hide the name tag and stuff + pub fn entity_sneaking_pressed() -> Self { + Self::new( + EntityMetadataIndexType::Byte, + EntityMetadataValue::Entity0(EntityStateMask::from_state( + EntityState::SneakingVisual, + )), + ) + } + /// Actual sneaking visual, so you can see the player sneaking + pub fn entity_sneaking_visual() -> Self { + Self::new( + EntityMetadataIndexType::Pose, + EntityMetadataValue::Entity6(EntityPose::Sneaking), + ) + } + + /// Entity in standing pose + pub fn entity_standing() -> Self { + Self::new( + EntityMetadataIndexType::Pose, + EntityMetadataValue::Entity6(EntityPose::Standing), + ) + } + } +} + +mod index_type { + use super::*; + + /// Available metadata field types + /// See: https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Entity_metadata#Entity_Metadata_Format + pub enum EntityMetadataIndexType { + Byte, // (0) Used for bit masks and small numbers + Pose, // (21) Used for entity pose + } + + impl EntityMetadataIndexType { + pub fn index(&self) -> VarInt { + use EntityMetadataIndexType::*; + let val = match self { + Byte => 0, + Pose => 21, + }; + + VarInt::new(val) + } + } + + impl NetEncode for EntityMetadataIndexType { + fn encode(&self, writer: &mut W, opts: &NetEncodeOpts) -> NetEncodeResult<()> { + self.index().encode(writer, opts) + } + + async fn encode_async( + &self, + writer: &mut W, + opts: &NetEncodeOpts, + ) -> NetEncodeResult<()> { + self.index().encode_async(writer, opts).await + } + } +} + +mod value { + use super::*; + use crate::packets::outgoing::entity_metadata::extra_data_types::EntityPose; + /// Possible metadata values that can be sent + /// + /// Couldn't be arsed coming up with the names. + /// Read here: + /// https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Entity_metadata#Entity + /// + /// Formatted like: + /// {Class Name}{Index} + #[derive(NetEncode)] + pub enum EntityMetadataValue { + Entity0(EntityStateMask), + Entity6(EntityPose), + } + + impl EntityMetadataValue { + pub fn index(&self) -> u8 { + use EntityMetadataValue::*; + match self { + Entity0(_) => 0, + Entity6(_) => 6, + } + } + } +} + +mod entity_state { + use ferrumc_macros::NetEncode; + use std::io::Write; + + /// Bit mask for various entity states + #[derive(Debug, NetEncode)] + pub struct EntityStateMask { + mask: u8, + } + + impl Default for EntityStateMask { + fn default() -> Self { + Self::new() + } + } + + impl EntityStateMask { + pub fn new() -> Self { + Self { mask: 0 } + } + + pub fn from_state(state: EntityState) -> Self { + let mut mask = Self::new(); + mask.set(state); + mask + } + + pub fn set(&mut self, state: EntityState) { + self.mask |= state.mask(); + } + } + + /// Individual states that can be applied to an entity + /// Multiple states can be combined using a bit mask + #[allow(dead_code)] + pub enum EntityState { + OnFire, // 0x01 + SneakingVisual, // 0x02 + Sprinting, // 0x08 + Swimming, // 0x10 + Invisible, // 0x20 + Glowing, // 0x40 + FlyingWithElytra, // 0x80 + } + + impl EntityState { + pub fn mask(&self) -> u8 { + use EntityState::*; + match self { + OnFire => 0x01, + SneakingVisual => 0x02, + Sprinting => 0x08, + Swimming => 0x10, + Invisible => 0x20, + Glowing => 0x40, + FlyingWithElytra => 0x80, + } + } + } +} + +mod extra_data_types { + use ferrumc_net_codec::encode::{NetEncode, NetEncodeOpts, NetEncodeResult}; + use ferrumc_net_codec::net_types::var_int::VarInt; + use std::io::Write; + use tokio::io::AsyncWrite; + // STANDING = 0, FALL_FLYING = 1, SLEEPING = 2, SWIMMING = 3, SPIN_ATTACK = 4, SNEAKING = 5, LONG_JUMPING = 6, DYING = 7, CROAKING = 8, + // USING_TONGUE = 9, SITTING = 10, ROARING = 11, SNIFFING = 12, EMERGING = 13, DIGGING = 14, (1.21.3: SLIDING = 15, SHOOTING = 16, + // INHALING = 17 + + /// Possible poses/animations an entity can have + #[derive(Debug)] + #[allow(dead_code)] + pub enum EntityPose { + Standing, + FallFlying, + Sleeping, + Swimming, + SpinAttack, + Sneaking, + LongJumping, + Dying, + Croaking, + UsingTongue, + Sitting, + Roaring, + Sniffing, + Emerging, + Digging, + Sliding, + Shooting, + Inhaling, + } + + impl EntityPose { + pub fn index(&self) -> VarInt { + use EntityPose::*; + let val = match self { + Standing => 0, + FallFlying => 1, + Sleeping => 2, + Swimming => 3, + SpinAttack => 4, + Sneaking => 5, + LongJumping => 6, + Dying => 7, + Croaking => 8, + UsingTongue => 9, + Sitting => 10, + Roaring => 11, + Sniffing => 12, + Emerging => 13, + Digging => 14, + Sliding => 15, + Shooting => 16, + Inhaling => 17, + }; + VarInt::new(val) + } + } + + impl NetEncode for EntityPose { + fn encode(&self, writer: &mut W, opts: &NetEncodeOpts) -> NetEncodeResult<()> { + self.index().encode(writer, opts) + } + + async fn encode_async( + &self, + writer: &mut W, + opts: &NetEncodeOpts, + ) -> NetEncodeResult<()> { + self.index().encode_async(writer, opts).await + } + } +} diff --git a/src/lib/net/src/packets/outgoing/mod.rs b/src/lib/net/src/packets/outgoing/mod.rs index 04ac2377..30102ed1 100644 --- a/src/lib/net/src/packets/outgoing/mod.rs +++ b/src/lib/net/src/packets/outgoing/mod.rs @@ -18,3 +18,18 @@ pub mod status_response; pub mod synchronize_player_position; pub mod system_message; pub mod update_time; + +pub mod remove_entities; +pub mod spawn_entity; + +pub mod entity_animation; +pub mod entity_metadata; +pub mod player_info_update; + +// --------- Movement ---------- +pub mod set_head_rotation; +pub mod teleport_entity; +pub mod update_entity_position; +pub mod update_entity_position_and_rotation; +pub mod update_entity_rotation; +// ----------------------------- diff --git a/src/lib/net/src/packets/outgoing/player_info_update.rs b/src/lib/net/src/packets/outgoing/player_info_update.rs new file mode 100644 index 00000000..9eae4f0a --- /dev/null +++ b/src/lib/net/src/packets/outgoing/player_info_update.rs @@ -0,0 +1,124 @@ +use crate::utils::broadcast::get_all_play_players; +use ferrumc_core::identity::player_identity::PlayerIdentity; +use ferrumc_ecs::entities::Entity; +use ferrumc_macros::{packet, NetEncode}; +use ferrumc_net_codec::net_types::length_prefixed_vec::LengthPrefixedVec; +use ferrumc_net_codec::net_types::var_int::VarInt; +use ferrumc_state::GlobalState; +use std::io::Write; +use tracing::debug; + +#[derive(NetEncode)] +#[packet(packet_id = 0x3E)] +pub struct PlayerInfoUpdatePacket { + pub actions: u8, + pub numbers_of_players: VarInt, + pub players: Vec, +} + +impl PlayerInfoUpdatePacket { + pub fn with_players(players: T) -> Self + where + T: IntoIterator, + { + let players: Vec = players.into_iter().collect(); + Self { + actions: players + .iter() + .map(|player| player.get_actions_mask()) + .fold(0, |acc, x| acc | x), + numbers_of_players: VarInt::new(players.len() as i32), + players, + } + } + + /// The packet to be sent to all already connected players when a new player joins the server + pub fn new_player_join_packet(new_player_id: Entity, state: &GlobalState) -> Self { + let identity = state + .universe + .get_component_manager() + .get::(new_player_id) + .unwrap(); + let uuid = identity.uuid; + let name = identity.username.clone(); + + let player = PlayerWithActions::add_player(uuid, name); + + Self::with_players(vec![player]) + } + + /// The packet to be sent to a new player when they join the server, + /// To let them know about all the players that are already connected + pub fn existing_player_info_packet(new_player_id: Entity, state: &GlobalState) -> Self { + let players = { + let mut players = get_all_play_players(state); + players.retain(|&player| player != new_player_id); + + players + }; + + let players = players + .into_iter() + .filter_map(|player| { + let identity = state + .universe + .get_component_manager() + .get::(player) + .ok()?; + let uuid = identity.uuid; + let name = identity.username.clone(); + + Some((uuid, name)) + }) + .map(|(uuid, name)| PlayerWithActions::add_player(uuid, name)) + .collect::>(); + + debug!("Sending PlayerInfoUpdatePacket with {:?} players", players); + + Self::with_players(players) + } +} + +#[derive(NetEncode, Debug)] +pub struct PlayerWithActions { + pub uuid: u128, + pub actions: Vec, +} + +impl PlayerWithActions { + pub fn get_actions_mask(&self) -> u8 { + let mut mask = 0; + for action in &self.actions { + mask |= match action { + PlayerAction::AddPlayer { .. } => 0x01, + } + } + mask + } + + pub fn add_player(uuid: impl Into, name: impl Into) -> Self { + Self { + uuid: uuid.into(), + actions: vec![PlayerAction::AddPlayer { + name: name.into(), + properties: LengthPrefixedVec::default(), + }], + } + } +} + +#[derive(NetEncode, Debug)] +pub enum PlayerAction { + AddPlayer { + name: String, + properties: LengthPrefixedVec, + }, +} + +#[derive(NetEncode, Debug)] +pub struct PlayerProperty { + pub name: String, + pub value: String, + pub is_signed: bool, + pub signature: Option, +} diff --git a/src/lib/net/src/packets/outgoing/remove_entities.rs b/src/lib/net/src/packets/outgoing/remove_entities.rs new file mode 100644 index 00000000..d9b8f7e4 --- /dev/null +++ b/src/lib/net/src/packets/outgoing/remove_entities.rs @@ -0,0 +1,26 @@ +use ferrumc_ecs::entities::Entity; +use ferrumc_macros::{packet, NetEncode}; +use ferrumc_net_codec::net_types::length_prefixed_vec::LengthPrefixedVec; +use ferrumc_net_codec::net_types::var_int::VarInt; +use std::io::Write; + +#[derive(NetEncode)] +#[packet(packet_id = 0x42)] +pub struct RemoveEntitiesPacket { + pub entity_ids: LengthPrefixedVec, +} + +impl RemoveEntitiesPacket { + pub fn from_entities(entity_ids: T) -> Self + where + T: IntoIterator, + { + let entity_ids: Vec = entity_ids + .into_iter() + .map(|entity| VarInt::new(entity as i32)) + .collect(); + Self { + entity_ids: LengthPrefixedVec::new(entity_ids), + } + } +} diff --git a/src/lib/net/src/packets/outgoing/set_head_rotation.rs b/src/lib/net/src/packets/outgoing/set_head_rotation.rs new file mode 100644 index 00000000..30b95570 --- /dev/null +++ b/src/lib/net/src/packets/outgoing/set_head_rotation.rs @@ -0,0 +1,20 @@ +use ferrumc_macros::{packet, NetEncode}; +use ferrumc_net_codec::net_types::angle::NetAngle; +use ferrumc_net_codec::net_types::var_int::VarInt; +use std::io::Write; + +#[derive(Debug, NetEncode)] +#[packet(packet_id = 0x48)] +pub struct SetHeadRotationPacket { + pub entity_id: VarInt, + pub head_yaw: NetAngle, +} + +impl SetHeadRotationPacket { + pub fn new(entity_id: i32, head_yaw: NetAngle) -> Self { + Self { + entity_id: VarInt::new(entity_id), + head_yaw, + } + } +} diff --git a/src/lib/net/src/packets/outgoing/spawn_entity.rs b/src/lib/net/src/packets/outgoing/spawn_entity.rs new file mode 100644 index 00000000..f8da3c5c --- /dev/null +++ b/src/lib/net/src/packets/outgoing/spawn_entity.rs @@ -0,0 +1,55 @@ +use crate::utils::ecs_helpers::EntityExt; +use crate::NetResult; +use ferrumc_core::identity::player_identity::PlayerIdentity; +use ferrumc_core::transform::position::Position; +use ferrumc_core::transform::rotation::Rotation; +use ferrumc_ecs::entities::Entity; +use ferrumc_macros::{packet, NetEncode}; +use ferrumc_net_codec::net_types::angle::NetAngle; +use ferrumc_net_codec::net_types::var_int::VarInt; +use ferrumc_state::GlobalState; +use std::io::Write; + +#[derive(NetEncode)] +#[packet(packet_id = 0x01)] +pub struct SpawnEntityPacket { + entity_id: VarInt, + entity_uuid: u128, + r#type: VarInt, + x: f64, + y: f64, + z: f64, + pitch: NetAngle, + yaw: NetAngle, + head_yaw: NetAngle, + data: VarInt, + velocity_x: i16, + velocity_y: i16, + velocity_z: i16, +} + +const PLAYER_ID: u8 = 128; + +impl SpawnEntityPacket { + pub fn player(entity_id: Entity, state: &GlobalState) -> NetResult { + let player_identity = entity_id.get::(state)?; + let position = entity_id.get::(state)?; + let rotation = entity_id.get::(state)?; + + Ok(Self { + entity_id: VarInt::new(entity_id as i32), + entity_uuid: player_identity.uuid, + r#type: VarInt::new(PLAYER_ID as i32), + x: position.x, + y: position.y, + z: position.z, + pitch: NetAngle::from_degrees(rotation.pitch as f64), + yaw: NetAngle::from_degrees(rotation.yaw as f64), + head_yaw: NetAngle::from_degrees(rotation.yaw as f64), + data: VarInt::new(0), + velocity_x: 0, + velocity_y: 0, + velocity_z: 0, + }) + } +} diff --git a/src/lib/net/src/packets/outgoing/teleport_entity.rs b/src/lib/net/src/packets/outgoing/teleport_entity.rs new file mode 100644 index 00000000..ba716ccd --- /dev/null +++ b/src/lib/net/src/packets/outgoing/teleport_entity.rs @@ -0,0 +1,33 @@ +use ferrumc_core::transform::position::Position; +use ferrumc_core::transform::rotation::Rotation; +use ferrumc_ecs::entities::Entity; +use ferrumc_macros::{packet, NetEncode}; +use ferrumc_net_codec::net_types::angle::NetAngle; +use ferrumc_net_codec::net_types::var_int::VarInt; +use std::io::Write; + +#[derive(NetEncode)] +#[packet(packet_id = 0x70)] +pub struct TeleportEntityPacket { + pub entity_id: VarInt, + pub x: f64, + pub y: f64, + pub z: f64, + pub yaw: NetAngle, + pub pitch: NetAngle, + pub on_ground: bool, +} + +impl TeleportEntityPacket { + pub fn new(entity_id: Entity, position: &Position, angle: &Rotation, on_ground: bool) -> Self { + Self { + entity_id: VarInt::new(entity_id as i32), + x: position.x, + y: position.y, + z: position.z, + yaw: NetAngle::from_degrees(angle.yaw as f64), + pitch: NetAngle::from_degrees(angle.pitch as f64), + on_ground, + } + } +} diff --git a/src/lib/net/src/packets/outgoing/update_entity_position.rs b/src/lib/net/src/packets/outgoing/update_entity_position.rs new file mode 100644 index 00000000..e7ab4320 --- /dev/null +++ b/src/lib/net/src/packets/outgoing/update_entity_position.rs @@ -0,0 +1,26 @@ +use ferrumc_ecs::entities::Entity; +use ferrumc_macros::{packet, NetEncode}; +use ferrumc_net_codec::net_types::var_int::VarInt; +use std::io::Write; + +#[derive(NetEncode)] +#[packet(packet_id = 0x2E)] +pub struct UpdateEntityPositionPacket { + pub entity_id: VarInt, + pub delta_x: i16, + pub delta_y: i16, + pub delta_z: i16, + pub on_ground: bool, +} + +impl UpdateEntityPositionPacket { + pub fn new(entity_id: Entity, delta_positions: (i16, i16, i16), on_ground: bool) -> Self { + Self { + entity_id: VarInt::new(entity_id as i32), + delta_x: delta_positions.0, + delta_y: delta_positions.1, + delta_z: delta_positions.2, + on_ground, + } + } +} diff --git a/src/lib/net/src/packets/outgoing/update_entity_position_and_rotation.rs b/src/lib/net/src/packets/outgoing/update_entity_position_and_rotation.rs new file mode 100644 index 00000000..f2a8c96d --- /dev/null +++ b/src/lib/net/src/packets/outgoing/update_entity_position_and_rotation.rs @@ -0,0 +1,37 @@ +use ferrumc_core::transform::rotation::Rotation; +use ferrumc_ecs::entities::Entity; +use ferrumc_macros::{packet, NetEncode}; +use ferrumc_net_codec::net_types::angle::NetAngle; +use ferrumc_net_codec::net_types::var_int::VarInt; +use std::io::Write; + +#[derive(NetEncode)] +#[packet(packet_id = 0x2F)] +pub struct UpdateEntityPositionAndRotationPacket { + pub entity_id: VarInt, + pub delta_x: i16, + pub delta_y: i16, + pub delta_z: i16, + pub yaw: NetAngle, + pub pitch: NetAngle, + pub on_ground: bool, +} + +impl UpdateEntityPositionAndRotationPacket { + pub fn new( + entity_id: Entity, + delta_positions: (i16, i16, i16), + new_rot: &Rotation, + on_ground: bool, + ) -> Self { + Self { + entity_id: VarInt::new(entity_id as i32), + delta_x: delta_positions.0, + delta_y: delta_positions.1, + delta_z: delta_positions.2, + yaw: NetAngle::from_degrees(new_rot.yaw as f64), + pitch: NetAngle::from_degrees(new_rot.pitch as f64), + on_ground, + } + } +} diff --git a/src/lib/net/src/packets/outgoing/update_entity_rotation.rs b/src/lib/net/src/packets/outgoing/update_entity_rotation.rs new file mode 100644 index 00000000..96b91916 --- /dev/null +++ b/src/lib/net/src/packets/outgoing/update_entity_rotation.rs @@ -0,0 +1,25 @@ +use ferrumc_core::transform::rotation::Rotation; +use ferrumc_ecs::entities::Entity; +use ferrumc_macros::{packet, NetEncode}; +use ferrumc_net_codec::net_types::angle::NetAngle; +use ferrumc_net_codec::net_types::var_int::VarInt; +use std::io::Write; + +#[derive(NetEncode)] +#[packet(packet_id = 0x30)] +pub struct UpdateEntityRotationPacket { + pub entity_id: VarInt, + pub yaw: NetAngle, + pub pitch: NetAngle, + pub on_ground: bool, +} +impl UpdateEntityRotationPacket { + pub fn new(entity_id: Entity, new_rot: &Rotation, on_ground: bool) -> Self { + Self { + entity_id: VarInt::new(entity_id as i32), + yaw: NetAngle::from_degrees(new_rot.yaw as f64), + pitch: NetAngle::from_degrees(new_rot.pitch as f64), + on_ground, + } + } +} diff --git a/src/lib/net/src/utils/broadcast.rs b/src/lib/net/src/utils/broadcast.rs index 5aff8f22..45e84272 100644 --- a/src/lib/net/src/utils/broadcast.rs +++ b/src/lib/net/src/utils/broadcast.rs @@ -1,10 +1,12 @@ use crate::connection::StreamWriter; use crate::NetResult; use async_trait::async_trait; +use ferrumc_core::chunks::chunk_receiver::ChunkReceiver; use ferrumc_ecs::entities::Entity; use ferrumc_net_codec::encode::{NetEncode, NetEncodeOpts}; use ferrumc_state::GlobalState; use futures::StreamExt; +use std::collections::HashSet; use std::future::Future; use std::pin::Pin; use tracing::debug; @@ -16,14 +18,26 @@ type SyncCallbackFn = Box; #[derive(Default)] pub struct BroadcastOptions { - pub only_entities: Option>, + pub only_entities: Option>, + pub except_entities: Option>, pub async_callback: Option, pub sync_callback: Option, } impl BroadcastOptions { - pub fn only(mut self, entities: Vec) -> Self { - self.only_entities = Some(entities); + pub fn only(mut self, entities: I) -> Self + where + I: IntoIterator, + { + self.only_entities = Some(entities.into_iter().collect()); + self + } + + pub fn except(mut self, entities: I) -> Self + where + I: IntoIterator, + { + self.except_entities = Some(entities.into_iter().collect()); self } @@ -50,19 +64,38 @@ impl BroadcastOptions { } } +/// Get all players in the 'play' state, so the players playing the playable game. +pub fn get_all_play_players(state: &GlobalState) -> HashSet { + // If it needs a chunk, then it's player!! :) + // !!!= === =.>>> if it works dont break it + state + .universe + .get_component_manager() + .get_entities_with::() + .into_iter() + .collect() +} + pub async fn broadcast( packet: &impl NetEncode, state: &GlobalState, opts: BroadcastOptions, ) -> NetResult<()> { - let entities = match opts.only_entities { - None => state - .universe - .get_component_manager() - .get_entities_with::(), + let mut entities = match opts.only_entities { + None => get_all_play_players(state), Some(entities) => entities, }; + // Remove excluded entities if any + if let Some(except_entities) = opts.except_entities { + entities.retain(|entity| !except_entities.contains(entity)); + } + + // No entities to broadcast to + if entities.is_empty() { + return Ok(()); + } + // Pre-encode the packet to save resources. let packet = { let mut buffer = Vec::new(); diff --git a/src/lib/storage/src/lmdb.rs b/src/lib/storage/src/lmdb.rs index bc9c5960..fe5fd660 100644 --- a/src/lib/storage/src/lmdb.rs +++ b/src/lib/storage/src/lmdb.rs @@ -159,6 +159,45 @@ impl LmdbBackend { .expect("Failed to run tokio task") } + pub async fn batch_upsert( + &self, + table: String, + data: Vec<(u128, Vec)>, + ) -> Result<(), StorageError> { + let env = self.env.clone(); + tokio::task::spawn_blocking(move || { + let mut rw_txn = env.write_txn()?; + + // Open or create the database for the given table + let db = env.create_database::, Bytes>(&mut rw_txn, Some(&table))?; + + // Create a map of keys and their associated values + let keymap: HashMap> = data.iter().map(|(k, v)| (*k, v)).collect(); + + // Iterate through the keys in sorted order + let mut sorted_keys: Vec = keymap.keys().cloned().collect(); + sorted_keys.sort(); + + // Iterate through the sorted keys to perform upserts + for key in sorted_keys { + // Check if the key already exists + if db.get(&rw_txn, &key)?.is_some() { + // Update the value if it exists (you can modify this logic as needed) + db.put(&mut rw_txn, &key, keymap[&key])?; + } else { + // Insert the new key-value pair if the key doesn't exist + db.put(&mut rw_txn, &key, keymap[&key])?; + } + } + + // Commit the transaction after all upserts are performed + rw_txn.commit()?; + Ok(()) + }) + .await + .expect("Failed to run tokio task") + } + pub async fn exists(&self, table: String, key: u128) -> Result { let env = self.env.clone(); tokio::task::spawn_blocking(move || { diff --git a/src/lib/utils/general_purpose/src/paths/exe_path.rs b/src/lib/utils/general_purpose/src/paths/exe_path.rs new file mode 100644 index 00000000..d6ab1366 --- /dev/null +++ b/src/lib/utils/general_purpose/src/paths/exe_path.rs @@ -0,0 +1,43 @@ +use std::path::PathBuf; +use std::env::current_exe; + +#[derive(thiserror::Error, Debug)] +pub enum RootPathError { + #[error("Failed to get the current executable location.")] + IoError(#[from] std::io::Error), + #[error("Failed to get the parent directory of the executable.")] + NoParent, +} + +pub fn get_root_path() -> PathBuf { + // Since it should technically never fail. + // And if it fails, then it's a critical error, and the program should exit. + get_root_path_internal().unwrap() +} + +fn get_root_path_internal() -> Result { + //! Returns the root path of the executable. + //! e.g. + //! - If the executable is located at "D:/server/ferrumc.exe", + //! this function will return "D:/server". + //! + //! + //! # Errors + //! - If the current executable location cannot be found. (RootPathError::IoError) + //! - If the parent directory of the executable cannot be found. (RootPathError::NoParent) + //! + //! # Examples + //! ```rust + //! use ferrumc_general_purpose::paths::get_root_path; + //! + //! // Returns a Result + //! let root_path = get_root_path(); + //! + //! let favicon_path = root_path.join("icon.png"); + //! ``` + //! + let exe_location = current_exe()?; + let exe_dir = exe_location.parent().ok_or(RootPathError::NoParent)?; + + Ok(exe_dir.to_path_buf()) +} \ No newline at end of file diff --git a/src/lib/utils/logging/Cargo.toml b/src/lib/utils/logging/Cargo.toml index 5bc7a18d..2025125b 100644 --- a/src/lib/utils/logging/Cargo.toml +++ b/src/lib/utils/logging/Cargo.toml @@ -6,7 +6,9 @@ edition = "2021" [dependencies] tracing = { workspace = true } tracing-subscriber = { workspace = true } +tracing-appender = { workspace = true } tokio = { workspace = true } ferrumc-profiling = { workspace = true } +ferrumc-general-purpose = { workspace = true } thiserror = { workspace = true } console-subscriber = { workspace = true } diff --git a/src/lib/utils/logging/src/lib.rs b/src/lib/utils/logging/src/lib.rs index 85c5b55c..97fece40 100644 --- a/src/lib/utils/logging/src/lib.rs +++ b/src/lib/utils/logging/src/lib.rs @@ -1,8 +1,10 @@ pub mod errors; +use ferrumc_general_purpose::paths::get_root_path; use ferrumc_profiling::ProfilerTracingLayer; use tracing::Level; -use tracing_subscriber::fmt::Layer; +use tracing_appender::rolling::Rotation; +use tracing_subscriber::fmt::{layer, Layer}; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::EnvFilter; @@ -11,10 +13,33 @@ pub fn init_logging(trace_level: Level) { //let console = console_subscriber::spawn(); let env_filter = EnvFilter::from_default_env().add_directive(trace_level.into()); + let is_verbose = trace_level > Level::INFO; + + let file_layer = { + let file_appender = tracing_appender::rolling::Builder::new() + .rotation(Rotation::DAILY) + .filename_prefix("ferrumc") + .filename_suffix("log.txt") + .build(get_root_path().join("logs")) + .unwrap(); + + if is_verbose { + layer().with_writer(file_appender).with_ansi(false) + } else { + layer() + .with_ansi(false) + .with_writer(file_appender) + .with_target(false) + .with_thread_ids(false) + .with_line_number(false) + .with_file(false) + } + }; + let mut fmt_layer = Layer::default(); // remove path from logs if log level is INFO - if trace_level == Level::INFO { + if !is_verbose { fmt_layer = fmt_layer .with_target(false) .with_thread_ids(false) @@ -24,7 +49,7 @@ pub fn init_logging(trace_level: Level) { let profiler_layer = ProfilerTracingLayer; tracing_subscriber::registry() - // .with(console) + .with(file_layer) .with(env_filter) .with(profiler_layer) .with(fmt_layer) diff --git a/src/lib/world/src/db_functions.rs b/src/lib/world/src/db_functions.rs index 8a7730c2..efa69ab3 100644 --- a/src/lib/world/src/db_functions.rs +++ b/src/lib/world/src/db_functions.rs @@ -1,3 +1,4 @@ +// db_functions.rs use crate::chunk_format::Chunk; use crate::errors::WorldError; use crate::World; @@ -129,6 +130,31 @@ pub(crate) async fn save_chunk_internal(world: &World, chunk: Chunk) -> Result<( Ok(()) } +pub(crate) async fn save_chunk_internal_batch( + world: &World, + chunks: Vec, +) -> Result<(), WorldError> { + // Prepare the batch data for the upsert + let mut batch_data = Vec::new(); + + for chunk in chunks.iter() { + // Compress the chunk and encode it + let as_bytes = world.compressor.compress(&bitcode::encode(chunk))?; + // Create the key for the chunk + let digest = create_key(chunk.dimension.as_str(), chunk.x, chunk.z); + // Collect the key-value pair into the batch data + batch_data.push((digest, as_bytes)); + } + + // Perform the batch upsert + world + .storage_backend + .batch_upsert("chunks".to_string(), batch_data) + .await?; + + Ok(()) +} + pub(crate) async fn load_chunk_internal( world: &World, compressor: &Compressor, diff --git a/src/lib/world/src/errors.rs b/src/lib/world/src/errors.rs index 9119e249..7fcb6ad6 100644 --- a/src/lib/world/src/errors.rs +++ b/src/lib/world/src/errors.rs @@ -38,6 +38,16 @@ pub enum WorldError { MissingBlockMapping(Palette), #[error("Invalid memory map size: {0}")] InvalidMapSize(u64), + #[error("Task Join Error: {0}")] + TaskJoinError(String), +} + +// implemente AcquireError for WorldError +use tokio::sync::AcquireError; +impl From for WorldError { + fn from(err: AcquireError) -> Self { + WorldError::TaskJoinError(err.to_string()) + } } impl From for WorldError { diff --git a/src/lib/world/src/importing.rs b/src/lib/world/src/importing.rs index d2ba641a..10a8cf0c 100644 --- a/src/lib/world/src/importing.rs +++ b/src/lib/world/src/importing.rs @@ -1,172 +1,221 @@ -use crate::db_functions::save_chunk_internal; +use crate::db_functions::save_chunk_internal_batch; use crate::errors::WorldError; use crate::vanilla_chunk_format::VanillaChunk; +use crate::Chunk; use crate::World; use ferrumc_anvil::load_anvil_file; -use ferrumc_general_purpose::paths::BetterPathExt; -use indicatif::ProgressBar; +//use ferrumc_general_purpose::paths::BetterPathExt; +use indicatif::{ProgressBar, ProgressStyle}; use rayon::prelude::*; -use std::path::PathBuf; -use std::sync::atomic::AtomicU64; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; +use tokio::sync::Semaphore; use tokio::task::JoinSet; use tracing::{error, info}; -/// This function is used to check if the import path is valid. It checks if the path exists, if it -/// is a file, if the region folder exists, if the region folder is a file, and if the region folder -/// is empty. -fn check_paths_validity(import_dir: PathBuf) -> Result<(), WorldError> { - if !import_dir.exists() { - error!( - "Import path does not exist: {}", - import_dir.better_display() - ); - return Err(WorldError::InvalidImportPath(import_dir.better_display())); - } - if import_dir.is_file() { - error!("Import path is a file: {}", import_dir.better_display()); - return Err(WorldError::InvalidImportPath(import_dir.better_display())); - } +/// TODO: dynamically find the best according to the system +const BATCH_SIZE: usize = 1000; // Number of chunks to process before flushing +const MAX_CONCURRENT_TASKS: usize = 512; // Limit concurrent tasks to prevent memory issues +const FLUSH_INTERVAL: u64 = 10_000; // Flush every 10,000 chunks - if let Ok(dir) = import_dir.read_dir() { - if dir.count() == 0 { - error!( - "Import path's region folder is empty: {}", - import_dir.better_display() - ); - return Err(WorldError::NoRegionFiles); +impl World { + async fn process_chunk_batch( + &self, + chunks: Vec, + progress: Arc, + processed_since_flush: Arc, + ) -> Result<(), WorldError> { + let chunk_objects: Vec = chunks + .into_iter() + .filter_map(|chunk| chunk.to_custom_format().ok()) + .collect(); + + let mut success_count = 0; + if let Ok(()) = save_chunk_internal_batch(self, chunk_objects.clone()).await { + success_count = chunk_objects.len(); } - } else { - error!( - "Could not read import path's region folder: {}", - import_dir.better_display() - ); - return Err(WorldError::InvalidImportPath(import_dir.better_display())); + + progress.inc(success_count.try_into().unwrap()); + + let total_processed = processed_since_flush + .fetch_add(success_count as u64, Ordering::Relaxed) + + success_count as u64; + if total_processed >= FLUSH_INTERVAL { + self.storage_backend.flush().await?; + processed_since_flush.store(0, Ordering::Relaxed); + info!("Performed periodic flush after {} chunks", total_processed); + } + + Ok(()) } - Ok(()) -} -impl World { - fn get_chunk_count(&self, import_dir: PathBuf) -> Result { + fn get_chunk_count(&self, import_dir: &Path) -> Result { info!("Counting chunks in import directory..."); let regions_dir = import_dir.join("region").read_dir()?; let chunk_count = AtomicU64::new(0); + regions_dir - .into_iter() .par_bridge() - .for_each(|region_file| match region_file { - Ok(dir_entry) => { - if dir_entry.path().is_dir() { - error!( - "Region file is a directory: {}", - dir_entry.path().to_string_lossy() - ); - } else { - let file_path = dir_entry.path(); - let Ok(anvil_file) = load_anvil_file(file_path.clone()) else { - error!( - "Could not load region file: {}", - file_path.clone().display() - ); - return; - }; - let locations = anvil_file.get_locations(); - chunk_count.fetch_add( - locations.len() as u64, - std::sync::atomic::Ordering::Relaxed, - ); - } + .try_for_each(|region_file| -> Result<(), WorldError> { + let entry = region_file?; + if entry.path().is_dir() { + return Ok(()); } - Err(e) => { - error!("Could not read region file: {}", e); + + if let Ok(anvil_file) = load_anvil_file(entry.path()) { + chunk_count + .fetch_add(anvil_file.get_locations().len() as u64, Ordering::Relaxed); } - }); - Ok(chunk_count.load(std::sync::atomic::Ordering::Relaxed)) + Ok(()) + })?; + + Ok(chunk_count.load(Ordering::Relaxed)) } pub async fn import(&mut self, import_dir: PathBuf, _: PathBuf) -> Result<(), WorldError> { - // Check if the import path is valid. We can assume the database path is valid since we - // checked it in the config validity check. - check_paths_validity(import_dir.clone())?; - let regions_dir = import_dir.join("region").read_dir()?; - let progress_bar = Arc::new(ProgressBar::new(self.get_chunk_count(import_dir)?)); - info!("Importing chunks from import directory..."); - let start = std::time::Instant::now(); - let mut task_set = JoinSet::new(); + check_paths_validity(&import_dir)?; + + let total_chunks = self.get_chunk_count(&import_dir)?; + let progress_style = ProgressStyle::default_bar() + .template("[{elapsed_precise}] {bar:40.cyan/blue} {pos:>7}/{len:7} {msg}") + .unwrap(); + + let progress = Arc::new(ProgressBar::new(total_chunks)); + progress.set_style(progress_style); + self.storage_backend .create_table("chunks".to_string()) .await?; - for region_file in regions_dir { - match region_file { - Ok(dir_entry) => { - if dir_entry.path().is_dir() { - error!( - "Region file is a directory: {}", - dir_entry.path().to_string_lossy() - ); - } else { - let file_path = dir_entry.path(); - let Ok(anvil_file) = load_anvil_file(file_path.clone()) else { - error!( - "Could not load region file: {}", - file_path.clone().display() + + info!("Starting chunk import..."); + let start = std::time::Instant::now(); + + let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT_TASKS)); + let mut task_set = JoinSet::new(); + + let processed_since_flush = Arc::new(AtomicU64::new(0)); + + let regions_dir = import_dir.join("region").read_dir()?; + let mut current_batch = Vec::with_capacity(BATCH_SIZE); + + for region_result in regions_dir { + let region_entry = region_result?; + if region_entry.path().is_dir() { + continue; + } + + let anvil_file = match load_anvil_file(region_entry.path()) { + Ok(file) => file, + Err(e) => { + error!( + "Failed to load region file {}: {}", + region_entry.path().display(), + e + ); + continue; + } + }; + + for location in anvil_file.get_locations() { + if let Ok(Some(chunk_data)) = anvil_file.get_chunk_from_location(location) { + if let Ok(vanilla_chunk) = VanillaChunk::from_bytes(&chunk_data) { + current_batch.push(vanilla_chunk); + + if current_batch.len() >= BATCH_SIZE { + let batch = std::mem::replace( + &mut current_batch, + Vec::with_capacity(BATCH_SIZE), ); - continue; - }; - let locations = anvil_file.get_locations(); - for location in locations { - // haha match statement go brrrrt - match anvil_file.get_chunk_from_location(location) { - Ok(possible_chunk) => match possible_chunk { - Some(chunk) => match VanillaChunk::from_bytes(&chunk) { - Ok(vanilla_chunk) => { - let cloned_progress_bar = progress_bar.clone(); - let self_clone = self.clone(); - task_set.spawn(async move { - if let Ok(chunk) = vanilla_chunk.to_custom_format() { - if let Err(e) = save_chunk_internal(&self_clone, chunk).await { - error!("Could not save chunk: {}", e); - } else { - cloned_progress_bar.inc(1); - } - } else { - error!("Could not convert chunk to custom format: {:?}", chunk); - } - }); - } - Err(e) => { - error!( - "Could not convert chunk to vanilla format: {}", - e - ); - } - }, - None => { - error!("Chunk is empty"); - } - }, - Err(e) => { - error!("Could not get chunk from location: {}", e); + let progress_clone = Arc::clone(&progress); + let self_clone = self.clone(); + let permit = Arc::clone(&semaphore).acquire_owned().await?; + let processed_since_flush_clone = Arc::clone(&processed_since_flush); + + task_set.spawn(async move { + let _permit = permit; + if let Err(e) = self_clone + .process_chunk_batch( + batch, + progress_clone, + processed_since_flush_clone, + ) + .await + { + error!("Batch processing error: {}", e); } - } + }); } } } - Err(e) => { - error!("Could not read region file: {}", e); - } } } + + if !current_batch.is_empty() { + let progress_clone = Arc::clone(&progress); + let permit = Arc::clone(&semaphore).acquire_owned().await?; + let self_clone = self.clone(); + let processed_since_flush_clone = Arc::clone(&processed_since_flush); + + task_set.spawn(async move { + let _permit = permit; + if let Err(e) = self_clone + .process_chunk_batch(current_batch, progress_clone, processed_since_flush_clone) + .await + { + error!("Final batch processing error: {}", e); + } + }); + } + while task_set.join_next().await.is_some() {} + self.sync().await?; - progress_bar.clone().finish(); + self.storage_backend.flush().await?; + + progress.finish(); + info!( "Imported {} chunks in {:?}", - progress_bar.clone().position(), + progress.position(), start.elapsed() ); - self.storage_backend.flush().await?; Ok(()) } } + +fn check_paths_validity(import_dir: &Path) -> Result<(), WorldError> { + if !import_dir.exists() { + return Err(WorldError::InvalidImportPath( + import_dir.display().to_string(), + )); + } + if import_dir.is_file() { + return Err(WorldError::InvalidImportPath( + import_dir.display().to_string(), + )); + } + + let region_dir = import_dir.join("region"); + if !region_dir.exists() || !region_dir.is_dir() { + return Err(WorldError::InvalidImportPath( + import_dir.display().to_string(), + )); + } + + match region_dir.read_dir() { + Ok(dir) => { + if dir.count() == 0 { + return Err(WorldError::NoRegionFiles); + } + } + Err(_) => { + return Err(WorldError::InvalidImportPath( + import_dir.display().to_string(), + )); + } + } + + Ok(()) +}