diff --git a/assets/example_pack.zip b/assets/example_pack.zip new file mode 100644 index 0000000..30224ee Binary files /dev/null and b/assets/example_pack.zip differ diff --git a/examples/resource_pack.rs b/examples/resource_pack.rs new file mode 100644 index 0000000..605dc2b --- /dev/null +++ b/examples/resource_pack.rs @@ -0,0 +1,223 @@ +use std::net::SocketAddr; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use log::LevelFilter; +use valence::async_trait; +use valence::block::{BlockPos, BlockState}; +use valence::chunk::UnloadedChunk; +use valence::client::{ + handle_event_default, Client, ClientEvent, GameMode, InteractWithEntityKind, ResourcePackStatus, +}; +use valence::config::{Config, ServerListPing}; +use valence::dimension::DimensionId; +use valence::entity::{EntityId, EntityKind, TrackedData}; +use valence::player_list::PlayerListId; +use valence::server::{Server, SharedServer, ShutdownResult}; +use valence::text::{Color, TextFormat}; + +pub fn main() -> ShutdownResult { + env_logger::Builder::new() + .filter_module("valence", LevelFilter::Trace) + .parse_default_env() + .init(); + + valence::start_server( + Game { + player_count: AtomicUsize::new(0), + }, + ServerState { + player_list: None, + sheep_id: None, + }, + ) +} + +struct Game { + player_count: AtomicUsize, +} + +struct ServerState { + player_list: Option, + sheep_id: Option, +} + +#[derive(Default)] +struct ClientState { + entity_id: EntityId, +} + +const MAX_PLAYERS: usize = 10; + +const SPAWN_POS: BlockPos = BlockPos::new(0, 100, 0); + +#[async_trait] +impl Config for Game { + type ServerState = ServerState; + type ClientState = ClientState; + type EntityState = (); + type WorldState = (); + type ChunkState = (); + type PlayerListState = (); + + fn max_connections(&self) -> usize { + // We want status pings to be successful even if the server is full. + MAX_PLAYERS + 64 + } + + async fn server_list_ping( + &self, + _server: &SharedServer, + _remote_addr: SocketAddr, + _protocol_version: i32, + ) -> ServerListPing { + ServerListPing::Respond { + online_players: self.player_count.load(Ordering::SeqCst) as i32, + max_players: MAX_PLAYERS as i32, + player_sample: Default::default(), + description: "Hello Valence!".color(Color::AQUA), + favicon_png: Some(include_bytes!("../assets/logo-64x64.png").as_slice().into()), + } + } + + fn init(&self, server: &mut Server) { + let (world_id, world) = server.worlds.insert(DimensionId::default(), ()); + server.state.player_list = Some(server.player_lists.insert(()).0); + + let size = 5; + for z in -size..size { + for x in -size..size { + world.chunks.insert([x, z], UnloadedChunk::default(), ()); + } + } + + let (sheep_id, sheep) = server.entities.insert(EntityKind::Sheep, ()); + server.state.sheep_id = Some(sheep_id); + sheep.set_world(world_id); + sheep.set_position([ + SPAWN_POS.x as f64 + 0.5, + SPAWN_POS.y as f64 + 4.0, + SPAWN_POS.z as f64 + 0.5, + ]); + + if let TrackedData::Sheep(sheep_data) = sheep.data_mut() { + sheep_data.set_custom_name("Hit me".color(Color::GREEN)); + } + + world.chunks.set_block_state(SPAWN_POS, BlockState::BEDROCK); + } + + fn update(&self, server: &mut Server) { + let (world_id, _) = server.worlds.iter_mut().next().expect("missing world"); + + server.clients.retain(|_, client| { + if client.created_this_tick() { + if self + .player_count + .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| { + (count < MAX_PLAYERS).then_some(count + 1) + }) + .is_err() + { + client.disconnect("The server is full!".color(Color::RED)); + return false; + } + + match server + .entities + .insert_with_uuid(EntityKind::Player, client.uuid(), ()) + { + Some((id, _)) => client.state.entity_id = id, + None => { + client.disconnect("Conflicting UUID"); + return false; + } + } + + client.spawn(world_id); + client.set_flat(true); + client.set_game_mode(GameMode::Creative); + client.teleport( + [ + SPAWN_POS.x as f64 + 0.5, + SPAWN_POS.y as f64 + 1.0, + SPAWN_POS.z as f64 + 0.5, + ], + 0.0, + 0.0, + ); + client.set_player_list(server.state.player_list.clone()); + + if let Some(id) = &server.state.player_list { + server.player_lists.get_mut(id).insert( + client.uuid(), + client.username(), + client.textures().cloned(), + client.game_mode(), + 0, + None, + ); + } + + set_example_pack(client); + } + + if client.is_disconnected() { + self.player_count.fetch_sub(1, Ordering::SeqCst); + if let Some(id) = &server.state.player_list { + server.player_lists.get_mut(id).remove(client.uuid()); + } + server.entities.remove(client.state.entity_id); + + return false; + } + + let player = server.entities.get_mut(client.state.entity_id).unwrap(); + + while let Some(event) = handle_event_default(client, player) { + match event { + ClientEvent::InteractWithEntity { kind, id, .. } => { + if kind == InteractWithEntityKind::Attack + && Some(id) == server.state.sheep_id + { + set_example_pack(client); + } + } + ClientEvent::ResourcePackStatusChanged(s) => { + let message = match s { + ResourcePackStatus::SuccessfullyLoaded => { + "The resource pack was successfully loaded!".color(Color::GREEN) + } + ResourcePackStatus::Declined => { + "You declined the resource pack :(".color(Color::RED) + } + ResourcePackStatus::FailedDownload => { + "The resource pack download failed.".color(Color::RED) + } + _ => continue, + }; + + client.send_message(message.italic()); + client.send_message( + "Hit the sheep above you to prompt the resource pack again." + .color(Color::GRAY) + .italic(), + ); + } + _ => (), + } + } + + true + }); + } +} + +fn set_example_pack(client: &mut Client) { + client.set_resource_pack( + "https://download843.mediafire.com/jbx81s8p7jig/dve0hxjaqecy7c6/example_pack.zip" + .to_owned(), // TODO: change to the GitHub URL of /assets/example_pack.zip + String::default(), + false, + None, + ); +} diff --git a/src/client.rs b/src/client.rs index bdfbbff..6cf6fde 100644 --- a/src/client.rs +++ b/src/client.rs @@ -30,14 +30,14 @@ use crate::protocol::packets::s2c::play::{ AcknowledgeBlockChange, BiomeRegistry, ChatTypeRegistry, ClearTitles, CustomSoundEffect, DimensionTypeRegistry, DimensionTypeRegistryEntry, DisconnectPlay, EntityAnimationS2c, EntityAttributesProperty, EntityEvent, GameEvent, GameStateChangeReason, KeepAliveS2c, - LoginPlay, PlayerPositionLookFlags, RegistryCodec, RemoveEntities, Respawn, S2cPlayPacket, - SetActionBarText, SetCenterChunk, SetDefaultSpawnPosition, SetEntityMetadata, + LoginPlay, PlayerPositionLookFlags, RegistryCodec, RemoveEntities, ResourcePackS2c, Respawn, + S2cPlayPacket, SetActionBarText, SetCenterChunk, SetDefaultSpawnPosition, SetEntityMetadata, SetEntityVelocity, SetExperience, SetHeadRotation, SetHealth, SetRenderDistance, SetSubtitleText, SetTitleText, SoundCategory, SynchronizePlayerPosition, SystemChatMessage, TeleportEntity, UnloadChunk, UpdateAttributes, UpdateEntityPosition, UpdateEntityPositionAndRotation, UpdateEntityRotation, }; -use crate::protocol::{BoundedInt, ByteAngle, NbtBridge, RawBytes, VarInt}; +use crate::protocol::{BoundedInt, BoundedString, ByteAngle, NbtBridge, RawBytes, VarInt}; use crate::server::{C2sPacketChannels, NewClientData, S2cPlayMessage, SharedServer}; use crate::slab_versioned::{Key, VersionedSlab}; use crate::text::Text; @@ -225,6 +225,7 @@ pub struct Client { /// Should be sent after login packet. msgs_to_send: Vec, bar_to_send: Option, + resource_pack_to_send: Option, attack_speed: f64, movement_speed: f64, bits: ClientBits, @@ -291,6 +292,7 @@ impl Client { dug_blocks: Vec::new(), msgs_to_send: Vec::new(), bar_to_send: None, + resource_pack_to_send: None, attack_speed: 4.0, movement_speed: 0.7, bits: ClientBits::new() @@ -641,6 +643,31 @@ impl Client { self.bits.hardcore() } + /// Requests that the client download and enable a resource pack. + /// + /// # Arguments + /// * `url` - The URL of the resource pack file. + /// * `hash` - The SHA-1 hash of the resource pack file. Any value other + /// than a 40-character hexadecimal string is ignored by the client. + /// * `forced` - Whether a client should be kicked from the server upon + /// declining the pack (this is enforced client-side) + /// * `prompt_message` - A message to be displayed with the resource pack + /// dialog. + pub fn set_resource_pack( + &mut self, + url: impl Into, + hash: impl Into, + forced: bool, + prompt_message: impl Into>, + ) { + self.resource_pack_to_send = Some(ResourcePackS2c { + url: url.into(), + hash: BoundedString(hash.into()), + forced, + prompt_message: prompt_message.into(), + }); + } + /// Gets the client's current settings. pub fn settings(&self) -> Option<&Settings> { self.settings.as_ref() @@ -893,7 +920,9 @@ impl Client { C2sPlayPacket::ChangeRecipeBookSettings(_) => {} C2sPlayPacket::SetSeenRecipe(_) => {} C2sPlayPacket::RenameItem(_) => {} - C2sPlayPacket::ResourcePackC2s(_) => {} + C2sPlayPacket::ResourcePackC2s(p) => self + .events + .push_back(ClientEvent::ResourcePackStatusChanged(p)), C2sPlayPacket::SeenAdvancements(_) => {} C2sPlayPacket::SelectTrade(_) => {} C2sPlayPacket::SetBeaconEffect(_) => {} @@ -1253,6 +1282,10 @@ impl Client { send_packet(&mut self.send, SetActionBarText { text: bar }); } + if let Some(p) = self.resource_pack_to_send.take() { + send_packet(&mut self.send, p); + } + let mut entities_to_unload = Vec::new(); // Update all entities that are visible and unload entities that are no diff --git a/src/client/event.rs b/src/client/event.rs index f4efa83..15421f8 100644 --- a/src/client/event.rs +++ b/src/client/event.rs @@ -8,7 +8,7 @@ use crate::config::Config; use crate::entity::types::Pose; use crate::entity::{Entity, EntityEvent, EntityId, TrackedData}; pub use crate::protocol::packets::c2s::play::{ - BlockFace, ChatMode, DisplayedSkinParts, Hand, MainHand, + BlockFace, ChatMode, DisplayedSkinParts, Hand, MainHand, ResourcePackC2s as ResourcePackStatus, }; pub use crate::protocol::packets::s2c::play::GameMode; use crate::protocol::VarInt; @@ -128,6 +128,7 @@ pub enum ClientEvent { /// Sequence number sequence: VarInt, }, + ResourcePackStatusChanged(ResourcePackStatus), } #[derive(Clone, PartialEq, Debug)] @@ -285,6 +286,7 @@ pub fn handle_event_default( ClientEvent::SteerBoat { .. } => {} ClientEvent::Digging { .. } => {} ClientEvent::InteractWithBlock { .. } => {} + ClientEvent::ResourcePackStatusChanged(_) => {} } entity.set_world(client.world()); diff --git a/src/protocol/packets/c2s.rs b/src/protocol/packets/c2s.rs index b8d2f66..8a004fe 100644 --- a/src/protocol/packets/c2s.rs +++ b/src/protocol/packets/c2s.rs @@ -528,6 +528,7 @@ pub mod play { } def_enum! { + #[derive(Copy, PartialEq, Eq)] ResourcePackC2s: VarInt { SuccessfullyLoaded = 0, Declined = 1, diff --git a/src/protocol/packets/s2c.rs b/src/protocol/packets/s2c.rs index fd4497d..8a26871 100644 --- a/src/protocol/packets/s2c.rs +++ b/src/protocol/packets/s2c.rs @@ -667,6 +667,15 @@ pub mod play { } } + def_struct! { + ResourcePackS2c { + url: String, + hash: BoundedString<0, 40>, + forced: bool, + prompt_message: Option, + } + } + def_struct! { Respawn { dimension_type_name: Ident, @@ -895,6 +904,7 @@ pub mod play { PlayerInfo = 55, SynchronizePlayerPosition = 57, RemoveEntities = 59, + ResourcePackS2c = 61, Respawn = 62, SetHeadRotation = 63, UpdateSectionBlocks = 64,