diff --git a/Cargo.toml b/Cargo.toml index b77081da..9b7b7203 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ members = [ "src/lib/derive_macros", "src/lib/ecs", "src/lib/events", + "src/lib/inventory", "src/lib/net", "src/lib/net/crates/codec", "src/lib/net/crates/encryption", @@ -94,6 +95,7 @@ ferrumc-logging = { path = "src/lib/utils/logging" } ferrumc-macros = { path = "src/lib/derive_macros" } ferrumc-nbt = { path = "src/lib/adapters/nbt" } ferrumc-net = { path = "src/lib/net" } +ferrumc-inventory = { path = "src/lib/inventory" } ferrumc-net-codec = { path = "src/lib/net/crates/codec" } ferrumc-net-encryption = { path = "src/lib/net/crates/encryption" } ferrumc-plugins = { path = "src/lib/plugins" } diff --git a/src/bin/Cargo.toml b/src/bin/Cargo.toml index 1451fff7..748b7f8d 100644 --- a/src/bin/Cargo.toml +++ b/src/bin/Cargo.toml @@ -19,6 +19,7 @@ ferrumc-plugins = { workspace = true } ferrumc-storage = { workspace = true } ferrumc-utils = { workspace = true } ferrumc-config = { workspace = true } +ferrumc-inventory = { workspace = true } ferrumc-profiling = { workspace = true } ferrumc-logging = { workspace = true } ferrumc-world = { workspace = true } diff --git a/src/bin/src/packet_handlers/containers.rs b/src/bin/src/packet_handlers/containers.rs new file mode 100644 index 00000000..84bdbcc4 --- /dev/null +++ b/src/bin/src/packet_handlers/containers.rs @@ -0,0 +1,111 @@ +use ferrumc_inventory::inventory::{Inventory, InventorySyncType}; +use ferrumc_inventory::slot::Slot; +use ferrumc_inventory::types::player::PlayerInventory; +use ferrumc_macros::event_handler; +use ferrumc_net::errors::NetError; +use ferrumc_net::packets::incoming::click_container::InventoryClickEvent; +use ferrumc_net::packets::incoming::close_container::InventoryCloseEvent; +use ferrumc_net::packets::incoming::set_creative_mode_slot::SetCreativeModeSlotEvent; +use ferrumc_net::packets::incoming::set_held_item::ChangeSlotEvent; +use ferrumc_net::packets::outgoing::set_equipment::{Equipment, EquipmentSlot, SetEquipmentPacket}; +use ferrumc_net::utils::broadcast::{broadcast, BroadcastOptions}; +use ferrumc_state::GlobalState; + +#[event_handler] +async fn container_close( + container_close_event: InventoryCloseEvent, + state: GlobalState, +) -> Result { + let conn_id = container_close_event.conn_id; + + let window_id = container_close_event.window_id; + if window_id != 0 { + state.universe.remove_component::(conn_id)?; + } + + Ok(container_close_event) +} + +#[event_handler] +async fn handle_container_click( + inventory_click_event: InventoryClickEvent, + state: GlobalState, +) -> Result { + if inventory_click_event.is_canceled { + return Err(NetError::Other(String::default())); + } + + let conn_id = inventory_click_event.conn_id; + let packet = &inventory_click_event.packet; + let mut inventory = state.universe.get_mut::(conn_id)?; + + if inventory.is_synced { + match packet.changed_slots.data.as_slice() { + [changed_slot] => { + let slot_num = changed_slot.slot_number; + + inventory + .set_slot(slot_num, Slot::from_network_slot(changed_slot.slot)) + .sync_inventory(conn_id, &InventorySyncType::Single(slot_num), state) + .await + .map_err(|err| NetError::Other(err.to_string()))?; + } + changed_slots => { + changed_slots.iter().for_each(|changed_slot| { + inventory.set_slot( + changed_slot.slot_number, + Slot::from_network_slot(changed_slot.slot), + ); + }); + + inventory + .sync_inventory(conn_id, &InventorySyncType::All, state) + .await + .map_err(|err| NetError::Other(err.to_string()))?; + } + } + } + + Ok(inventory_click_event) +} + +#[event_handler] +async fn set_creative_mode_slot( + creative_mode_slot: SetCreativeModeSlotEvent, + state: GlobalState, +) -> Result { + let conn_id = creative_mode_slot.conn_id; + + let mut inventory = state.universe.get_mut::(conn_id)?; + inventory.set_slot( + creative_mode_slot.slot, + Slot::from_network_slot(creative_mode_slot.clicked_item), + ); + + Ok(creative_mode_slot) +} + +#[event_handler] +async fn handle_carried_item( + change_slot_event: ChangeSlotEvent, + state: GlobalState, +) -> Result { + let conn_id = change_slot_event.conn_id; + + let mut inventory = state.universe.get_mut::(conn_id)?; + inventory.set_carried_item(change_slot_event.slot); + + let equipment = Equipment::new( + EquipmentSlot::MainHand, + Slot::with_item(change_slot_event.slot as i32).to_network_slot(), + ); + + broadcast( + &SetEquipmentPacket::new(conn_id, vec![equipment]), + &state, + BroadcastOptions::default().except([conn_id]), + ) + .await?; + + Ok(change_slot_event) +} diff --git a/src/bin/src/packet_handlers/login_process.rs b/src/bin/src/packet_handlers/login_process.rs index 0d2159e5..a98a9649 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::position::Position; use ferrumc_core::transform::rotation::Rotation; use ferrumc_ecs::components::storage::ComponentRefMut; use ferrumc_ecs::entities::Entity; +use ferrumc_inventory::types::player::PlayerInventory; use ferrumc_macros::event_handler; use ferrumc_net::connection::{ConnectionState, StreamWriter}; use ferrumc_net::errors::NetError; @@ -53,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 @@ -165,7 +166,8 @@ async fn handle_ack_finish_configuration( .add_component::(entity_id, Position::default())? .add_component::(entity_id, Rotation::default())? .add_component::(entity_id, OnGround::default())? - .add_component::(entity_id, ChunkReceiver::default())?; + .add_component::(entity_id, ChunkReceiver::default())? + .add_component::(entity_id, PlayerInventory::default())?; let mut writer = state.universe.get_mut::(entity_id)?; diff --git a/src/bin/src/packet_handlers/mod.rs b/src/bin/src/packet_handlers/mod.rs index 21fee1f7..50359528 100644 --- a/src/bin/src/packet_handlers/mod.rs +++ b/src/bin/src/packet_handlers/mod.rs @@ -1,4 +1,5 @@ mod animations; +mod containers; mod handshake; mod login_process; mod player; diff --git a/src/lib/derive_macros/src/events/mod.rs b/src/lib/derive_macros/src/events/mod.rs index ac4e0f95..184989c5 100644 --- a/src/lib/derive_macros/src/events/mod.rs +++ b/src/lib/derive_macros/src/events/mod.rs @@ -123,7 +123,8 @@ pub(crate) fn derive(input: TokenStream) -> TokenStream { quote! {crate} } FoundCrate::Name(name) => { - quote! {::#name} + let name = syn::Ident::new(&name, proc_macro2::Span::call_site()); + quote! {#name} } }; diff --git a/src/lib/derive_macros/src/inventory/mod.rs b/src/lib/derive_macros/src/inventory/mod.rs new file mode 100644 index 00000000..b979679e --- /dev/null +++ b/src/lib/derive_macros/src/inventory/mod.rs @@ -0,0 +1,185 @@ +use proc_macro::TokenStream; +use proc_macro_crate::{crate_name, FoundCrate}; +use quote::quote; +use syn::{Data, Visibility}; + +pub fn create(input: TokenStream) -> TokenStream { + let input = syn::parse_macro_input!(input as syn::DeriveInput); + let name = &input.ident; + + if let Data::Struct(data_struct) = &input.data { + let mut default_statements = Vec::new(); + let mut field_statements = Vec::new(); + + let found_crate = crate_name("ferrumc-inventory").unwrap(); + + let net_crate = match found_crate { + FoundCrate::Itself => { + quote! { crate } + } + FoundCrate::Name(name) => { + let name = syn::Ident::new(&name, proc_macro2::Span::call_site()); + quote! { #name } + } + }; + + let mut inventory_type_creator = quote! {}; + + // Extract inventory_type attribute + for attr in &input.attrs { + if attr.path().is_ident("inventory") { + attr.parse_nested_meta(|meta| { + if let Some(ident) = meta.path.get_ident() { + let ident_str = ident.to_string(); + let ident_str_ident = syn::Ident::new(&ident_str, proc_macro2::Span::call_site()); + match ident_str.as_str() { + "inventory_type" => { + if let Some(ident) = handle_meta("inventory_type", &meta, true) { + inventory_type_creator = quote! { + #inventory_type_creator + .#ident_str_ident(#net_crate::inventory::InventoryType::#ident) + }; + } + }, + "is_synced" => { + if let Some(expr) = handle_meta("is_synced", &meta, false) { + inventory_type_creator = quote! { + #inventory_type_creator + .#ident_str_ident(#expr) + }; + } + }, + "title" => { + if let Some(expr) = handle_meta("title", &meta, false) { + inventory_type_creator = quote! { + #inventory_type_creator + .#ident_str_ident(ferrumc_text::TextComponent::new(#expr).build()) + }; + } + } + _ => {}, + } + } + Ok(()) + }) + .unwrap(); + } + } + + // Process fields + for field in &data_struct.fields { + let field_name = &field.ident.clone().expect("Missing field"); + + let mut id_expr = None; + let mut value_expr = None; + + if let Visibility::Public(_) = &field.vis { + for attr in &field.attrs { + if attr.path().is_ident("slot") { + attr.parse_nested_meta(|meta| { + if let Some(ident) = meta.path.get_ident() { + let ident_str = ident.to_string(); + match ident_str.as_str() { + "id" => { + id_expr = handle_meta("id", &meta, true); + } + "default_value" => { + value_expr = handle_meta("default_value", &meta, true); + } + _ => {} + } + } + Ok(()) + }) + .unwrap(); + } + } + } + + // Generate default initialization and setter methods + if let (Some(id), Some(value)) = (id_expr, value_expr) { + default_statements.push(quote! { + #field_name: #value, + }); + + let setter_name = + syn::Ident::new(&format!("set_{}", field_name), field_name.span()); + field_statements.push(quote! { + pub fn #setter_name + Copy>(&mut self, #field_name: S) { + self.#field_name = #field_name.into(); + self.set_slot(#id, #field_name); + } + }); + } + } + + // Generate the `new` method + let new_method = quote! { + pub fn new(id: u8) -> Self { + Self { + inventory: #net_crate::builder::InventoryBuilder::new(id) + #inventory_type_creator + .build(), + #(#default_statements)* + } + } + }; + + // Generate the complete implementation block + // Wacky ass code because rust is retarded + let output = quote! { + impl #name { + #new_method + + #(#field_statements)* + } + + impl std::ops::Deref for #name { + type Target = Inventory; + + fn deref(&self) -> &Self::Target { + &self.inventory + } + } + + impl std::ops::DerefMut for #name { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inventory + } + } + }; + + output.into() + } else { + TokenStream::new() + } +} + +fn handle_meta( + name: &str, + meta: &syn::meta::ParseNestedMeta, + is_required: bool, +) -> Option { + match meta.value() { + Ok(value) => { + if let Ok(value) = value.parse::() { + Some(value) + } else if is_required { + panic!("Failed to parse value for attribute '{}'", name); + } else { + None + } + } + Err(_) => { + if is_required { + panic!("Missing required attribute '{}'", name); + } + + None + } + } +} + +pub fn inventory_type(_args: TokenStream, input: TokenStream) -> TokenStream { + input +} diff --git a/src/lib/derive_macros/src/lib.rs b/src/lib/derive_macros/src/lib.rs index 6f6ca1a8..20198000 100644 --- a/src/lib/derive_macros/src/lib.rs +++ b/src/lib/derive_macros/src/lib.rs @@ -4,6 +4,7 @@ use proc_macro::TokenStream; mod events; mod helpers; +mod inventory; mod nbt; mod net; mod profiling; @@ -75,3 +76,15 @@ pub fn get_packet_entry(input: TokenStream) -> TokenStream { pub fn get_registry_entry(input: TokenStream) -> TokenStream { static_loading::registry::get(input) } + +// #=================== INVENTORY ===================# +#[proc_macro_derive(Inventory, attributes(slot))] +pub fn create_inventory(input: TokenStream) -> TokenStream { + inventory::create(input) +} + +#[proc_macro_attribute] +pub fn inventory(args: TokenStream, input: TokenStream) -> TokenStream { + inventory::inventory_type(args, input) +} +// #=================== INVENTORY ===================# diff --git a/src/lib/derive_macros/src/net/decode.rs b/src/lib/derive_macros/src/net/decode.rs index b3407f27..8d79c7e4 100644 --- a/src/lib/derive_macros/src/net/decode.rs +++ b/src/lib/derive_macros/src/net/decode.rs @@ -154,6 +154,8 @@ pub(crate) fn derive(input: TokenStream) -> TokenStream { let mut decode_statements = Vec::new(); let mut field_names = Vec::new(); + let optional_type_regex = regex::Regex::new(r"Option\s*<\s*(.+?)\s*>").unwrap(); + for field in fields { let field_name = field .ident @@ -168,24 +170,16 @@ pub(crate) fn derive(input: TokenStream) -> TokenStream { // 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"); + let value = meta.value().expect("Missing optional_trigger value"); - optional_trigger_expr = Some(val); - } else { - panic!("Expected an expression for optional_trigger"); - } + let value = value + .parse::() + .expect("Failed to parse optional_trigger"); - Ok(()) - }) - .expect("Failed to parse optional_trigger expression"); + optional_trigger_expr = Some(value); } } Ok(()) @@ -196,9 +190,16 @@ pub(crate) fn derive(input: TokenStream) -> TokenStream { // 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. + let field_type = quote! { #field_ty }.to_string(); + + let inner = optional_type_regex + .captures(&field_type) + .expect("Field must be Option") + .get(1) + .unwrap() + .as_str(); + let field_ty = syn::parse_str::(inner).expect("Failed to parse field type"); + decode_statements.push(quote! { let #field_name = { if #expr { diff --git a/src/lib/events/src/infrastructure.rs b/src/lib/events/src/infrastructure.rs index 16774b21..abf4d6a2 100644 --- a/src/lib/events/src/infrastructure.rs +++ b/src/lib/events/src/infrastructure.rs @@ -73,12 +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;"); + let Some(listeners) = EVENTS_LISTENERS.get(Self::name()) else { + return Ok(()); + }; // Convert listeners iterator into Stream stream::iter(listeners.iter()) @@ -99,9 +96,6 @@ pub trait Event: Sized + Send + Sync + 'static { }) .await?; - #[cfg(debug_assertions)] - tracing::trace!("Event {} took {:?}", Self::name(), start.elapsed()); - Ok(()) } diff --git a/src/lib/inventory/Cargo.toml b/src/lib/inventory/Cargo.toml new file mode 100644 index 00000000..5136c8be --- /dev/null +++ b/src/lib/inventory/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "ferrumc-inventory" +description = "Implements Inventory Capablities to FerrumC" +version = "0.1.0" +edition = "2024" + +[dependencies] +ferrumc-net = { workspace = true } +ferrumc-net-codec = { workspace = true } +ferrumc-text = { workspace = true } +ferrumc-ecs = { workspace = true } +ferrumc-events = { workspace = true } +ferrumc-macros = { workspace = true } +ferrumc-core = { workspace = true } +ferrumc-state = { workspace = true } + +tokio = {workspace = true } +dashmap = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } + +[lints] +workspace = true diff --git a/src/lib/inventory/src/builder.rs b/src/lib/inventory/src/builder.rs new file mode 100644 index 00000000..a264214e --- /dev/null +++ b/src/lib/inventory/src/builder.rs @@ -0,0 +1,66 @@ +use ferrumc_text::{TextComponent, TextComponentBuilder}; + +use crate::{ + contents::InventoryContents, + inventory::{Inventory, InventoryType}, + slot::Slot, +}; + +pub struct InventoryBuilder { + pub id: u8, + pub inventory_type: InventoryType, + pub title: TextComponent, + pub(crate) contents: InventoryContents, + pub carried_item: Slot, + pub is_synced: bool, +} + +impl InventoryBuilder { + pub fn new(id: u8) -> Self { + let inventory_type = InventoryType::Chest(3); + Self { + id, + inventory_type, + title: TextComponentBuilder::new("").build(), + contents: InventoryContents::empty(inventory_type), + carried_item: Slot::empty(), + is_synced: false, + } + } + + pub fn inventory_type(&mut self, inventory_type: InventoryType) -> &mut Self { + self.inventory_type = inventory_type; + self + } + + pub fn title(&mut self, title: TextComponent) -> &mut Self { + self.title = title; + self + } + + pub fn contents(&mut self, contents: InventoryContents) -> &mut Self { + self.contents = contents; + self + } + + pub fn carried_item(&mut self, carried_item: Slot) -> &mut Self { + self.carried_item = carried_item; + self + } + + pub fn is_synced(&mut self, is_synced: bool) -> &mut Self { + self.is_synced = is_synced; + self + } + + pub fn build(&mut self) -> Inventory { + Inventory { + id: self.id, + inventory_type: self.inventory_type, + title: self.title.clone(), + contents: self.contents.clone(), + carried_item: self.carried_item, + is_synced: self.is_synced, + } + } +} diff --git a/src/lib/inventory/src/contents.rs b/src/lib/inventory/src/contents.rs new file mode 100644 index 00000000..a9333d4c --- /dev/null +++ b/src/lib/inventory/src/contents.rs @@ -0,0 +1,46 @@ +use crate::{inventory::InventoryType, slot::Slot}; +use ferrumc_net::packets::outgoing::set_container_slot::NetworkSlot; +use ferrumc_net_codec::net_types::length_prefixed_vec::LengthPrefixedVec; +use std::collections::BTreeMap; + +#[derive(Debug, Clone)] +pub struct InventoryContents { + pub contents: BTreeMap, + pub size: i16, +} + +impl InventoryContents { + pub fn empty(inventory_type: InventoryType) -> Self { + let mut empty = Self::new(inventory_type.get_size(), BTreeMap::new()); + empty.fill(Slot::empty()); + empty + } + + pub fn new(size: i16, contents: BTreeMap) -> Self { + Self { contents, size } + } + + pub fn fill + Copy>(&mut self, slot: S) { + for i in 0..self.size { + self.contents.insert(i, slot.into()); + } + } + + pub fn set_slot>(&mut self, slot_id: i16, slot: S) -> &mut Self { + self.contents.insert(slot_id, slot.into()); + self + } + + pub fn get_slot(&self, item: i16) -> Option { + self.contents.get(&item).copied() + } + + pub(crate) fn construct_packet_contents(&self) -> LengthPrefixedVec { + let mut contents = vec![]; + self.contents.iter().for_each(|(_, slot)| { + contents.push(slot.to_network_slot()); + }); + + LengthPrefixedVec::new(contents) + } +} diff --git a/src/lib/inventory/src/errors.rs b/src/lib/inventory/src/errors.rs new file mode 100644 index 00000000..f39e2654 --- /dev/null +++ b/src/lib/inventory/src/errors.rs @@ -0,0 +1,27 @@ +use ferrumc_ecs::{entities::Entity, errors::ECSError}; +use ferrumc_net::errors::NetError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum InventoryError { + #[error("Entity [{0}] already has an open inventory. Cannot open another one.")] + AlreadyOpenedInventory(Entity), + + #[error("Invalid equipment slot for PlayerInventory")] + InvalidEquipmentSlot, + + #[error("Invalid slot id in Inventory")] + InvalidSlot, + + #[error("Trying to sync an inventory that isn't syncable! [ID: {0}]")] + SyncingANonSyncedInventory(u8), + + #[error("Net error: [{0}].")] + NetError(#[from] NetError), + + #[error("ECS error: [{0}].")] + ECSError(#[from] ECSError), + + #[error("Unknown error occurred with inventories...")] + Unknown, +} diff --git a/src/lib/inventory/src/inventory.rs b/src/lib/inventory/src/inventory.rs new file mode 100644 index 00000000..39274ca5 --- /dev/null +++ b/src/lib/inventory/src/inventory.rs @@ -0,0 +1,364 @@ +use crate::contents::InventoryContents; +use crate::errors::InventoryError; +use crate::slot::Slot; +use std::collections::BTreeMap; + +use ferrumc_core::chunks::chunk_receiver::ChunkReceiver; +use ferrumc_ecs::{components::storage::ComponentRefMut, entities::Entity}; +use ferrumc_events::infrastructure::Event; +use ferrumc_net::connection::StreamWriter; +use ferrumc_net::packets::incoming::close_container::InventoryCloseEvent; +use ferrumc_net::packets::outgoing::close_container::CloseContainerPacket; +use ferrumc_net::packets::outgoing::open_screen::{OpenInventoryEvent, OpenScreenPacket}; +use ferrumc_net::packets::outgoing::set_container_content::SetContainerContentPacket; +use ferrumc_net::packets::outgoing::set_container_property::{ + ContainerProperty, SetContainerPropertyPacket, +}; +use ferrumc_net::packets::outgoing::set_container_slot::SetContainerSlotPacket; +use ferrumc_net_codec::encode::NetEncodeOpts; +use ferrumc_net_codec::net_types::var_int::VarInt; +use ferrumc_state::ServerState; +use ferrumc_text::TextComponent; +use std::sync::Arc; + +#[derive(Debug, Clone, Copy)] +pub enum InventoryType { + Chest(i8), + Anvil, + Beacon, + BlastFurnace, + BrewingStand, + CraftingTable, + EnchantmentTable, + Furnace, + Grindstone, + Hopper, + Dispenser, + Dropper, + Lectern, + Loom, + ShulkerBox, + SmithingTable, + Smoker, + Cartography, + Stonecutter, +} + +impl InventoryType { + pub fn get_id(&self) -> i32 { + match self { + InventoryType::Chest(i) => { + let value = i32::from(*i); + if (1..=6).contains(&value) { + value - 1 + } else { + 1 // defaults to 1 row chest + } + } + InventoryType::Anvil => 8, + InventoryType::Beacon => 9, + InventoryType::BlastFurnace => 10, + InventoryType::BrewingStand => 11, + InventoryType::CraftingTable => 12, + InventoryType::EnchantmentTable => 13, + InventoryType::Furnace => 14, + InventoryType::Grindstone => 15, + InventoryType::Hopper => 16, + InventoryType::Dispenser | InventoryType::Dropper => 6, + InventoryType::Lectern => 17, + InventoryType::Loom => 18, + InventoryType::ShulkerBox => 20, + InventoryType::SmithingTable => 21, + InventoryType::Smoker => 22, + InventoryType::Cartography => 23, + InventoryType::Stonecutter => 24, + } + } + + pub fn get_size(&self) -> i16 { + match self { + InventoryType::Chest(i) => i16::from(*i) * 9, + InventoryType::Anvil + | InventoryType::BlastFurnace + | InventoryType::Furnace + | InventoryType::Smoker + | InventoryType::Cartography + | InventoryType::Grindstone => 2, + InventoryType::Stonecutter | InventoryType::EnchantmentTable => 1, + InventoryType::Dispenser | InventoryType::Dropper => 8, + InventoryType::Loom | InventoryType::SmithingTable => 3, + InventoryType::Beacon => 0, + InventoryType::Hopper => 4, + InventoryType::ShulkerBox => 26, + InventoryType::CraftingTable => 9, + _ => 0, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +pub enum InventorySyncType { + All, + Single(i16), +} + +#[derive(Debug, Clone)] +pub struct Inventory { + pub id: u8, + pub inventory_type: InventoryType, + pub title: TextComponent, + pub(crate) contents: InventoryContents, + pub carried_item: Slot, + pub is_synced: bool, +} + +impl Inventory { + pub fn set_carried_item(&mut self, carried_item: u16) { + if !(0..=9).contains(&carried_item) { + return; + } + + let slot = self + .get_slot(36 + (carried_item as i16)) + .unwrap_or_else(Slot::empty); + + self.carried_item = slot; + } + + pub async fn send_inventory_properties( + &self, + properties: Vec, + mut writer: ComponentRefMut<'_, StreamWriter>, + ) -> Result<(), InventoryError> { + for property in properties { + writer + .send_packet( + &SetContainerPropertyPacket::new(self.id, property), + &NetEncodeOpts::WithLength, + ) + .await?; + } + + Ok(()) + } + + pub async fn send_inventory_slot_content( + &self, + slot_num: i16, + mut writer: ComponentRefMut<'_, StreamWriter>, + ) -> Result<(), InventoryError> { + let Some(slot) = self.get_slot(slot_num) else { + return Err(InventoryError::InvalidSlot); + }; + + writer + .send_packet( + &SetContainerSlotPacket::new( + VarInt::new(i32::from(self.id)), + slot_num, + slot.to_network_slot(), + ), + &NetEncodeOpts::WithLength, + ) + .await?; + + Ok(()) + } + + pub async fn send_inventory_content( + &self, + mut writer: ComponentRefMut<'_, StreamWriter>, + ) -> Result<(), InventoryError> { + writer + .send_packet( + &SetContainerContentPacket::new( + self.id, + self.contents.construct_packet_contents(), + self.carried_item.to_network_slot(), + ), + &NetEncodeOpts::WithLength, + ) + .await?; + + Ok(()) + } + + pub async fn sync_inventory_with( + &mut self, + sync_type: &InventorySyncType, + writer: ComponentRefMut<'_, StreamWriter>, + ) -> Result<(), InventoryError> { + match sync_type { + InventorySyncType::All => self.send_inventory_content(writer).await?, + InventorySyncType::Single(slot_num) => { + self.send_inventory_slot_content(*slot_num, writer).await? + } + } + Ok(()) + } + + pub async fn sync_inventory( + &mut self, + without_entity: Entity, + sync_type: &InventorySyncType, + state: Arc, + ) -> Result<(), InventoryError> { + if !self.is_synced { + return Err(InventoryError::SyncingANonSyncedInventory(self.id)); + } + + let universe = &state.universe; + let query = universe + .get_component_manager() + .get_entities_with::(); + + for entity_id in query { + if entity_id == without_entity { + continue; + } + + let inventory_result = universe.get_mut::(entity_id); + if let Ok(inventory) = inventory_result { + if self.id != inventory.id { + continue; + } + + let writer = universe.get_mut::(entity_id)?; + self.sync_inventory_with(sync_type, writer).await?; + } + } + + Ok(()) + } + + pub async fn add_viewer( + &self, + state: Arc, + entity_id: Entity, + ) -> Result<(), InventoryError> { + let universe = &state.universe; + let mut writer = universe.get_mut::(entity_id)?; + + if universe.get::(entity_id).is_ok() { + return Err(InventoryError::AlreadyOpenedInventory(entity_id)); + } + + let packet = + OpenScreenPacket::new(self.id, self.inventory_type.get_id(), self.title.clone()); + + writer + .send_packet(&packet, &NetEncodeOpts::WithLength) + .await?; + + self.send_inventory_content(writer).await?; + + // handle event + let event = OpenInventoryEvent::new(entity_id, self.id); + OpenInventoryEvent::trigger(event, state.clone()).await?; + + universe.add_component::(entity_id, self.clone())?; + Ok(()) + } + + pub async fn remove_viewer( + &mut self, + state: Arc, + entity_id: Entity, + ) -> Result<(), InventoryError> { + let universe = &state.universe; + let mut writer = universe.get_mut::(entity_id)?; + let inventory = universe.get::(entity_id)?; + + writer + .send_packet( + &CloseContainerPacket::new(self.id), + &NetEncodeOpts::WithLength, + ) + .await?; + + // handle event + let event = InventoryCloseEvent::new(entity_id, inventory.id); + InventoryCloseEvent::trigger(event, state.clone()).await?; + Ok(()) + } + + pub fn set_slot + Copy>(&mut self, slot_id: i16, slot: S) -> &mut Self { + let size = self.inventory_type.get_size(); + if (0..=size).contains(&slot_id) { + self.contents.set_slot(slot_id, slot); + } + + self + } + + pub fn set_slots + Copy>(&mut self, slots: Vec<(i16, S)>) -> &mut Self { + for (slot_num, slot) in slots { + self.set_slot(slot_num, slot); + } + + self + } + + pub fn get_slot(&self, slot_id: i16) -> Option { + let size = self.inventory_type.get_size(); + if (0..=size).contains(&slot_id) { + self.contents.get_slot(slot_id) + } else { + None + } + } + + pub fn get_contents(&self) -> &BTreeMap { + &self.contents.contents + } + + pub fn get_contents_mut(&mut self) -> &mut BTreeMap { + &mut self.contents.contents + } + + pub fn clear(&mut self) { + self.get_contents_mut().clear(); + } + + pub fn fill + Copy>(&mut self, slot: S) { + self.contents.fill(slot); + } + + pub fn contains(&self, item: i32) -> bool { + self.get_contents().iter().any(|slot| slot.1.item == item) + } + + pub fn contains_atleast(&self, item: i32, amount: i32) -> bool { + let mut container_amount = 0; + self.get_contents().iter().for_each(|(_, slot)| { + if slot.item == item { + container_amount += slot.count; + } + }); + + container_amount >= amount + } + + pub fn get_first_empty(&self) -> i16 { + let contents = self.get_contents(); + for i in 0..self.get_size() { + if contents.get(&i).is_none() { + return i; + } + } + + 0 + } + + pub fn get_size(&self) -> i16 { + self.inventory_type.get_size() + } + + pub fn is_empty(&self) -> bool { + self.get_contents().is_empty() + } + + pub fn is_full(&self) -> bool { + self.get_contents().len() == self.get_size() as usize + } +} diff --git a/src/lib/inventory/src/lib.rs b/src/lib/inventory/src/lib.rs new file mode 100644 index 00000000..90c54e33 --- /dev/null +++ b/src/lib/inventory/src/lib.rs @@ -0,0 +1,6 @@ +pub mod builder; +pub mod contents; +pub mod errors; +pub mod inventory; +pub mod slot; +pub mod types; diff --git a/src/lib/inventory/src/slot.rs b/src/lib/inventory/src/slot.rs new file mode 100644 index 00000000..2ae501be --- /dev/null +++ b/src/lib/inventory/src/slot.rs @@ -0,0 +1,50 @@ +use ferrumc_net::packets::outgoing::set_container_slot::NetworkSlot; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Slot { + pub count: i32, + pub item: i32, +} + +impl Default for Slot { + fn default() -> Self { + Self::empty() + } +} + +impl From for Slot { + fn from(value: i32) -> Self { + Self::with_item(value) + } +} + +impl From<(i32, i32)> for Slot { + fn from(value: (i32, i32)) -> Self { + Self::new(value.0, value.1) + } +} + +impl Slot { + pub fn new(count: i32, item: i32) -> Self { + Self { count, item } + } + + pub fn with_item(item: i32) -> Self { + Self::new(1, item) + } + + pub fn empty() -> Self { + Self::new(0, 0) + } + + pub fn from_network_slot(slot: NetworkSlot) -> Self { + match slot.item_id { + Some(item) => Self::new(*slot.item_count, *item), + None => Self::empty(), + } + } + + pub fn to_network_slot(&self) -> NetworkSlot { + NetworkSlot::new(self.count, self.item) + } +} diff --git a/src/lib/inventory/src/types/mod.rs b/src/lib/inventory/src/types/mod.rs new file mode 100644 index 00000000..fb15eb58 --- /dev/null +++ b/src/lib/inventory/src/types/mod.rs @@ -0,0 +1,169 @@ +use ferrumc_macros::{Inventory, inventory}; + +use crate::{inventory::Inventory, slot::Slot}; + +pub mod player; + +#[derive(Inventory, Debug, Clone)] +#[inventory(inventory_type = Anvil)] +pub struct AnvilInventory { + inventory: Inventory, + #[slot(id = 0, default_value = Slot::empty())] + pub first: Slot, + #[slot(id = 1, default_value = Slot::empty())] + pub second: Slot, + #[slot(id = 2, default_value = Slot::empty())] + pub result: Slot, +} + +#[derive(Inventory, Debug, Clone)] +#[inventory(inventory_type = Beacon)] +pub struct BeaconInventory { + inventory: Inventory, + #[slot(id = 0, default_value = Slot::empty())] + pub powered_item: Slot, +} + +#[derive(Inventory, Debug, Clone)] +#[inventory(inventory_type = Cartography)] +pub struct EnchantingInventory { + inventory: Inventory, + #[slot(id = 0, default_value = Slot::empty())] + pub map: Slot, + #[slot(id = 1, default_value = Slot::empty())] + pub paper: Slot, + #[slot(id = 2, default_value = Slot::empty())] + pub output: Slot, +} + +#[derive(Inventory, Debug, Clone)] +#[inventory(inventory_type = Furnace)] +pub struct FurnaceInventory { + inventory: Inventory, + #[slot(id = 0, default_value = Slot::empty())] + pub ingredient: Slot, + #[slot(id = 1, default_value = Slot::empty())] + pub fuel: Slot, + #[slot(id = 2, default_value = Slot::empty())] + pub output: Slot, +} + +#[derive(Inventory, Debug, Clone)] +#[inventory(inventory_type = BlastFurnace)] +pub struct BlastFurnaceInventory { + inventory: Inventory, + #[slot(id = 0, default_value = Slot::empty())] + pub ingredient: Slot, + #[slot(id = 1, default_value = Slot::empty())] + pub fuel: Slot, + #[slot(id = 2, default_value = Slot::empty())] + pub output: Slot, +} + +#[derive(Inventory, Debug, Clone)] +#[inventory(inventory_type = Smoker)] +pub struct SmokerInventory { + inventory: Inventory, + #[slot(id = 0, default_value = Slot::empty())] + pub ingredient: Slot, + #[slot(id = 1, default_value = Slot::empty())] + pub fuel: Slot, + #[slot(id = 2, default_value = Slot::empty())] + pub output: Slot, +} + +#[derive(Inventory, Debug, Clone)] +#[inventory(inventory_type = Grindstone)] +pub struct GrindstoneInventory { + inventory: Inventory, + #[slot(id = 0, default_value = Slot::empty())] + pub first: Slot, + #[slot(id = 1, default_value = Slot::empty())] + pub second: Slot, + #[slot(id = 2, default_value = Slot::empty())] + pub result: Slot, +} + +#[derive(Inventory, Debug)] +#[inventory(inventory_type = Loom)] +pub struct LoomInventory { + inventory: Inventory, + #[slot(id = 0, default_value = Slot::empty())] + pub banner: Slot, + #[slot(id = 1, default_value = Slot::empty())] + pub dye: Slot, + #[slot(id = 2, default_value = Slot::empty())] + pub pattern: Slot, + #[slot(id = 3, default_value = Slot::empty())] + pub result: Slot, +} + +#[derive(Inventory, Debug, Clone)] +#[inventory(inventory_type = SmithingTable)] +pub struct SmithingTableInventory { + inventory: Inventory, + #[slot(id = 0, default_value = Slot::empty())] + pub template: Slot, + #[slot(id = 1, default_value = Slot::empty())] + pub base: Slot, + #[slot(id = 2, default_value = Slot::empty())] + pub additional: Slot, + #[slot(id = 3, default_value = Slot::empty())] + pub result: Slot, +} + +#[derive(Inventory, Debug, Clone)] +#[inventory(inventory_type = Stonecutter)] +pub struct StoneCutterInventory { + inventory: Inventory, + #[slot(id = 0, default_value = Slot::empty())] + pub input: Slot, + #[slot(id = 1, default_value = Slot::empty())] + pub result: Slot, +} + +#[derive(Inventory, Debug, Clone)] +#[inventory(inventory_type = BrewingStand)] +pub struct BrewingStandInventory { + inventory: Inventory, + #[slot(id = 3, default_value = Slot::empty())] + pub potion_ingredient: Slot, + #[slot(id = 4, default_value = Slot::empty())] + pub blaze_powder: Slot, +} + +impl BrewingStandInventory { + pub fn set_potion_slot + Copy>(&mut self, index: i16, slot: S) { + if (0..=2).contains(&index) { + self.set_slot(index, slot); + } + } +} + +#[derive(Inventory, Debug, Clone)] +#[inventory(inventory_type = CraftingTable)] +pub struct CraftingTableInventory { + inventory: Inventory, + #[slot(id = 0, default_value = Slot::empty())] + pub output: Slot, +} + +impl CraftingTableInventory { + pub fn set_crafting_input + Copy>(&mut self, index: i16, slot: S) { + if (0..=8).contains(&index) { + self.set_slot(1 + index, slot); + } + } +} + +#[derive(Inventory, Debug, Clone)] +#[inventory(inventory_type = Hopper)] +pub struct HopperInventory { + inventory: Inventory, +} + +#[derive(Inventory, Debug, Clone)] +#[inventory(inventory_type = ShulkerBox)] +pub struct ShulkerBoxInventory { + inventory: Inventory, +} diff --git a/src/lib/inventory/src/types/player.rs b/src/lib/inventory/src/types/player.rs new file mode 100644 index 00000000..7c7bfb10 --- /dev/null +++ b/src/lib/inventory/src/types/player.rs @@ -0,0 +1,93 @@ +use std::ops::{Deref, DerefMut}; + +use ferrumc_net::packets::outgoing::set_equipment::EquipmentSlot; + +use crate::{ + builder::InventoryBuilder, + errors::InventoryError, + inventory::{Inventory, InventoryType}, + slot::Slot, +}; + +#[derive(Default, Debug, Clone)] +pub struct EquipmentContent { + pub helmet: Slot, + pub chestplate: Slot, + pub leggings: Slot, + pub boots: Slot, +} + +#[derive(Debug, Clone)] +pub struct PlayerInventory { + inventory: Inventory, + pub equipment_content: EquipmentContent, + pub main_hand: Slot, + pub off_hand: Slot, +} + +impl Deref for PlayerInventory { + type Target = Inventory; + + fn deref(&self) -> &Self::Target { + &self.inventory + } +} + +impl DerefMut for PlayerInventory { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inventory + } +} + +impl Default for PlayerInventory { + fn default() -> Self { + Self::new( + EquipmentContent::default(), + Slot::default(), + Slot::default(), + ) + } +} + +impl PlayerInventory { + pub fn new(equipment_content: EquipmentContent, main_hand: Slot, off_hand: Slot) -> Self { + Self { + inventory: InventoryBuilder::new(0) + .inventory_type(InventoryType::Chest(5)) + .build(), + equipment_content, + main_hand, + off_hand, + } + } + + pub fn set_equipment_slot( + &mut self, + equipment_slot: EquipmentSlot, + slot: Slot, + ) -> Result<(), InventoryError> { + let slot_id = match equipment_slot { + EquipmentSlot::Boots => { + self.equipment_content.boots = slot; + 8 + } + EquipmentSlot::Leggings => { + self.equipment_content.leggings = slot; + 7 + } + EquipmentSlot::Chestplate => { + self.equipment_content.chestplate = slot; + 6 + } + EquipmentSlot::Helmet => { + self.equipment_content.helmet = slot; + 5 + } + EquipmentSlot::OffHand => 45, + _ => return Err(InventoryError::InvalidEquipmentSlot), + }; + + self.inventory.set_slot(slot_id, slot); + Ok(()) + } +} 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 4a745b0a..a822faeb 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 @@ -1,8 +1,9 @@ // I have no clue why it is saving i32 and i16. There is no precision. The actual player position is saved in f32. +use crate::decode::{NetDecode, NetDecodeOpts, NetDecodeResult}; use crate::encode::{NetEncode, NetEncodeOpts, NetEncodeResult}; use std::fmt::Display; -use std::io::Write; +use std::io::{Read, Write}; use tokio::io::AsyncWrite; /// The definition of a "Position" in the Minecraft protocol. @@ -47,6 +48,18 @@ impl NetEncode for NetworkPosition { Ok(()) } } + +impl NetDecode for NetworkPosition { + fn decode(reader: &mut R, opts: &NetDecodeOpts) -> NetDecodeResult { + let value = u64::decode(reader, opts)?; + Ok(Self { + x: (value >> 38) as i32, + y: (value << 54 >> 52) as i16, + z: (value << 26 >> 38) as i32, + }) + } +} + impl NetworkPosition { pub fn as_u64(&self) -> u64 { ((self.x as u64 & 0x3FFFFFF) << 38) diff --git a/src/lib/net/crates/codec/src/net_types/var_int.rs b/src/lib/net/crates/codec/src/net_types/var_int.rs index 33231791..3c4e6202 100644 --- a/src/lib/net/crates/codec/src/net_types/var_int.rs +++ b/src/lib/net/crates/codec/src/net_types/var_int.rs @@ -6,9 +6,10 @@ use crate::net_types::NetTypesError; use bitcode::{Decode, Encode}; use deepsize::DeepSizeOf; use std::io::{Read, Write}; +use std::ops::{Deref, DerefMut}; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; -#[derive(Debug, Encode, Decode, Clone, DeepSizeOf)] +#[derive(Debug, Encode, Decode, Clone, Copy, DeepSizeOf)] pub struct VarInt { /// The value of the VarInt. pub val: i32, @@ -16,6 +17,20 @@ pub struct VarInt { pub len: usize, } +impl Deref for VarInt { + type Target = i32; + + fn deref(&self) -> &Self::Target { + &self.val + } +} + +impl DerefMut for VarInt { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.val + } +} + mod adapters { use crate::net_types::var_int::VarInt; diff --git a/src/lib/net/src/errors.rs b/src/lib/net/src/errors.rs index d24d3684..750115d7 100644 --- a/src/lib/net/src/errors.rs +++ b/src/lib/net/src/errors.rs @@ -43,6 +43,9 @@ pub enum NetError { #[error("{0}")] Chunk(#[from] ChunkError), + + #[error("NetError: {0}")] + Other(String), } #[derive(Debug, Error)] diff --git a/src/lib/net/src/packets/incoming/click_container.rs b/src/lib/net/src/packets/incoming/click_container.rs new file mode 100644 index 00000000..57b2c6e2 --- /dev/null +++ b/src/lib/net/src/packets/incoming/click_container.rs @@ -0,0 +1,188 @@ +use crate::packets::outgoing::set_container_slot::NetworkSlot; +use crate::packets::IncomingPacket; +use crate::NetResult; +use ferrumc_events::infrastructure::Event; +use ferrumc_macros::{packet, Event, NetDecode}; +use ferrumc_net_codec::decode::{NetDecode, NetDecodeOpts, NetDecodeResult}; +use ferrumc_net_codec::net_types::length_prefixed_vec::LengthPrefixedVec; +use ferrumc_net_codec::net_types::var_int::VarInt; +use ferrumc_state::ServerState; +use std::io::Read; +use std::sync::Arc; +use tracing::debug; + +#[derive(Debug, Clone, Copy)] +pub enum InventoryClickActions { + // Mode 0 actions + LeftMouseClick, + RightMouseClick, + LeftClickOutsideInventory, + RightClickOutsideInventory, + + // Mode 1 actions + ShiftLeftMouseClick, + ShiftRightMouseClick, + + // Mode 2 actions + NumberKey(u8), // keys 1 through 9. + + // Mode 3 actions + OffhandSwap, + + // Mode 4 actions + MiddleClick, + DropKey, + ControlDropKey, + + // Mode 5 actions + StartMouseDrag { button: u8 }, // Starting drag with a specific button + AddSlotToMouseDrag { button: u8 }, // Adding slot with a specific button + EndMouseDrag { button: u8 }, // Ending drag with a specific button + + // Mode 6 actions + DoubleClick, + ReversePickup, +} + +impl InventoryClickActions { + pub fn get_action(mode: i32, button: u8, slot: i16) -> Option { + match mode { + 0 => match (button, slot) { + (0, -999) => Some(InventoryClickActions::LeftClickOutsideInventory), + (_, -999) => Some(InventoryClickActions::RightClickOutsideInventory), + (0, _) => Some(InventoryClickActions::LeftMouseClick), + _ => Some(InventoryClickActions::RightMouseClick), + }, + 1 => match button { + 0 => Some(InventoryClickActions::ShiftLeftMouseClick), + _ => Some(InventoryClickActions::ShiftRightMouseClick), + }, + 2 => match button { + 1..=9 => Some(InventoryClickActions::NumberKey(button)), + _ => None, + }, + 3 => match button { + 40 => Some(InventoryClickActions::OffhandSwap), + _ => None, + }, + 4 => match button { + 2 => Some(InventoryClickActions::MiddleClick), + 0 => Some(InventoryClickActions::DropKey), + 1 => Some(InventoryClickActions::ControlDropKey), + _ => None, + }, + 5 => match (slot, button) { + (-999, 0) => Some(InventoryClickActions::StartMouseDrag { button }), + (_, 0) => Some(InventoryClickActions::AddSlotToMouseDrag { button }), + (_, 1) => Some(InventoryClickActions::AddSlotToMouseDrag { button }), + (-999, _) => Some(InventoryClickActions::EndMouseDrag { button }), + _ => None, + }, + 6 => match button { + 0 => Some(InventoryClickActions::DoubleClick), + 1 => Some(InventoryClickActions::ReversePickup), + _ => None, + }, + _ => None, + } + } +} + +#[derive(NetDecode, Debug)] +pub struct ChangedSlots { + pub slot_number: i16, + pub slot: NetworkSlot, +} + +impl ChangedSlots { + pub fn new(slot_number: i16, slot: NetworkSlot) -> Self { + Self { slot_number, slot } + } +} + +#[derive(Debug)] +#[packet(packet_id = "container_click", state = "play")] +pub struct ClickContainerPacket { + pub window_id: u8, + pub state_id: VarInt, + pub slot: i16, + pub button: Option, + pub mode: VarInt, + pub changed_slots: LengthPrefixedVec, + pub carried_item: NetworkSlot, +} + +impl NetDecode for ClickContainerPacket { + fn decode(reader: &mut R, opts: &NetDecodeOpts) -> NetDecodeResult { + let window_id = u8::decode(reader, opts)?; + let state_id = VarInt::decode(reader, opts)?; + let slot = i16::decode(reader, opts)?; + let button = u8::decode(reader, opts)?; + let mode = VarInt::decode(reader, opts)?; + let changed_slots = LengthPrefixedVec::::decode(reader, opts)?; + let carried_item = NetworkSlot::decode(reader, opts)?; + + Ok(Self::new( + window_id, + state_id, + slot, + InventoryClickActions::get_action(*mode, button, slot), + mode, + changed_slots, + carried_item, + )) + } +} + +impl IncomingPacket for ClickContainerPacket { + async fn handle(self, conn_id: usize, state: Arc) -> NetResult<()> { + debug!("{:#?}", self); + + let event = InventoryClickEvent::new(conn_id, self); + InventoryClickEvent::trigger(event, state).await?; + Ok(()) + } +} + +impl ClickContainerPacket { + pub fn new( + window_id: u8, + state_id: VarInt, + slot: i16, + button: Option, + mode: VarInt, + changed_slots: LengthPrefixedVec, + carried_item: NetworkSlot, + ) -> Self { + Self { + window_id, + state_id, + slot, + button, + mode, + changed_slots, + carried_item, + } + } +} + +#[derive(Event, Debug)] +pub struct InventoryClickEvent { + pub conn_id: usize, + pub packet: ClickContainerPacket, + pub is_canceled: bool, +} + +impl InventoryClickEvent { + pub fn new(conn_id: usize, packet: ClickContainerPacket) -> Self { + Self { + conn_id, + packet, + is_canceled: false, + } + } + + pub fn set_canceled(&mut self, is_cancelled: bool) { + self.is_canceled = is_cancelled; + } +} diff --git a/src/lib/net/src/packets/incoming/close_container.rs b/src/lib/net/src/packets/incoming/close_container.rs new file mode 100644 index 00000000..1dae2986 --- /dev/null +++ b/src/lib/net/src/packets/incoming/close_container.rs @@ -0,0 +1,38 @@ +use crate::packets::IncomingPacket; +use crate::NetResult; +use ferrumc_events::infrastructure::Event; +use ferrumc_macros::{packet, Event, NetDecode}; +use ferrumc_state::ServerState; +use std::sync::Arc; + +#[derive(NetDecode, Debug)] +#[packet(packet_id = "container_close", state = "play")] +pub struct IncomingCloseContainerPacket { + pub window_id: u8, +} + +impl IncomingCloseContainerPacket { + pub fn new(window_id: u8) -> Self { + IncomingCloseContainerPacket { window_id } + } +} + +impl IncomingPacket for IncomingCloseContainerPacket { + async fn handle(self, conn_id: usize, state: Arc) -> NetResult<()> { + let event = InventoryCloseEvent::new(conn_id, self.window_id); + InventoryCloseEvent::trigger(event, state).await?; + Ok(()) + } +} + +#[derive(Event, Debug)] +pub struct InventoryCloseEvent { + pub conn_id: usize, + pub window_id: u8, +} + +impl InventoryCloseEvent { + pub fn new(conn_id: usize, window_id: u8) -> Self { + InventoryCloseEvent { conn_id, window_id } + } +} diff --git a/src/lib/net/src/packets/incoming/mod.rs b/src/lib/net/src/packets/incoming/mod.rs index 22de710b..baa3e35c 100644 --- a/src/lib/net/src/packets/incoming/mod.rs +++ b/src/lib/net/src/packets/incoming/mod.rs @@ -17,3 +17,9 @@ pub mod set_player_rotation; pub mod player_command; pub mod swing_arm; + +pub mod click_container; +pub mod close_container; +pub mod set_creative_mode_slot; +pub mod set_held_item; +pub mod use_item_on; diff --git a/src/lib/net/src/packets/incoming/set_creative_mode_slot.rs b/src/lib/net/src/packets/incoming/set_creative_mode_slot.rs new file mode 100644 index 00000000..e8aea661 --- /dev/null +++ b/src/lib/net/src/packets/incoming/set_creative_mode_slot.rs @@ -0,0 +1,42 @@ +use std::sync::Arc; + +use ferrumc_events::infrastructure::Event; +use ferrumc_macros::{packet, Event, NetDecode}; +use ferrumc_state::ServerState; + +use crate::{ + packets::{outgoing::set_container_slot::NetworkSlot, IncomingPacket}, + NetResult, +}; + +#[derive(NetDecode, Debug)] +#[packet(packet_id = "set_creative_mode_slot", state = "play")] +pub struct SetCreativeModeSlotPacket { + pub slot: i16, + pub clicked_item: NetworkSlot, +} + +impl IncomingPacket for SetCreativeModeSlotPacket { + async fn handle(self, conn_id: usize, state: Arc) -> NetResult<()> { + let event = SetCreativeModeSlotEvent::new(conn_id, self.slot, self.clicked_item); + SetCreativeModeSlotEvent::trigger(event, state).await?; + Ok(()) + } +} + +#[derive(Event, Debug)] +pub struct SetCreativeModeSlotEvent { + pub conn_id: usize, + pub slot: i16, + pub clicked_item: NetworkSlot, +} + +impl SetCreativeModeSlotEvent { + pub fn new(conn_id: usize, slot: i16, clicked_item: NetworkSlot) -> Self { + Self { + conn_id, + slot, + clicked_item, + } + } +} diff --git a/src/lib/net/src/packets/incoming/set_held_item.rs b/src/lib/net/src/packets/incoming/set_held_item.rs new file mode 100644 index 00000000..24fa7b89 --- /dev/null +++ b/src/lib/net/src/packets/incoming/set_held_item.rs @@ -0,0 +1,34 @@ +use std::sync::Arc; + +use ferrumc_events::infrastructure::Event; +use ferrumc_macros::{packet, Event, NetDecode}; +use ferrumc_state::ServerState; + +use crate::{packets::IncomingPacket, NetResult}; + +#[derive(NetDecode, Debug)] +#[packet(packet_id = "set_carried_item", state = "play")] +pub struct IncomingSetHeldItemPacket { + pub slot: u16, +} + +impl IncomingPacket for IncomingSetHeldItemPacket { + async fn handle(self, conn_id: usize, state: Arc) -> NetResult<()> { + let event = ChangeSlotEvent::new(conn_id, self.slot); + ChangeSlotEvent::trigger(event, state).await?; + + Ok(()) + } +} + +#[derive(Event, Debug)] +pub struct ChangeSlotEvent { + pub conn_id: usize, + pub slot: u16, +} + +impl ChangeSlotEvent { + pub fn new(conn_id: usize, slot: u16) -> Self { + Self { conn_id, slot } + } +} diff --git a/src/lib/net/src/packets/incoming/use_item_on.rs b/src/lib/net/src/packets/incoming/use_item_on.rs new file mode 100644 index 00000000..edaa130a --- /dev/null +++ b/src/lib/net/src/packets/incoming/use_item_on.rs @@ -0,0 +1,41 @@ +use crate::packets::IncomingPacket; +use crate::NetResult; +use ferrumc_events::infrastructure::Event; +use ferrumc_macros::{packet, Event, NetDecode}; +use ferrumc_net_codec::net_types::network_position::NetworkPosition; +use ferrumc_net_codec::net_types::var_int::VarInt; +use ferrumc_state::ServerState; +use std::sync::Arc; + +#[derive(NetDecode, Debug)] +#[packet(packet_id = "use_item_on", state = "play")] +pub struct UseItemOnPacket { + pub hand: VarInt, + pub location: NetworkPosition, + pub face: VarInt, + pub cursor_pos_x: f32, + pub cursor_pos_y: f32, + pub cursor_pos_z: f32, + pub inside_block: bool, + pub sequence: VarInt, +} + +impl IncomingPacket for UseItemOnPacket { + async fn handle(self, conn_id: usize, state: Arc) -> NetResult<()> { + let event = UseItemOnEvent::new(self, conn_id); + UseItemOnEvent::trigger(event, state).await?; + Ok(()) + } +} + +#[derive(Event, Debug)] +pub struct UseItemOnEvent { + pub conn_id: usize, + pub packet: UseItemOnPacket, +} + +impl UseItemOnEvent { + pub fn new(packet: UseItemOnPacket, conn_id: usize) -> Self { + Self { conn_id, packet } + } +} diff --git a/src/lib/net/src/packets/outgoing/close_container.rs b/src/lib/net/src/packets/outgoing/close_container.rs new file mode 100644 index 00000000..d3f8cff9 --- /dev/null +++ b/src/lib/net/src/packets/outgoing/close_container.rs @@ -0,0 +1,14 @@ +use ferrumc_macros::{packet, NetEncode}; +use std::io::Write; + +#[derive(NetEncode)] +#[packet(packet_id = "container_close", state_id = "play")] +pub struct CloseContainerPacket { + pub window_id: u8, +} + +impl CloseContainerPacket { + pub fn new(window_id: u8) -> Self { + Self { window_id } + } +} diff --git a/src/lib/net/src/packets/outgoing/mod.rs b/src/lib/net/src/packets/outgoing/mod.rs index ce630555..9460422b 100644 --- a/src/lib/net/src/packets/outgoing/mod.rs +++ b/src/lib/net/src/packets/outgoing/mod.rs @@ -2,6 +2,7 @@ pub mod chunk_and_light_data; pub mod chunk_batch_finish; pub mod chunk_batch_start; pub mod client_bound_known_packs; +pub mod close_container; pub mod disconnect; pub mod finish_configuration; pub mod game_event; @@ -9,10 +10,15 @@ pub mod keep_alive; pub mod login_disconnect; pub mod login_play; pub mod login_success; +pub mod open_screen; pub mod ping_response; pub mod registry_data; pub mod set_center_chunk; +pub mod set_container_content; +pub mod set_container_property; +pub mod set_container_slot; pub mod set_default_spawn_position; +pub mod set_equipment; pub mod set_render_distance; pub mod status_response; pub mod synchronize_player_position; diff --git a/src/lib/net/src/packets/outgoing/open_screen.rs b/src/lib/net/src/packets/outgoing/open_screen.rs new file mode 100644 index 00000000..a59204a6 --- /dev/null +++ b/src/lib/net/src/packets/outgoing/open_screen.rs @@ -0,0 +1,45 @@ +use ferrumc_macros::{packet, Event, NetEncode}; +use ferrumc_net_codec::net_types::var_int::VarInt; +use ferrumc_text::{TextComponent, TextComponentBuilder}; +use std::io::Write; + +#[derive(NetEncode)] +#[packet(packet_id = "open_screen", state_id = "play")] +pub struct OpenScreenPacket { + pub window_id: VarInt, + pub window_type: VarInt, + pub window_title: TextComponent, +} + +impl OpenScreenPacket { + pub fn new(window_id: u8, window_type: i32, window_title: TextComponent) -> Self { + Self { + window_id: VarInt::new(i32::from(window_id)), + window_type: VarInt::new(window_type), + window_title, + } + } + + pub fn with_empty_title(window_id: u8, window_type: i32) -> Self { + Self::new( + window_id, + window_type, + TextComponentBuilder::new("").build(), + ) + } +} + +#[derive(Event, Debug)] +pub struct OpenInventoryEvent { + pub conn_id: usize, + pub inventory_id: u8, +} + +impl OpenInventoryEvent { + pub fn new(conn_id: usize, inventory_id: u8) -> Self { + Self { + conn_id, + inventory_id, + } + } +} diff --git a/src/lib/net/src/packets/outgoing/player_info_update.rs b/src/lib/net/src/packets/outgoing/player_info_update.rs index 65f6e9db..80d2d88e 100644 --- a/src/lib/net/src/packets/outgoing/player_info_update.rs +++ b/src/lib/net/src/packets/outgoing/player_info_update.rs @@ -91,6 +91,7 @@ impl PlayerWithActions { for action in &self.actions { mask |= match action { PlayerAction::AddPlayer { .. } => 0x01, + PlayerAction::UpdateListed { .. } => 0x08, } } mask @@ -113,6 +114,9 @@ pub enum PlayerAction { name: String, properties: LengthPrefixedVec, }, + UpdateListed { + listed: bool, + }, } #[derive(NetEncode, Debug)] diff --git a/src/lib/net/src/packets/outgoing/set_container_content.rs b/src/lib/net/src/packets/outgoing/set_container_content.rs new file mode 100644 index 00000000..85c9c2ea --- /dev/null +++ b/src/lib/net/src/packets/outgoing/set_container_content.rs @@ -0,0 +1,28 @@ +use super::set_container_slot::NetworkSlot; +use ferrumc_macros::{packet, NetEncode}; +use ferrumc_net_codec::net_types::{length_prefixed_vec::LengthPrefixedVec, var_int::VarInt}; +use std::io::Write; + +#[derive(NetEncode, Debug)] +#[packet(packet_id = "container_set_content", state_id = "play")] +pub struct SetContainerContentPacket { + pub window_id: u8, + pub state_id: VarInt, + pub slot_data: LengthPrefixedVec, + pub carried_item: NetworkSlot, +} + +impl SetContainerContentPacket { + pub fn new( + window_id: u8, + slot_data: LengthPrefixedVec, + carried_item: NetworkSlot, + ) -> Self { + Self { + window_id, + state_id: VarInt::new(0), + slot_data, + carried_item, + } + } +} diff --git a/src/lib/net/src/packets/outgoing/set_container_property.rs b/src/lib/net/src/packets/outgoing/set_container_property.rs new file mode 100644 index 00000000..51e701d6 --- /dev/null +++ b/src/lib/net/src/packets/outgoing/set_container_property.rs @@ -0,0 +1,171 @@ +use ferrumc_macros::{packet, NetEncode}; +use ferrumc_net_codec::encode::{NetEncode, NetEncodeOpts, NetEncodeResult}; +use std::io::Write; +use tokio::io::AsyncWrite; + +#[derive(Debug, Copy, Clone)] +pub enum ContainerProperty { + // Furnace + FurnaceFireIcon(u16), + FurnaceMaxFuelTime(u16), + FurnaceProgressArrow(u16), + FurnaceMaxProgress(u16), + + // Enchantment + EnchantmentLevelTop(u16), + EnchantmentLevelMiddle(u16), + EnchantmentLevelBottom(u16), + EnchantmentSeed(u16), + EnchantmentIdTop(u16), + EnchantmentIdMiddle(u16), + EnchantmentIdBottom(u16), + EnchantmentLevelIdTop(u16), + EnchantmentLevelIdMiddle(u16), + EnchantmentLevelIdBottom(u16), + + // Beacon + BeaconPowerLevel(u16), + BeaconFirstPotionEffect(u16), + BeaconSecondPotionEffect(u16), + + // Anvil + AnvilRepairCost(u16), + + // Brewing Stand + BrewingStandBrewTime(u16), + BrewingStandFuelTime(u16), + + // Stonecutter + StonecutterSelectedRecipe(u16), + + // Loom + LoomSelectedPattern(u16), + + // Lectern + LecternPageNumber(u16), +} + +impl ContainerProperty { + pub fn get_property_id(&self) -> u16 { + match self { + // Furnace + ContainerProperty::FurnaceFireIcon(_) => 0, + ContainerProperty::FurnaceMaxFuelTime(_) => 1, + ContainerProperty::FurnaceProgressArrow(_) => 2, + ContainerProperty::FurnaceMaxProgress(_) => 3, + + // Enchantment Table + ContainerProperty::EnchantmentLevelTop(_) => 0, + ContainerProperty::EnchantmentLevelMiddle(_) => 1, + ContainerProperty::EnchantmentLevelBottom(_) => 2, + ContainerProperty::EnchantmentSeed(_) => 3, + ContainerProperty::EnchantmentIdTop(_) => 4, + ContainerProperty::EnchantmentIdMiddle(_) => 5, + ContainerProperty::EnchantmentIdBottom(_) => 6, + ContainerProperty::EnchantmentLevelIdTop(_) => 7, + ContainerProperty::EnchantmentLevelIdMiddle(_) => 8, + ContainerProperty::EnchantmentLevelIdBottom(_) => 9, + + // Beacon + ContainerProperty::BeaconPowerLevel(_) => 0, + ContainerProperty::BeaconFirstPotionEffect(_) => 1, + ContainerProperty::BeaconSecondPotionEffect(_) => 2, + + // Anvil + ContainerProperty::AnvilRepairCost(_) => 0, + + // Brewing Stand + ContainerProperty::BrewingStandBrewTime(_) => 0, + ContainerProperty::BrewingStandFuelTime(_) => 1, + + // Stonecutter + ContainerProperty::StonecutterSelectedRecipe(_) => 0, + + // Loom + ContainerProperty::LoomSelectedPattern(_) => 0, + + // Lectern + ContainerProperty::LecternPageNumber(_) => 0, + } + } +} + +impl NetEncode for ContainerProperty { + fn encode(&self, writer: &mut W, opts: &NetEncodeOpts) -> NetEncodeResult<()> { + self.get_property_id().encode(writer, opts)?; + match self { + Self::FurnaceFireIcon(value) + | Self::FurnaceMaxFuelTime(value) + | Self::FurnaceProgressArrow(value) + | Self::FurnaceMaxProgress(value) + | Self::EnchantmentLevelTop(value) + | Self::EnchantmentLevelMiddle(value) + | Self::EnchantmentLevelBottom(value) + | Self::EnchantmentSeed(value) + | Self::EnchantmentIdTop(value) + | Self::EnchantmentIdMiddle(value) + | Self::EnchantmentIdBottom(value) + | Self::EnchantmentLevelIdTop(value) + | Self::EnchantmentLevelIdMiddle(value) + | Self::EnchantmentLevelIdBottom(value) + | Self::BeaconPowerLevel(value) + | Self::BeaconFirstPotionEffect(value) + | Self::BeaconSecondPotionEffect(value) + | Self::AnvilRepairCost(value) + | Self::BrewingStandBrewTime(value) + | Self::BrewingStandFuelTime(value) + | Self::StonecutterSelectedRecipe(value) + | Self::LoomSelectedPattern(value) + | Self::LecternPageNumber(value) => value.encode(writer, opts), + } + } + + async fn encode_async( + &self, + writer: &mut W, + opts: &NetEncodeOpts, + ) -> NetEncodeResult<()> { + self.get_property_id().encode_async(writer, opts).await?; + match self { + Self::FurnaceFireIcon(value) + | Self::FurnaceMaxFuelTime(value) + | Self::FurnaceProgressArrow(value) + | Self::FurnaceMaxProgress(value) + | Self::EnchantmentLevelTop(value) + | Self::EnchantmentLevelMiddle(value) + | Self::EnchantmentLevelBottom(value) + | Self::EnchantmentSeed(value) + | Self::EnchantmentIdTop(value) + | Self::EnchantmentIdMiddle(value) + | Self::EnchantmentIdBottom(value) + | Self::EnchantmentLevelIdTop(value) + | Self::EnchantmentLevelIdMiddle(value) + | Self::EnchantmentLevelIdBottom(value) + | Self::BeaconPowerLevel(value) + | Self::BeaconFirstPotionEffect(value) + | Self::BeaconSecondPotionEffect(value) + | Self::AnvilRepairCost(value) + | Self::BrewingStandBrewTime(value) + | Self::BrewingStandFuelTime(value) + | Self::StonecutterSelectedRecipe(value) + | Self::LoomSelectedPattern(value) + | Self::LecternPageNumber(value) => value.encode_async(writer, opts).await, + } + } +} + +#[derive(NetEncode)] +#[packet(packet_id = "container_set_data", state_id = "play")] +pub struct SetContainerPropertyPacket { + pub window_id: u8, + pub property: ContainerProperty, +} + +impl SetContainerPropertyPacket { + pub fn new(window_id: u8, property: ContainerProperty) -> Self { + Self { + window_id, + property, + } + } +} diff --git a/src/lib/net/src/packets/outgoing/set_container_slot.rs b/src/lib/net/src/packets/outgoing/set_container_slot.rs new file mode 100644 index 00000000..f43879de --- /dev/null +++ b/src/lib/net/src/packets/outgoing/set_container_slot.rs @@ -0,0 +1,63 @@ +use ferrumc_macros::{packet, NetDecode, NetEncode}; +use ferrumc_net_codec::net_types::var_int::VarInt; +use std::io::Write; + +#[derive(NetDecode, NetEncode, Debug, Clone, Copy)] +pub struct NetworkSlot { + pub item_count: VarInt, + #[net(optional_trigger = *item_count > 0)] + pub item_id: Option, + #[net(optional_trigger = *item_count > 0)] + pub num_of_components_to_add: Option, + #[net(optional_trigger = *item_count > 0)] + pub num_of_components_to_remove: Option, +} + +impl NetworkSlot { + pub fn new(item_count: i32, item_id: i32) -> Self { + let components = if item_count == 0 { + None + } else { + Some(VarInt::new(0)) + }; + Self { + item_count: VarInt::new(item_count), + item_id: if item_count == 0 { + None + } else { + Some(VarInt::new(item_id)) + }, + num_of_components_to_add: components, + num_of_components_to_remove: components, + } + } + + pub fn empty() -> Self { + Self::new(0, 0) + } + + pub fn item_id(&mut self, item_id: VarInt) -> &mut Self { + self.item_id = Some(item_id); + self + } +} + +#[derive(NetEncode)] +#[packet(packet_id = "container_set_slot", state_id = "play")] +pub struct SetContainerSlotPacket { + pub window_id: VarInt, + pub state_id: VarInt, + pub slot: i16, + pub slot_data: NetworkSlot, +} + +impl SetContainerSlotPacket { + pub fn new(window_id: VarInt, slot: i16, slot_data: NetworkSlot) -> Self { + Self { + window_id, + state_id: VarInt::new(0), + slot, + slot_data, + } + } +} diff --git a/src/lib/net/src/packets/outgoing/set_equipment.rs b/src/lib/net/src/packets/outgoing/set_equipment.rs new file mode 100644 index 00000000..34ef7740 --- /dev/null +++ b/src/lib/net/src/packets/outgoing/set_equipment.rs @@ -0,0 +1,75 @@ +use ferrumc_macros::{packet, NetEncode}; +use ferrumc_net_codec::{ + encode::{NetEncode, NetEncodeOpts, NetEncodeResult}, + net_types::var_int::VarInt, +}; +use std::io::Write; + +use super::set_container_slot::NetworkSlot; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EquipmentSlot { + MainHand, + OffHand, + Boots, + Leggings, + Chestplate, + Helmet, + Body, +} + +impl NetEncode for EquipmentSlot { + fn encode(&self, writer: &mut W, opts: &NetEncodeOpts) -> NetEncodeResult<()> { + self.get_index().encode(writer, opts) + } + + async fn encode_async( + &self, + writer: &mut W, + opts: &NetEncodeOpts, + ) -> NetEncodeResult<()> { + self.get_index().encode_async(writer, opts).await + } +} + +impl EquipmentSlot { + pub fn get_index(&self) -> u8 { + match self { + Self::MainHand => 0, + Self::OffHand => 1, + Self::Boots => 2, + Self::Leggings => 3, + Self::Chestplate => 4, + Self::Helmet => 5, + Self::Body => 6, + } + } +} + +#[derive(NetEncode)] +pub struct Equipment { + pub equipment: EquipmentSlot, + pub slot: NetworkSlot, +} + +impl Equipment { + pub fn new(equipment: EquipmentSlot, slot: NetworkSlot) -> Self { + Self { equipment, slot } + } +} + +#[derive(NetEncode)] +#[packet(packet_id = "set_equipment", state_id = "play")] +pub struct SetEquipmentPacket { + pub conn_id: VarInt, + pub equipment: Vec, +} + +impl SetEquipmentPacket { + pub fn new(conn_id: usize, equipment: Vec) -> Self { + Self { + conn_id: VarInt::new(conn_id as i32), + equipment, + } + } +} diff --git a/src/lib/net/src/slot.rs b/src/lib/net/src/slot.rs new file mode 100644 index 00000000..8f522219 --- /dev/null +++ b/src/lib/net/src/slot.rs @@ -0,0 +1,56 @@ +use std::io::Read; + +use ferrumc_macros::{NetDecode, NetEncode}; +use ferrumc_net_codec::{ + decode::{NetDecode, NetDecodeOpts, NetDecodeResult}, + net_types::{length_prefixed_vec::LengthPrefixedVec, var_int::VarInt}, +}; + +#[derive(NetEncode, Debug, Clone, Copy)] +pub enum SlotComponent { + MaxStackSize { max_stack_size: VarInt }, +} + +#[derive(Debug, Clone, Copy)] +pub struct NetworkSlot { + pub item_count: VarInt, + pub item_id: Option, + pub components_to_add: Option>, + pub components_to_remove: Option>, +} + +impl NetworkSlot { + pub fn new(item_count: i32, item_id: i32) -> Self { + Self::with_components(item_count, item_id, vec![]) + } + + pub fn with_components(item_count: i32, item_id: i32, components: Vec) -> Self { + Self { + item_count: VarInt::new(item_count), + item_id: if item_count == 0 { + None + } else { + Some(VarInt::new(item_id)) + }, + components_to_add: if item_count == 0 { + None + } else { + Some(LengthPrefixedVec::new(components)) + }, + components_to_remove: if item_count == 0 { + None + } else { + Some(LengthPrefixedVec::default()) + }, + } + } + + pub fn empty() -> Self { + Self::new(0, 0) + } + + pub fn item_id(&mut self, item_id: VarInt) -> &mut Self { + self.item_id = Some(item_id); + self + } +}