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
- 🚄 Extremely fast and adaptable update speeds
+ 🚄 Extremely fast
@@ -50,7 +50,7 @@ our [Discord server](https://discord.gg/qT5J8EMjwk) for help or to discuss the p
- 🗂️ Customizable configuration
+ 🗂️ Straightforward Configuration
@@ -58,7 +58,7 @@ our [Discord server](https://discord.gg/qT5J8EMjwk) for help or to discuss the p
- 🌐 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*
@@ -82,19 +82,19 @@ our [Discord server](https://discord.gg/qT5J8EMjwk) for help or to discuss the p
-
-
Ability to view other players
+ PvE mechanics, and entities.
-
World modification (place / break blocks etc)
-
-
Chat & Command system
+ Web based server dashboard
-
Optimizations
-
-
Plugin support (JVM currently, other languages will be considered later)
+ Plugin support (FFI currently, other languages will be considered later)
@@ -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
-
\ 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(())
+}