From f4a3c53eee7d29bade0c074f402c4a45aa98eca8 Mon Sep 17 00:00:00 2001 From: mat <27899617+mat-1@users.noreply.github.com> Date: Fri, 12 Dec 2025 01:29:49 -0600 Subject: Delete `StartError` and `JoinError` (#296) * delete StartError and JoinError * update changelog --- CHANGELOG.md | 2 +- azalea-client/src/client.rs | 47 ++----- azalea-client/src/lib.rs | 4 +- azalea-client/src/ping.rs | 24 ++-- azalea-client/src/plugins/join.rs | 23 ++-- azalea-protocol/src/address.rs | 157 ++++++++++++++++++++++++ azalea-protocol/src/lib.rs | 99 +-------------- azalea-protocol/src/resolve.rs | 10 +- azalea/README.md | 3 +- azalea/examples/echo.rs | 3 +- azalea/examples/nearest_entity.rs | 4 +- azalea/examples/steal.rs | 3 +- azalea/examples/testbot/main.rs | 3 +- azalea/examples/todo/craft_dig_straight_down.rs | 3 +- azalea/examples/todo/mine_a_chunk.rs | 3 +- azalea/examples/todo/pvp.rs | 3 +- azalea/src/lib.rs | 67 +++++----- azalea/src/prelude.rs | 1 + azalea/src/swarm/mod.rs | 117 ++++++++++-------- 19 files changed, 309 insertions(+), 267 deletions(-) create mode 100644 azalea-protocol/src/address.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index ce03fcf3..4616e8e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,9 +27,9 @@ is breaking anyways, semantic versioning is not followed. - Moved `azalea_client::inventory::Inventory` to `azalea_entity::inventory::Inventory`. - Add `Client::open_container_at_with_timeout_ticks`, and `Client::open_container_at` now times out after 5 seconds. - Rename `ResourceLocation` to `Identifier` to match Minecraft's new internal naming. -- Rename `azalea_protocol::resolver` to `resolve` and `ResolverError` to `ResolveError`. - Refactor `RegistryHolder` to pre-deserialize some registries. - The handler function is now automatically single-threaded, making `#[tokio::main(flavor = "current_thread")]` unnecessary. +- Improve APIs related to resolving server addresses. - Mojang's sessionserver is now requested using the SOCKS5 proxy given in `JoinOpts::proxy`. - Refactor `azalea-registry`. Notably, `Item` and `Block` are now named `ItemKind` and `BlockKind`. diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs index aa2da1d5..579f5d26 100644 --- a/azalea-client/src/client.rs +++ b/azalea-client/src/client.rs @@ -2,7 +2,6 @@ use std::{ collections::HashMap, fmt::Debug, mem, - net::SocketAddr, sync::Arc, thread, time::{Duration, Instant}, @@ -23,10 +22,10 @@ use azalea_entity::{ }; use azalea_physics::local_player::PhysicsState; use azalea_protocol::{ - ServerAddress, + address::{ResolvableAddr, ResolvedAddr}, connect::Proxy, packets::{Packet, game::ServerboundGamePacket}, - resolve, + resolve::ResolveError, }; use azalea_registry::{DataRegistryKeyRef, identifier::Identifier}; use azalea_world::{Instance, InstanceContainer, InstanceName, MinecraftEntityId, PartialInstance}; @@ -37,7 +36,6 @@ use bevy_ecs::{ schedule::{InternedScheduleLabel, LogLevel, ScheduleBuildSettings}, }; use parking_lot::{Mutex, RwLock}; -use thiserror::Error; use tokio::{ sync::{ mpsc::{self}, @@ -87,15 +85,6 @@ pub struct Client { pub ecs: Arc>, } -/// An error that happened while joining the server. -#[derive(Error, Debug)] -pub enum JoinError { - #[error(transparent)] - Resolver(#[from] resolve::ResolveError), - #[error("The given address could not be parsed into a ServerAddress")] - InvalidAddress, -} - pub struct StartClientOpts { pub ecs_lock: Arc>, pub account: Account, @@ -106,8 +95,7 @@ pub struct StartClientOpts { impl StartClientOpts { pub fn new( account: Account, - address: ServerAddress, - resolved_address: SocketAddr, + address: ResolvedAddr, event_sender: Option>, ) -> StartClientOpts { let mut app = App::new(); @@ -123,7 +111,6 @@ impl StartClientOpts { account, connect_opts: ConnectOpts { address, - resolved_address, server_proxy: None, sessionserver_proxy: None, }, @@ -197,35 +184,25 @@ impl Client { /// ``` pub async fn join( account: Account, - address: impl TryInto, - ) -> Result<(Self, mpsc::UnboundedReceiver), JoinError> { - let address: ServerAddress = address.try_into().map_err(|_| JoinError::InvalidAddress)?; - let resolved_address = resolve::resolve_address(&address).await?; + address: impl ResolvableAddr, + ) -> Result<(Self, mpsc::UnboundedReceiver), ResolveError> { + let address = address.resolve().await?; let (tx, rx) = mpsc::unbounded_channel(); - let client = Self::start_client(StartClientOpts::new( - account, - address, - resolved_address, - Some(tx), - )) - .await; + let client = Self::start_client(StartClientOpts::new(account, address, Some(tx))).await; Ok((client, rx)) } pub async fn join_with_proxy( account: Account, - address: impl TryInto, + address: impl ResolvableAddr, proxy: Proxy, - ) -> Result<(Self, mpsc::UnboundedReceiver), JoinError> { - let address: ServerAddress = address.try_into().map_err(|_| JoinError::InvalidAddress)?; - let resolved_address = resolve::resolve_address(&address).await?; + ) -> Result<(Self, mpsc::UnboundedReceiver), ResolveError> { + let address = address.resolve().await?; let (tx, rx) = mpsc::unbounded_channel(); - let client = Self::start_client( - StartClientOpts::new(account, address, resolved_address, Some(tx)).proxy(proxy), - ) - .await; + let client = + Self::start_client(StartClientOpts::new(account, address, Some(tx)).proxy(proxy)).await; Ok((client, rx)) } diff --git a/azalea-client/src/lib.rs b/azalea-client/src/lib.rs index c818d96a..6d216b2b 100644 --- a/azalea-client/src/lib.rs +++ b/azalea-client/src/lib.rs @@ -27,8 +27,8 @@ pub use azalea_protocol::common::client_information::ClientInformation; // version. pub use bevy_tasks; pub use client::{ - Client, InConfigState, InGameState, JoinError, JoinedClientBundle, LocalPlayerBundle, - StartClientOpts, start_ecs_runner, + Client, InConfigState, InGameState, JoinedClientBundle, LocalPlayerBundle, StartClientOpts, + start_ecs_runner, }; pub use events::Event; pub use movement::{StartSprintEvent, StartWalkEvent}; diff --git a/azalea-client/src/ping.rs b/azalea-client/src/ping.rs index 2c3ddfcf..08f909e4 100644 --- a/azalea-client/src/ping.rs +++ b/azalea-client/src/ping.rs @@ -3,7 +3,7 @@ use std::io; use azalea_protocol::{ - ServerAddress, + address::{ResolvableAddr, ServerAddr}, connect::{Connection, ConnectionError, Proxy}, packets::{ ClientIntention, PROTOCOL_VERSION, @@ -23,7 +23,7 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum PingError { #[error("{0}")] - Resolver(#[from] resolve::ResolveError), + Resolve(#[from] resolve::ResolveError), #[error("{0}")] Connection(#[from] ConnectionError), #[error("{0}")] @@ -48,23 +48,21 @@ pub enum PingError { /// } /// ``` pub async fn ping_server( - address: impl TryInto, + address: impl ResolvableAddr, ) -> Result { - let address: ServerAddress = address.try_into().map_err(|_| PingError::InvalidAddress)?; - let resolved_address = resolve::resolve_address(&address).await?; - let conn = Connection::new(&resolved_address).await?; - ping_server_with_connection(address, conn).await + let address = address.resolve().await?; + let conn = Connection::new(&address.socket).await?; + ping_server_with_connection(address.server, conn).await } /// Ping a Minecraft server through a SOCKS5 proxy. pub async fn ping_server_with_proxy( - address: impl TryInto, + address: impl ResolvableAddr, proxy: Proxy, ) -> Result { - let address: ServerAddress = address.try_into().map_err(|_| PingError::InvalidAddress)?; - let resolved_address = resolve::resolve_address(&address).await?; - let conn = Connection::new_with_proxy(&resolved_address, proxy).await?; - ping_server_with_connection(address, conn).await + let address = address.resolve().await?; + let conn = Connection::new_with_proxy(&address.socket, proxy).await?; + ping_server_with_connection(address.server, conn).await } /// Ping a Minecraft server after we've already created a [`Connection`]. @@ -72,7 +70,7 @@ pub async fn ping_server_with_proxy( /// The `Connection` must still be in the handshake state (which is the state /// it's in immediately after it's created). pub async fn ping_server_with_connection( - address: ServerAddress, + address: ServerAddr, mut conn: Connection, ) -> Result { // send the client intention packet and switch to the status state diff --git a/azalea-client/src/plugins/join.rs b/azalea-client/src/plugins/join.rs index 538369b0..f1e27165 100644 --- a/azalea-client/src/plugins/join.rs +++ b/azalea-client/src/plugins/join.rs @@ -1,8 +1,8 @@ -use std::{net::SocketAddr, sync::Arc}; +use std::sync::Arc; use azalea_entity::{LocalEntity, indexing::EntityUuidIndex}; use azalea_protocol::{ - ServerAddress, + address::ResolvedAddr, common::client_information::ClientInformation, connect::{Connection, ConnectionError, Proxy}, packets::{ @@ -59,16 +59,11 @@ pub struct StartJoinServerEvent { /// Options for how the connection to the server will be made. /// -/// These are persisted on reconnects. -/// -/// This is inserted as a component on clients to make auto-reconnecting work. +/// These are persisted on reconnects. This is inserted as a component on +/// clients to make auto-reconnecting work. #[derive(Debug, Clone, Component)] pub struct ConnectOpts { - /// The unresolved address that we're going to tell the server that we used - /// to connect. - pub address: ServerAddress, - /// The actual IP and port that we're going to make a connection to. - pub resolved_address: SocketAddr, + pub address: ResolvedAddr, /// The SOCKS5 proxy used for connecting to the Minecraft server. pub server_proxy: Option, /// The SOCKS5 proxy that will be used when authenticating our server join @@ -172,15 +167,15 @@ async fn create_conn_and_send_intention_packet( opts: ConnectOpts, ) -> Result { let mut conn = if let Some(proxy) = opts.server_proxy { - Connection::new_with_proxy(&opts.resolved_address, proxy).await? + Connection::new_with_proxy(&opts.address.socket, proxy).await? } else { - Connection::new(&opts.resolved_address).await? + Connection::new(&opts.address.socket).await? }; conn.write(ServerboundIntention { protocol_version: PROTOCOL_VERSION, - hostname: opts.address.host.clone(), - port: opts.address.port, + hostname: opts.address.server.host.clone(), + port: opts.address.server.port, intention: ClientIntention::Login, }) .await?; diff --git a/azalea-protocol/src/address.rs b/azalea-protocol/src/address.rs new file mode 100644 index 00000000..a3e487a1 --- /dev/null +++ b/azalea-protocol/src/address.rs @@ -0,0 +1,157 @@ +use std::{ + fmt::{self, Debug, Display}, + net::SocketAddr, + str::FromStr, +}; + +use hickory_resolver::ResolveError; + +use crate::resolve::resolve_address; + +/// Something that might be able to be parsed and looked up as a server address. +/// +/// This is typically used by Azalea as a generic argument, so the user can +/// choose to pass either a string or an already-resolved address. +pub trait ResolvableAddr: Debug + Clone { + fn server_addr(self) -> Result; + fn resolve(self) -> impl Future> + Send; +} +impl + Debug + Send + Clone> ResolvableAddr + for T +{ + fn server_addr(self) -> Result { + self.try_into() + .map_err(|_| "failed to parse address".into()) + } + + async fn resolve(self) -> Result { + ResolvedAddr::new(self.server_addr()?).await + } +} + +impl ResolvableAddr for &ResolvedAddr { + fn server_addr(self) -> Result { + Ok(self.server.clone()) + } + + async fn resolve(self) -> Result { + Ok(self.clone()) + } +} + +/// A host and port. It's possible that the port doesn't resolve to anything. +/// +/// # Examples +/// +/// `ServerAddr` implements TryFrom<&str>, so you can use it like this: +/// ``` +/// use azalea_protocol::address::ServerAddr; +/// +/// let addr = ServerAddr::try_from("localhost:25565").unwrap(); +/// assert_eq!(addr.host, "localhost"); +/// assert_eq!(addr.port, 25565); +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ServerAddr { + pub host: String, + pub port: u16, +} + +/// An empty error type that's used when we fail to convert a type to a +/// `ServerAddr`. +/// +/// You usually want to use the [`ResolvableAddr`] type instead, which works +/// with [`ResolveError`]s. +#[derive(Debug)] +pub struct ServerAddrParseError; + +impl TryFrom<&str> for ServerAddr { + type Error = ServerAddrParseError; + + /// Convert a Minecraft server address (`host:port`, the port is optional) + /// to a `ServerAddress` + fn try_from(string: &str) -> Result { + if string.is_empty() { + return Err(ServerAddrParseError); + } + let mut parts = string.split(':'); + let host = parts.next().ok_or(ServerAddrParseError)?.to_string(); + // default the port to 25565 + let port = parts.next().unwrap_or("25565"); + let port = u16::from_str(port).ok().ok_or(ServerAddrParseError)?; + Ok(ServerAddr { host, port }) + } +} +impl TryFrom for ServerAddr { + type Error = ServerAddrParseError; + + fn try_from(string: String) -> Result { + ServerAddr::try_from(string.as_str()) + } +} + +impl From for ServerAddr { + /// Convert an existing `SocketAddr` into a `ServerAddress`. + /// + /// This just converts the IP to a string and passes along the port. The + /// resolver will realize it's already an IP address and not do any DNS + /// requests. + fn from(addr: SocketAddr) -> Self { + ServerAddr { + host: addr.ip().to_string(), + port: addr.port(), + } + } +} + +impl Display for ServerAddr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}:{}", self.host, self.port) + } +} + +/// Serde deserialization for ServerAddress. +/// +/// This is useful if you're storing the server address in a config file. +impl<'de> serde::Deserialize<'de> for ServerAddr { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let string = String::deserialize(deserializer)?; + ServerAddr::try_from(string.as_str()) + .map_err(|_| serde::de::Error::custom("failed to parse address")) + } +} + +/// Serde serialization for ServerAddress. +/// +/// This uses the Display impl, so it will serialize to a string. +impl serde::Serialize for ServerAddr { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +/// An address that may be used to connect to a Minecraft server. +#[derive(Debug, Clone)] +pub struct ResolvedAddr { + /// The initial address that we passed when trying to connect. + /// + /// This is necessary because clients send this to the server when they + /// connect. + pub server: ServerAddr, + /// The IP and port that we will actually connect to. + pub socket: SocketAddr, +} + +impl ResolvedAddr { + pub async fn new(server: impl Into) -> Result { + let server = server.into(); + let socket = resolve_address(&server).await?; + Ok(Self { server, socket }) + } +} diff --git a/azalea-protocol/src/lib.rs b/azalea-protocol/src/lib.rs index b481afb8..803f8630 100644 --- a/azalea-protocol/src/lib.rs +++ b/azalea-protocol/src/lib.rs @@ -12,12 +12,7 @@ // this is necessary for thiserror backtraces #![feature(error_generic_member_access)] -use std::{ - fmt::{self, Display}, - net::SocketAddr, - str::FromStr, -}; - +pub mod address; pub mod common; #[cfg(feature = "connecting")] pub mod connect; @@ -27,98 +22,14 @@ pub mod resolve; pub mod write; #[doc(hidden)] -#[deprecated(note = "Renamed to resolve")] +#[deprecated(note = "renamed to `resolve`.")] pub mod resolver { pub use super::resolve::*; } -/// A host and port. It's possible that the port doesn't resolve to anything. -/// -/// # Examples -/// -/// `ServerAddress` implements TryFrom<&str>, so you can use it like this: -/// ``` -/// use azalea_protocol::ServerAddress; -/// -/// let addr = ServerAddress::try_from("localhost:25565").unwrap(); -/// assert_eq!(addr.host, "localhost"); -/// assert_eq!(addr.port, 25565); -/// ``` -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct ServerAddress { - pub host: String, - pub port: u16, -} - -impl TryFrom<&str> for ServerAddress { - type Error = String; - - /// Convert a Minecraft server address (host:port, the port is optional) to - /// a `ServerAddress` - fn try_from(string: &str) -> Result { - if string.is_empty() { - return Err("Empty string".to_string()); - } - let mut parts = string.split(':'); - let host = parts.next().ok_or("No host specified")?.to_string(); - // default the port to 25565 - let port = parts.next().unwrap_or("25565"); - let port = u16::from_str(port).map_err(|_| "Invalid port specified")?; - Ok(ServerAddress { host, port }) - } -} -impl TryFrom for ServerAddress { - type Error = String; - - fn try_from(string: String) -> Result { - ServerAddress::try_from(string.as_str()) - } -} - -impl From for ServerAddress { - /// Convert an existing `SocketAddr` into a `ServerAddress`. - /// - /// This just converts the IP to a string and passes along the port. The - /// resolver will realize it's already an IP address and not do any DNS - /// requests. - fn from(addr: SocketAddr) -> Self { - ServerAddress { - host: addr.ip().to_string(), - port: addr.port(), - } - } -} - -impl Display for ServerAddress { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}:{}", self.host, self.port) - } -} - -/// Serde deserialization for ServerAddress. -/// -/// This is useful if you're storing the server address in a config file. -impl<'de> serde::Deserialize<'de> for ServerAddress { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let string = String::deserialize(deserializer)?; - ServerAddress::try_from(string.as_str()).map_err(serde::de::Error::custom) - } -} - -/// Serde serialization for ServerAddress. -/// -/// This uses the Display impl, so it will serialize to a string. -impl serde::Serialize for ServerAddress { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} +#[doc(hidden)] +#[deprecated(note = "moved to `address::ServerAddr`.")] +pub type ServerAddress = address::ServerAddr; #[cfg(test)] mod tests { diff --git a/azalea-protocol/src/resolve.rs b/azalea-protocol/src/resolve.rs index 468371aa..92939c8c 100644 --- a/azalea-protocol/src/resolve.rs +++ b/azalea-protocol/src/resolve.rs @@ -8,7 +8,7 @@ use std::{ pub use hickory_resolver::ResolveError; use hickory_resolver::{Name, TokioResolver, name_server::TokioConnectionProvider}; -use crate::ServerAddress; +use crate::address::ServerAddr; #[doc(hidden)] #[deprecated(note = "Renamed to ResolveError")] @@ -23,7 +23,7 @@ static RESOLVER: LazyLock = LazyLock::new(|| { /// Resolve a Minecraft server address into an IP address and port. /// /// If it's already an IP address, it's returned as-is. -pub async fn resolve_address(mut address: &ServerAddress) -> Result { +pub async fn resolve_address(mut address: &ServerAddr) -> Result { let redirect = resolve_srv_redirect(address).await; if let Ok(redirect_target) = &redirect { address = redirect_target; @@ -32,7 +32,7 @@ pub async fn resolve_address(mut address: &ServerAddress) -> Result Result { +async fn resolve_ip_without_redirects(address: &ServerAddr) -> Result { if let Ok(ip) = address.host.parse::() { // no need to do a lookup return Ok(SocketAddr::new(ip, address.port)); @@ -51,7 +51,7 @@ async fn resolve_ip_without_redirects(address: &ServerAddress) -> Result Result { +async fn resolve_srv_redirect(address: &ServerAddr) -> Result { if address.port != 25565 { return Err(ResolveError::from("Port must be 25565 to do a SRV lookup")); } @@ -63,7 +63,7 @@ async fn resolve_srv_redirect(address: &ServerAddress) -> Result AppExit { let account = Account::offline("bot"); // or Account::microsoft("example@example.com").await.unwrap(); @@ -51,7 +51,6 @@ async fn main() { .set_handler(handle) .start(account, "localhost") .await - .unwrap(); } #[derive(Default, Clone, Component)] diff --git a/azalea/examples/echo.rs b/azalea/examples/echo.rs index a2219008..ed013a66 100644 --- a/azalea/examples/echo.rs +++ b/azalea/examples/echo.rs @@ -3,7 +3,7 @@ use azalea::prelude::*; #[tokio::main] -async fn main() { +async fn main() -> AppExit { let account = Account::offline("bot"); // or let account = Account::microsoft("email").await.unwrap(); @@ -11,7 +11,6 @@ async fn main() { .set_handler(handle) .start(account, "localhost") .await - .unwrap(); } #[derive(Default, Clone, Component)] diff --git a/azalea/examples/nearest_entity.rs b/azalea/examples/nearest_entity.rs index 19223589..a0385917 100644 --- a/azalea/examples/nearest_entity.rs +++ b/azalea/examples/nearest_entity.rs @@ -2,6 +2,7 @@ use azalea::{ ClientBuilder, bot::{Bot, LookAtEvent}, nearest_entity::EntityFinder, + prelude::*, }; use azalea_client::Account; use azalea_core::tick::GameTick; @@ -18,14 +19,13 @@ use bevy_ecs::{ }; #[tokio::main] -async fn main() { +async fn main() -> AppExit { let account = Account::offline("bot"); ClientBuilder::new() .add_plugins(LookAtStuffPlugin) .start(account, "localhost") .await - .unwrap(); } pub struct LookAtStuffPlugin; diff --git a/azalea/examples/steal.rs b/azalea/examples/steal.rs index c928545f..99254d51 100644 --- a/azalea/examples/steal.rs +++ b/azalea/examples/steal.rs @@ -8,7 +8,7 @@ use azalea_registry::builtin::{BlockKind, ItemKind}; use parking_lot::Mutex; #[tokio::main] -async fn main() { +async fn main() -> AppExit { let account = Account::offline("bot"); // or let bot = Account::microsoft("email").await.unwrap(); @@ -16,7 +16,6 @@ async fn main() { .set_handler(handle) .start(account, "localhost") .await - .unwrap(); } #[derive(Default, Clone, Component)] diff --git a/azalea/examples/testbot/main.rs b/azalea/examples/testbot/main.rs index 6adb782c..2c282f07 100644 --- a/azalea/examples/testbot/main.rs +++ b/azalea/examples/testbot/main.rs @@ -33,7 +33,7 @@ use commands::{CommandSource, register_commands}; use parking_lot::Mutex; #[tokio::main] -async fn main() { +async fn main() -> AppExit { let args = parse_args(); thread::spawn(deadlock_detection_thread); @@ -65,7 +65,6 @@ async fn main() { }) .start(join_address) .await - .unwrap(); } /// Runs a loop that checks for deadlocks every 10 seconds. diff --git a/azalea/examples/todo/craft_dig_straight_down.rs b/azalea/examples/todo/craft_dig_straight_down.rs index 0d9961d4..a76c1ffe 100644 --- a/azalea/examples/todo/craft_dig_straight_down.rs +++ b/azalea/examples/todo/craft_dig_straight_down.rs @@ -9,7 +9,7 @@ struct State { } #[tokio::main] -async fn main() { +async fn main() -> AppExit { let account = Account::offline("bot"); // or let bot = Account::microsoft("email").await; @@ -17,7 +17,6 @@ async fn main() { .set_handler(handle) .start(account, "localhost") .await - .unwrap(); } async fn handle(bot: Client, event: Event, state: State) -> anyhow::Result<()> { diff --git a/azalea/examples/todo/mine_a_chunk.rs b/azalea/examples/todo/mine_a_chunk.rs index 0c439f26..6f6606f4 100644 --- a/azalea/examples/todo/mine_a_chunk.rs +++ b/azalea/examples/todo/mine_a_chunk.rs @@ -1,7 +1,7 @@ use azalea::{prelude::*, swarm::prelude::*}; #[tokio::main] -async fn main() { +async fn main() -> AppExit { let mut accounts = Vec::new(); let mut states = Vec::new(); @@ -16,7 +16,6 @@ async fn main() { .set_swarm_handler(swarm_handle) .start("localhost") .await - .unwrap(); } #[derive(Default, Clone, Component)] diff --git a/azalea/examples/todo/pvp.rs b/azalea/examples/todo/pvp.rs index d85278e8..9121444c 100644 --- a/azalea/examples/todo/pvp.rs +++ b/azalea/examples/todo/pvp.rs @@ -6,7 +6,7 @@ use azalea::{ }; #[tokio::main] -async fn main() { +async fn main() -> AppExit { let mut accounts = Vec::new(); let mut states = Vec::new(); @@ -22,7 +22,6 @@ async fn main() { .join_delay(Duration::from_millis(1000)) .start("localhost") .await - .unwrap(); } #[derive(Component, Default, Clone)] diff --git a/azalea/src/lib.rs b/azalea/src/lib.rs index 8440e31f..45c0114d 100644 --- a/azalea/src/lib.rs +++ b/azalea/src/lib.rs @@ -37,6 +37,7 @@ pub use azalea_core::position::{BlockPos, Vec3}; pub use azalea_entity as entity; pub use azalea_physics as physics; pub use azalea_protocol as protocol; +use azalea_protocol::address::{ResolvableAddr, ServerAddr}; pub use azalea_registry as registry; #[doc(hidden)] #[deprecated(note = "renamed to `Identifier`.")] @@ -48,9 +49,8 @@ use bevy_app::AppExit; pub use bevy_ecs as ecs; use ecs::component::Component; use futures::{Future, future::BoxFuture}; -use protocol::{ServerAddress, connect::Proxy, resolve::ResolveError}; +use protocol::connect::Proxy; use swarm::SwarmBuilder; -use thiserror::Error; use crate::bot::DefaultBotPlugins; @@ -58,15 +58,6 @@ pub type BoxHandleFn = Box BoxFuture<'static, R> + Send>; pub type HandleFn = fn(Client, azalea_client::Event, S) -> Fut; -/// An error related to resolving the server address when starting a client. -#[derive(Error, Debug)] -pub enum StartError { - #[error("Invalid address")] - InvalidAddress, - #[error(transparent)] - ResolveAddress(#[from] ResolveError), -} - /// A builder for creating new [`Client`]s. This is the recommended way of /// making a bot. /// @@ -217,20 +208,17 @@ where /// /// If the client can't join, it'll keep retrying forever until it can. /// - /// The `address` argument can be a `&str`, [`ServerAddress`], or anything - /// that implements `TryInto`. + /// The `address` argument can be a `&str`, [`ServerAddr`], + /// [`ResolvedAddr`], or anything else that implements [`ResolvableAddr`]. /// /// # Errors /// /// This will error if the given address is invalid or couldn't be resolved /// to a Minecraft server. /// - /// [`ServerAddress`]: azalea_protocol::ServerAddress - pub async fn start( - mut self, - account: Account, - address: impl TryInto, - ) -> Result { + /// [`ServerAddr`]: azalea_protocol::address::ServerAddr + /// [`ResolvedAddr`]: azalea_protocol::address::ResolvedAddr + pub async fn start(mut self, account: Account, address: impl ResolvableAddr) -> AppExit { self.swarm.accounts = vec![(account, JoinOpts::default())]; if self.swarm.states.is_empty() { self.swarm.states = vec![S::default()]; @@ -243,14 +231,14 @@ where pub async fn start_with_opts( mut self, account: Account, - address: impl TryInto, + address: impl ResolvableAddr, opts: JoinOpts, - ) -> Result { + ) -> AppExit { self.swarm.accounts = vec![(account, opts.clone())]; if self.swarm.states.is_empty() { self.swarm.states = vec![S::default()]; } - self.swarm.start_with_default_opts(address, opts).await + self.swarm.start_with_opts(address, opts).await } } impl Default for ClientBuilder { @@ -287,10 +275,12 @@ pub struct JoinOpts { pub sessionserver_proxy: Option, /// Override the server address that this specific bot will send in the /// handshake packet. - pub custom_address: Option, - /// Override the socket address that this specific bot will use to connect + #[doc(alias = "custom_address")] + pub custom_server_addr: Option, + /// Override the IP and port that this specific bot will use to connect /// to the server. - pub custom_resolved_address: Option, + #[doc(alias = "custom_resolved_address")] + pub custom_socket_addr: Option, } impl JoinOpts { @@ -305,11 +295,11 @@ impl JoinOpts { if let Some(proxy) = other.sessionserver_proxy.clone() { self.sessionserver_proxy = Some(proxy); } - if let Some(custom_address) = other.custom_address.clone() { - self.custom_address = Some(custom_address); + if let Some(custom_server_addr) = other.custom_server_addr.clone() { + self.custom_server_addr = Some(custom_server_addr); } - if let Some(custom_resolved_address) = other.custom_resolved_address { - self.custom_resolved_address = Some(custom_resolved_address); + if let Some(custom_socket_addr) = other.custom_socket_addr { + self.custom_socket_addr = Some(custom_socket_addr); } } @@ -347,15 +337,26 @@ impl JoinOpts { /// Set the custom address that this bot will send in the handshake packet. #[must_use] - pub fn custom_address(mut self, custom_address: ServerAddress) -> Self { - self.custom_address = Some(custom_address); + pub fn custom_server_addr(mut self, server_addr: ServerAddr) -> Self { + self.custom_server_addr = Some(server_addr); self } /// Set the custom resolved address that this bot will use to connect to the /// server. #[must_use] - pub fn custom_resolved_address(mut self, custom_resolved_address: SocketAddr) -> Self { - self.custom_resolved_address = Some(custom_resolved_address); + pub fn custom_socket_addr(mut self, socket_addr: SocketAddr) -> Self { + self.custom_socket_addr = Some(socket_addr); self } + + #[doc(hidden)] + #[deprecated = "renamed to `custom_server_addr`."] + pub fn custom_address(self, server_addr: ServerAddr) -> Self { + self.custom_server_addr(server_addr) + } + #[doc(hidden)] + #[deprecated = "renamed to `custom_socket_addr`."] + pub fn custom_resolved_address(self, socket_addr: SocketAddr) -> Self { + self.custom_socket_addr(socket_addr) + } } diff --git a/azalea/src/prelude.rs b/azalea/src/prelude.rs index 969b1e86..335244cc 100644 --- a/azalea/src/prelude.rs +++ b/azalea/src/prelude.rs @@ -3,6 +3,7 @@ pub use azalea_client::{Account, Client, Event}; pub use azalea_core::tick::GameTick; +pub use bevy_app::AppExit; // this is necessary to make the macros that reference bevy_ecs work pub use crate::ecs as bevy_ecs; diff --git a/azalea/src/swarm/mod.rs b/azalea/src/swarm/mod.rs index feb3ff38..24e9c1b2 100644 --- a/azalea/src/swarm/mod.rs +++ b/azalea/src/swarm/mod.rs @@ -10,7 +10,6 @@ use std::{ collections::{HashMap, hash_map}, future::Future, mem, - net::SocketAddr, sync::{ Arc, atomic::{self, AtomicBool}, @@ -19,14 +18,14 @@ use std::{ }; use azalea_client::{ - Account, Client, DefaultPlugins, Event, JoinError, StartClientOpts, + Account, Client, DefaultPlugins, Event, StartClientOpts, auto_reconnect::{AutoReconnectDelay, DEFAULT_RECONNECT_DELAY}, chat::ChatPacket, join::ConnectOpts, start_ecs_runner, }; use azalea_entity::LocalEntity; -use azalea_protocol::{ServerAddress, resolve}; +use azalea_protocol::address::{ResolvableAddr, ResolvedAddr}; use azalea_world::InstanceContainer; use bevy_app::{App, AppExit, PluginGroup, PluginGroupBuilder, Plugins, SubApp}; use bevy_ecs::prelude::*; @@ -35,7 +34,7 @@ use parking_lot::{Mutex, RwLock}; use tokio::{sync::mpsc, task}; use tracing::{debug, error, warn}; -use crate::{BoxHandleFn, DefaultBotPlugins, HandleFn, JoinOpts, NoState, StartError}; +use crate::{BoxHandleFn, DefaultBotPlugins, HandleFn, JoinOpts, NoState}; /// A swarm is a way to conveniently control many bots at once, while also /// being able to control bots at an individual level when desired. @@ -51,8 +50,7 @@ pub struct Swarm { pub ecs_lock: Arc>, // the address is public and mutable so plugins can change it - pub resolved_address: Arc>, - pub address: Arc>, + pub address: Arc>, pub instance_container: Arc>, @@ -405,28 +403,31 @@ where /// Build this `SwarmBuilder` into an actual [`Swarm`] and join the given /// server. /// - /// The `address` argument can be a `&str`, [`ServerAddress`], or anything - /// that implements `TryInto`. + /// The `address` argument can be a `&str`, [`ServerAddr`], + /// [`ResolvedAddr`], or anything else that implements [`ResolvableAddr`]. /// - /// [`ServerAddress`]: azalea_protocol::ServerAddress - pub async fn start(self, address: impl TryInto) -> Result { - // convert the TryInto into a ServerAddress - let address: ServerAddress = match address.try_into() { - Ok(address) => address, - Err(_) => return Err(StartError::InvalidAddress), - }; + /// [`ServerAddr`]: azalea_protocol::address::ServerAddr + pub async fn start(self, address: impl ResolvableAddr) -> AppExit { + self.start_with_opts(address, JoinOpts::default()).await + } - self.start_with_default_opts(address, JoinOpts::default()) - .await + #[doc(hidden)] + #[deprecated = "renamed to `start_with_opts`."] + pub async fn start_with_default_opts( + self, + address: impl ResolvableAddr, + default_join_opts: JoinOpts, + ) -> AppExit { + self.start_with_opts(address, default_join_opts).await } /// Do the same as [`Self::start`], but allow passing in default join /// options for the bots. - pub async fn start_with_default_opts( + pub async fn start_with_opts( mut self, - address: impl TryInto, - default_join_opts: JoinOpts, - ) -> Result { + address: impl ResolvableAddr, + join_opts: JoinOpts, + ) -> AppExit { assert_eq!( self.accounts.len(), self.states.len(), @@ -435,17 +436,32 @@ where debug!("Starting Azalea {}", env!("CARGO_PKG_VERSION")); - // convert the TryInto into a ServerAddress - let address = match address.try_into() { - Ok(address) => address, - Err(_) => return Err(StartError::InvalidAddress), - }; + let address = if let Some(socket_addr) = join_opts.custom_socket_addr.clone() { + let server_addr = if let Some(server_addr) = join_opts + .custom_server_addr + .clone() + .or_else(|| address.clone().server_addr().ok()) + { + server_addr + } else { + error!( + "Failed to parse address: {address:?}. If this was expected, consider passing in a `ServerAddr` instead." + ); + return AppExit::error(); + }; - let address: ServerAddress = default_join_opts.custom_address.clone().unwrap_or(address); - let resolved_address = if let Some(a) = default_join_opts.custom_resolved_address { - a + ResolvedAddr { + server: server_addr, + socket: socket_addr, + } } else { - resolve::resolve_address(&address).await? + let Ok(addr) = address.clone().resolve().await else { + error!( + "Failed to resolve address: {address:?}. If this was expected, consider resolving the address earlier with `ResolvableAddr::resolve`." + ); + return AppExit::error(); + }; + addr }; let instance_container = Arc::new(RwLock::new(InstanceContainer::default())); @@ -460,14 +476,13 @@ where let local_set = task::LocalSet::new(); - let appexit = local_set.run_until(async move { + let app_exit = local_set.run_until(async move { // start_ecs_runner must be run inside of the LocalSet let (ecs_lock, start_running_systems, appexit_rx) = start_ecs_runner(&mut self.app); let swarm = Swarm { ecs_lock: ecs_lock.clone(), - resolved_address: Arc::new(RwLock::new(resolved_address)), address: Arc::new(RwLock::new(address)), instance_container, @@ -507,7 +522,7 @@ where if let Some(join_delay) = join_delay { // if there's a join delay, then join one by one for ((account, bot_join_opts), state) in accounts.iter().zip(states) { - let mut join_opts = default_join_opts.clone(); + let mut join_opts = join_opts.clone(); join_opts.update(bot_join_opts); let _ = swarm_clone.add_with_opts(account, state, &join_opts).await; tokio::time::sleep(join_delay).await; @@ -517,7 +532,7 @@ where let swarm_borrow = &swarm_clone; join_all(accounts.iter().zip(states).map( |((account, bot_join_opts), state)| async { - let mut join_opts = default_join_opts.clone(); + let mut join_opts = join_opts.clone(); join_opts.update(bot_join_opts); let _ = swarm_borrow .clone() @@ -603,17 +618,17 @@ where } }); - let appexit = appexit_rx + let app_exit = appexit_rx .await .expect("appexit_tx shouldn't be dropped by the ECS runner before sending"); swarm_handler_task.abort(); client_handler_task.abort(); - appexit + app_exit }).await; - Ok(appexit) + app_exit } } @@ -667,7 +682,7 @@ pub type BoxSwarmHandleFn = /// struct SwarmState {} /// /// #[tokio::main] -/// async fn main() { +/// async fn main() -> AppExit { /// let mut accounts = Vec::new(); /// let mut states = Vec::new(); /// @@ -683,7 +698,6 @@ pub type BoxSwarmHandleFn = /// .join_delay(Duration::from_millis(1000)) /// .start("localhost") /// .await -/// .unwrap(); /// } /// /// async fn handle(bot: Client, event: Event, _state: State) -> anyhow::Result<()> { @@ -714,11 +728,7 @@ impl Swarm { /// # Errors /// /// Returns an error if the server's address could not be resolved. - pub async fn add( - &self, - account: &Account, - state: S, - ) -> Result { + pub async fn add(&self, account: &Account, state: S) -> Client { self.add_with_opts(account, state, &JoinOpts::default()) .await } @@ -735,19 +745,19 @@ impl Swarm { account: &Account, state: S, join_opts: &JoinOpts, - ) -> Result { + ) -> Client { debug!( "add_with_opts called for account {} with opts {join_opts:?}", account.username ); - let address = join_opts - .custom_address - .clone() - .unwrap_or_else(|| self.address.read().clone()); - let resolved_address = join_opts - .custom_resolved_address - .unwrap_or_else(|| *self.resolved_address.read()); + let mut address = self.address.read().clone(); + if let Some(custom_server_addr) = join_opts.custom_server_addr.clone() { + address.server = custom_server_addr; + } + if let Some(custom_socket_addr) = join_opts.custom_socket_addr.clone() { + address.socket = custom_socket_addr; + } let server_proxy = join_opts.server_proxy.clone(); let sessionserver_proxy = join_opts.sessionserver_proxy.clone(); @@ -758,7 +768,6 @@ impl Swarm { account: account.clone(), connect_opts: ConnectOpts { address, - resolved_address, server_proxy, sessionserver_proxy, }, @@ -780,7 +789,7 @@ impl Swarm { rx, swarm_tx, bots_tx, cloned_bot, join_opts, )); - Ok(client) + client } /// Copy the events from a client's receiver into bots_tx, until the bot is -- cgit v1.2.3