From 9642558f8f8d983a7087f15d68be8cf07a85f0c2 Mon Sep 17 00:00:00 2001 From: mat Date: Wed, 15 Dec 2021 23:10:55 -0600 Subject: azalea --- Cargo.lock | 84 ++-- Cargo.toml | 8 +- README.md | 6 +- azalea-chat/Cargo.toml | 11 + azalea-chat/src/base_component.rs | 23 + azalea-chat/src/component.rs | 262 +++++++++++ azalea-chat/src/events.rs | 26 ++ azalea-chat/src/lib.rs | 11 + azalea-chat/src/style.rs | 478 +++++++++++++++++++++ azalea-chat/src/text_component.rs | 122 ++++++ azalea-chat/src/translatable_component.rs | 24 ++ azalea-chat/tests/integration_test.rs | 75 ++++ azalea-client/Cargo.toml | 9 + azalea-client/src/connect.rs | 53 +++ azalea-client/src/crypt.rs | 0 azalea-client/src/lib.rs | 13 + azalea-client/src/ping.rs | 44 ++ azalea-core/Cargo.toml | 9 + azalea-core/src/lib.rs | 12 + azalea-core/src/serializable_uuid.rs | 54 +++ azalea-protocol/Cargo.toml | 19 + azalea-protocol/src/connect.rs | 116 +++++ azalea-protocol/src/lib.rs | 56 +++ azalea-protocol/src/mc_buf.rs | 203 +++++++++ azalea-protocol/src/packets/game/mod.rs | 35 ++ .../packets/handshake/client_intention_packet.rs | 36 ++ azalea-protocol/src/packets/handshake/mod.rs | 49 +++ .../login/clientbound_custom_query_packet.rs | 41 ++ .../src/packets/login/clientbound_hello_packet.rs | 38 ++ azalea-protocol/src/packets/login/mod.rs | 63 +++ .../src/packets/login/serverbound_hello_packet.rs | 27 ++ azalea-protocol/src/packets/mod.rs | 146 +++++++ .../status/clientbound_status_response_packet.rs | 58 +++ azalea-protocol/src/packets/status/mod.rs | 66 +++ .../status/serverbound_status_request_packet.rs | 23 + azalea-protocol/src/read.rs | 28 ++ azalea-protocol/src/resolver.rs | 55 +++ azalea-protocol/src/write.rs | 27 ++ bot/Cargo.toml | 6 +- bot/src/main.rs | 2 +- minecraft-chat/Cargo.toml | 11 - minecraft-chat/src/base_component.rs | 23 - minecraft-chat/src/component.rs | 262 ----------- minecraft-chat/src/events.rs | 26 -- minecraft-chat/src/lib.rs | 11 - minecraft-chat/src/style.rs | 478 --------------------- minecraft-chat/src/text_component.rs | 122 ------ minecraft-chat/src/translatable_component.rs | 24 -- minecraft-chat/tests/integration_test.rs | 75 ---- minecraft-client/Cargo.toml | 9 - minecraft-client/src/connect.rs | 53 --- minecraft-client/src/crypt.rs | 0 minecraft-client/src/lib.rs | 13 - minecraft-client/src/ping.rs | 44 -- minecraft-core/Cargo.toml | 9 - minecraft-core/src/lib.rs | 12 - minecraft-core/src/serializable_uuid.rs | 54 --- minecraft-protocol/Cargo.toml | 19 - minecraft-protocol/src/connect.rs | 116 ----- minecraft-protocol/src/lib.rs | 56 --- minecraft-protocol/src/mc_buf.rs | 203 --------- minecraft-protocol/src/packets/game/mod.rs | 35 -- .../packets/handshake/client_intention_packet.rs | 36 -- minecraft-protocol/src/packets/handshake/mod.rs | 49 --- .../login/clientbound_custom_query_packet.rs | 41 -- .../src/packets/login/clientbound_hello_packet.rs | 38 -- minecraft-protocol/src/packets/login/mod.rs | 63 --- .../src/packets/login/serverbound_hello_packet.rs | 27 -- minecraft-protocol/src/packets/mod.rs | 146 ------- .../status/clientbound_status_response_packet.rs | 58 --- minecraft-protocol/src/packets/status/mod.rs | 66 --- .../status/serverbound_status_request_packet.rs | 23 - minecraft-protocol/src/read.rs | 28 -- minecraft-protocol/src/resolver.rs | 55 --- minecraft-protocol/src/write.rs | 27 -- 75 files changed, 2367 insertions(+), 2363 deletions(-) create mode 100644 azalea-chat/Cargo.toml create mode 100644 azalea-chat/src/base_component.rs create mode 100644 azalea-chat/src/component.rs create mode 100644 azalea-chat/src/events.rs create mode 100644 azalea-chat/src/lib.rs create mode 100644 azalea-chat/src/style.rs create mode 100644 azalea-chat/src/text_component.rs create mode 100644 azalea-chat/src/translatable_component.rs create mode 100644 azalea-chat/tests/integration_test.rs create mode 100644 azalea-client/Cargo.toml create mode 100644 azalea-client/src/connect.rs create mode 100644 azalea-client/src/crypt.rs create mode 100644 azalea-client/src/lib.rs create mode 100644 azalea-client/src/ping.rs create mode 100644 azalea-core/Cargo.toml create mode 100644 azalea-core/src/lib.rs create mode 100644 azalea-core/src/serializable_uuid.rs create mode 100644 azalea-protocol/Cargo.toml create mode 100644 azalea-protocol/src/connect.rs create mode 100644 azalea-protocol/src/lib.rs create mode 100644 azalea-protocol/src/mc_buf.rs create mode 100644 azalea-protocol/src/packets/game/mod.rs create mode 100644 azalea-protocol/src/packets/handshake/client_intention_packet.rs create mode 100644 azalea-protocol/src/packets/handshake/mod.rs create mode 100644 azalea-protocol/src/packets/login/clientbound_custom_query_packet.rs create mode 100644 azalea-protocol/src/packets/login/clientbound_hello_packet.rs create mode 100644 azalea-protocol/src/packets/login/mod.rs create mode 100644 azalea-protocol/src/packets/login/serverbound_hello_packet.rs create mode 100644 azalea-protocol/src/packets/mod.rs create mode 100644 azalea-protocol/src/packets/status/clientbound_status_response_packet.rs create mode 100644 azalea-protocol/src/packets/status/mod.rs create mode 100644 azalea-protocol/src/packets/status/serverbound_status_request_packet.rs create mode 100644 azalea-protocol/src/read.rs create mode 100644 azalea-protocol/src/resolver.rs create mode 100644 azalea-protocol/src/write.rs delete mode 100644 minecraft-chat/Cargo.toml delete mode 100644 minecraft-chat/src/base_component.rs delete mode 100644 minecraft-chat/src/component.rs delete mode 100644 minecraft-chat/src/events.rs delete mode 100644 minecraft-chat/src/lib.rs delete mode 100644 minecraft-chat/src/style.rs delete mode 100644 minecraft-chat/src/text_component.rs delete mode 100644 minecraft-chat/src/translatable_component.rs delete mode 100644 minecraft-chat/tests/integration_test.rs delete mode 100644 minecraft-client/Cargo.toml delete mode 100644 minecraft-client/src/connect.rs delete mode 100644 minecraft-client/src/crypt.rs delete mode 100644 minecraft-client/src/lib.rs delete mode 100644 minecraft-client/src/ping.rs delete mode 100644 minecraft-core/Cargo.toml delete mode 100644 minecraft-core/src/lib.rs delete mode 100644 minecraft-core/src/serializable_uuid.rs delete mode 100644 minecraft-protocol/Cargo.toml delete mode 100644 minecraft-protocol/src/connect.rs delete mode 100644 minecraft-protocol/src/lib.rs delete mode 100644 minecraft-protocol/src/mc_buf.rs delete mode 100644 minecraft-protocol/src/packets/game/mod.rs delete mode 100644 minecraft-protocol/src/packets/handshake/client_intention_packet.rs delete mode 100644 minecraft-protocol/src/packets/handshake/mod.rs delete mode 100644 minecraft-protocol/src/packets/login/clientbound_custom_query_packet.rs delete mode 100644 minecraft-protocol/src/packets/login/clientbound_hello_packet.rs delete mode 100644 minecraft-protocol/src/packets/login/mod.rs delete mode 100644 minecraft-protocol/src/packets/login/serverbound_hello_packet.rs delete mode 100644 minecraft-protocol/src/packets/mod.rs delete mode 100644 minecraft-protocol/src/packets/status/clientbound_status_response_packet.rs delete mode 100644 minecraft-protocol/src/packets/status/mod.rs delete mode 100644 minecraft-protocol/src/packets/status/serverbound_status_request_packet.rs delete mode 100644 minecraft-protocol/src/read.rs delete mode 100644 minecraft-protocol/src/resolver.rs delete mode 100644 minecraft-protocol/src/write.rs diff --git a/Cargo.lock b/Cargo.lock index f2728389..e999fcc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,6 +30,46 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "azalea-chat" +version = "0.1.0" +dependencies = [ + "lazy_static", + "serde", + "serde_json", +] + +[[package]] +name = "azalea-client" +version = "0.1.0" +dependencies = [ + "azalea-protocol", +] + +[[package]] +name = "azalea-core" +version = "0.1.0" +dependencies = [ + "uuid", +] + +[[package]] +name = "azalea-protocol" +version = "0.1.0" +dependencies = [ + "async-recursion", + "async-trait", + "azalea-chat", + "byteorder", + "bytes", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-util", + "trust-dns-resolver", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -40,8 +80,8 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" name = "bot" version = "0.1.0" dependencies = [ - "minecraft-client", - "minecraft-protocol", + "azalea-client", + "azalea-protocol", "tokio", ] @@ -284,46 +324,6 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" -[[package]] -name = "minecraft-chat" -version = "0.1.0" -dependencies = [ - "lazy_static", - "serde", - "serde_json", -] - -[[package]] -name = "minecraft-client" -version = "0.1.0" -dependencies = [ - "minecraft-protocol", -] - -[[package]] -name = "minecraft-core" -version = "0.1.0" -dependencies = [ - "uuid", -] - -[[package]] -name = "minecraft-protocol" -version = "0.1.0" -dependencies = [ - "async-recursion", - "async-trait", - "byteorder", - "bytes", - "minecraft-chat", - "serde", - "serde_json", - "thiserror", - "tokio", - "tokio-util", - "trust-dns-resolver", -] - [[package]] name = "mio" version = "0.7.14" diff --git a/Cargo.toml b/Cargo.toml index a4ad743a..22f3e5a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,8 +2,8 @@ members = [ "bot", - "minecraft-client", - "minecraft-protocol", - "minecraft-chat", - "minecraft-core", + "azalea-client", + "azalea-protocol", + "azalea-chat", + "azalea-core", ] diff --git a/README.md b/README.md index 06f78a52..539e1fdb 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,6 @@ -# this library doesn't have a name yet idk what to call it +# Azalea + +A Minecraft botting library + +I named this Azalea because it sounds like a cool word and this is a cool library. diff --git a/azalea-chat/Cargo.toml b/azalea-chat/Cargo.toml new file mode 100644 index 00000000..7019e5bf --- /dev/null +++ b/azalea-chat/Cargo.toml @@ -0,0 +1,11 @@ +[package] +edition = "2021" +name = "azalea-chat" +version = "0.1.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +lazy_static = "1.4.0" +serde = "^1.0.130" +serde_json = "^1.0.72" diff --git a/azalea-chat/src/base_component.rs b/azalea-chat/src/base_component.rs new file mode 100644 index 00000000..fa39a11c --- /dev/null +++ b/azalea-chat/src/base_component.rs @@ -0,0 +1,23 @@ +use crate::{component::Component, style::Style}; + +#[derive(Clone, Debug)] +pub struct BaseComponent { + // implements mutablecomponent + pub siblings: Vec, + pub style: Style, +} + +impl BaseComponent { + pub fn new() -> Self { + Self { + siblings: Vec::new(), + style: Style::default(), + } + } +} + +impl Default for BaseComponent { + fn default() -> Self { + Self::new() + } +} diff --git a/azalea-chat/src/component.rs b/azalea-chat/src/component.rs new file mode 100644 index 00000000..1ed1f836 --- /dev/null +++ b/azalea-chat/src/component.rs @@ -0,0 +1,262 @@ +use serde::{de, Deserialize, Deserializer}; + +use crate::{ + base_component::BaseComponent, + style::{ChatFormatting, Style}, + text_component::TextComponent, + translatable_component::{StringOrComponent, TranslatableComponent}, +}; + +#[derive(Clone, Debug)] +pub enum Component { + Text(TextComponent), + Translatable(TranslatableComponent), +} + +lazy_static! { + pub static ref DEFAULT_STYLE: Style = Style { + color: Some(ChatFormatting::WHITE.try_into().unwrap()), + ..Style::default() + }; +} + +/// A chat component +impl Component { + // TODO: is it possible to use a macro so this doesn't have to be duplicated? + + pub fn get_base_mut(&mut self) -> &mut BaseComponent { + match self { + Self::Text(c) => &mut c.base, + Self::Translatable(c) => &mut c.base, + } + } + + pub fn get_base(&self) -> &BaseComponent { + match self { + Self::Text(c) => &c.base, + Self::Translatable(c) => &c.base, + } + } + + /// Add a component as a sibling of this one + fn append(&mut self, sibling: Component) { + self.get_base_mut().siblings.push(sibling); + } + + /// Get the "separator" component from the json + fn parse_separator(json: &serde_json::Value) -> Result, serde_json::Error> { + if json.get("separator").is_some() { + return Ok(Some(Component::deserialize( + json.get("separator").unwrap(), + )?)); + } + Ok(None) + } + + /// Convert this component into an ansi string + pub fn to_ansi(&self, default_style: Option<&Style>) -> String { + // default the default_style to white if it's not set + let default_style: &Style = default_style.unwrap_or(&DEFAULT_STYLE); + + // this contains the final string will all the ansi escape codes + let mut built_string = String::new(); + // this style will update as we visit components + let mut running_style = Style::default(); + + for component in self.clone().into_iter() { + let component_text = match &component { + Self::Text(c) => &c.text, + Self::Translatable(c) => &c.key, + }; + let component_style = &component.get_base().style; + + let ansi_text = running_style.compare_ansi(component_style, default_style); + built_string.push_str(&ansi_text); + built_string.push_str(component_text); + + running_style.apply(component_style); + } + + if !running_style.is_empty() { + built_string.push_str("\u{1b}[m"); + } + + built_string + } +} + +impl IntoIterator for Component { + /// Recursively call the function for every component in this component + fn into_iter(self) -> Self::IntoIter { + let base = self.get_base(); + let siblings = base.siblings.clone(); + let mut v: Vec = Vec::with_capacity(siblings.len() + 1); + v.push(self); + for sibling in siblings { + v.extend(sibling.into_iter()); + } + + v.into_iter() + } + + type Item = Component; + type IntoIter = std::vec::IntoIter; +} + +impl<'de> Deserialize<'de> for Component { + fn deserialize(de: D) -> Result + where + D: Deserializer<'de>, + { + let json: serde_json::Value = serde::Deserialize::deserialize(de)?; + + // we create a component that we might add siblings to + let mut component: Component; + + // if it's primitive, make it a text component + if !json.is_array() && !json.is_object() { + return Ok(Component::Text(TextComponent::new( + json.as_str().unwrap_or("").to_string(), + ))); + } + // if it's an object, do things with { text } and stuff + else if json.is_object() { + if json.get("text").is_some() { + let text = json.get("text").unwrap().as_str().unwrap_or("").to_string(); + component = Component::Text(TextComponent::new(text)); + } else if json.get("translate").is_some() { + let translate = json.get("translate").unwrap().to_string(); + if json.get("with").is_some() { + let with = json.get("with").unwrap().as_array().unwrap(); + let mut with_array = Vec::with_capacity(with.len()); + for item in with { + // if it's a string component with no styling and no siblings, just add a string to with_array + // otherwise add the component to the array + let c = Component::deserialize(item).map_err(de::Error::custom)?; + if let Component::Text(text_component) = c { + if text_component.base.siblings.is_empty() + && text_component.base.style.is_empty() + { + with_array.push(StringOrComponent::String(text_component.text)); + break; + } + } + with_array.push(StringOrComponent::Component( + Component::deserialize(item).map_err(de::Error::custom)?, + )); + } + component = + Component::Translatable(TranslatableComponent::new(translate, with_array)); + } else { + // if it doesn't have a "with", just have the with_array be empty + component = + Component::Translatable(TranslatableComponent::new(translate, Vec::new())); + } + } else if json.get("score").is_some() { + // object = GsonHelper.getAsJsonObject(jsonObject, "score"); + let score_json = json.get("score").unwrap(); + // if (!object.has("name") || !object.has("objective")) throw new JsonParseException("A score component needs a least a name and an objective"); + // ScoreComponent scoreComponent = new ScoreComponent(GsonHelper.getAsString((JsonObject)object, "name"), GsonHelper.getAsString((JsonObject)object, "objective")); + if score_json.get("name").is_none() || score_json.get("objective").is_none() { + return Err(de::Error::missing_field( + "A score component needs at least a name and an objective", + )); + } + // TODO + return Err(de::Error::custom( + "score text components aren't yet supported", + )); + // component = ScoreComponent + } else if json.get("selector").is_some() { + // } else if (jsonObject.has("selector")) { + // object = this.parseSeparator(type, jsonDeserializationContext, jsonObject); + // SelectorComponent selectorComponent = new SelectorComponent(GsonHelper.getAsString(jsonObject, "selector"), (Optional)object); + + return Err(de::Error::custom( + "selector text components aren't yet supported", + )); + // } else if (jsonObject.has("keybind")) { + // KeybindComponent keybindComponent = new KeybindComponent(GsonHelper.getAsString(jsonObject, "keybind")); + } else if json.get("keybind").is_some() { + return Err(de::Error::custom( + "keybind text components aren't yet supported", + )); + } else { + // } else { + // if (!jsonObject.has("nbt")) throw new JsonParseException("Don't know how to turn " + jsonElement + " into a Component"); + if json.get("nbt").is_none() { + return Err(de::Error::custom( + format!("Don't know how to turn {} into a Component", json).as_str(), + )); + } + // object = GsonHelper.getAsString(jsonObject, "nbt"); + let _nbt = json.get("nbt").unwrap().to_string(); + // Optional optional = this.parseSeparator(type, jsonDeserializationContext, jsonObject); + let _separator = Component::parse_separator(&json).map_err(de::Error::custom)?; + + let _interpret = match json.get("interpret") { + Some(v) => v.as_bool().ok_or(Some(false)).unwrap(), + None => false, + }; + // boolean bl = GsonHelper.getAsBoolean(jsonObject, "interpret", false); + // if (jsonObject.has("block")) { + if json.get("block").is_some() {} + return Err(de::Error::custom( + "nbt text components aren't yet supported", + )); + // NbtComponent.BlockNbtComponent blockNbtComponent = new NbtComponent.BlockNbtComponent((String)object, bl, GsonHelper.getAsString(jsonObject, "block"), optional); + // } else if (jsonObject.has("entity")) { + // NbtComponent.EntityNbtComponent entityNbtComponent = new NbtComponent.EntityNbtComponent((String)object, bl, GsonHelper.getAsString(jsonObject, "entity"), optional); + // } else { + // if (!jsonObject.has("storage")) throw new JsonParseException("Don't know how to turn " + jsonElement + " into a Component"); + // NbtComponent.StorageNbtComponent storageNbtComponent = new NbtComponent.StorageNbtComponent((String)object, bl, new ResourceLocation(GsonHelper.getAsString(jsonObject, "storage")), optional); + // } + // } + } + // if (jsonObject.has("extra")) { + // object = GsonHelper.getAsJsonArray(jsonObject, "extra"); + // if (object.size() <= 0) throw new JsonParseException("Unexpected empty array of components"); + // for (int i = 0; i < object.size(); ++i) { + // var5_17.append(this.deserialize(object.get(i), type, jsonDeserializationContext)); + // } + // } + // var5_17.setStyle((Style)jsonDeserializationContext.deserialize(jsonElement, Style.class)); + // return var5_17; + // } + if json.get("extra").is_some() { + let extra = match json.get("extra").unwrap().as_array() { + Some(r) => r, + None => return Err(de::Error::custom("Extra isn't an array")), + }; + if extra.is_empty() { + return Err(de::Error::custom("Unexpected empty array of components")); + } + for extra_component in extra { + let sibling = + Component::deserialize(extra_component).map_err(de::Error::custom)?; + component.append(sibling); + } + } + + let style = Style::deserialize(&json); + component.get_base_mut().style = style; + + return Ok(component); + } + // ok so it's not an object, if it's an array deserialize every item + else if !json.is_array() { + return Err(de::Error::custom( + format!("Don't know how to turn {} into a Component", json).as_str(), + )); + } + let json_array = json.as_array().unwrap(); + // the first item in the array is the one that we're gonna return, the others are siblings + let mut component = Component::deserialize(&json_array[0]).map_err(de::Error::custom)?; + for i in 1..json_array.len() { + component.append( + Component::deserialize(json_array.get(i).unwrap()).map_err(de::Error::custom)?, + ); + } + Ok(component) + } +} diff --git a/azalea-chat/src/events.rs b/azalea-chat/src/events.rs new file mode 100644 index 00000000..a547169e --- /dev/null +++ b/azalea-chat/src/events.rs @@ -0,0 +1,26 @@ +enum ClickAction { + OPEN_URL = Action::new("open_url", true), + OPEN_FILE = Action::new("open_file", false), + RUN_COMMAND = Action::new("run_command", true), + SUGGEST_COMMAND = Action::new("suggest_command", true), + CHANGE_PAGE = Action::new("change_page", true), + COPY_TO_CLIPBOARD = Action::new("copy_to_clipboard", true), +} + +struct ClickAction { + pub name: String, + pub allow_from_server: bool, +} + +impl ClickAction { + fn new(name: &str, allow_from_server: bool) -> Self { + Self { + name: name.to_string(), + allow_from_server, + } + } +} + +struct ClickEvent { + action: ClickAction, +} diff --git a/azalea-chat/src/lib.rs b/azalea-chat/src/lib.rs new file mode 100644 index 00000000..b7035e13 --- /dev/null +++ b/azalea-chat/src/lib.rs @@ -0,0 +1,11 @@ +//! Things for working with Minecraft chat messages. +//! This was inspired by Minecraft and prismarine-chat. + +#[macro_use] +extern crate lazy_static; + +pub mod base_component; +pub mod component; +pub mod style; +pub mod text_component; +pub mod translatable_component; diff --git a/azalea-chat/src/style.rs b/azalea-chat/src/style.rs new file mode 100644 index 00000000..4e3b24de --- /dev/null +++ b/azalea-chat/src/style.rs @@ -0,0 +1,478 @@ +use std::{collections::HashMap, fmt}; + +use serde_json::Value; + +#[derive(Clone, PartialEq, Debug)] +pub struct TextColor { + pub value: u32, + pub name: Option, +} + +impl TextColor { + pub fn parse(value: String) -> Result { + if value.starts_with('#') { + let n = value.chars().skip(1).collect::(); + let n = u32::from_str_radix(&n, 16).unwrap(); + return Ok(TextColor::from_rgb(n)); + } + let color_option = NAMED_COLORS.get(&value.to_ascii_uppercase()); + if let Some(color) = color_option { + return Ok(color.clone()); + } + Err(format!("Invalid color {}", value)) + } + + fn from_rgb(value: u32) -> TextColor { + TextColor { value, name: None } + } +} + +lazy_static! { + static ref LEGACY_FORMAT_TO_COLOR: HashMap<&'static ChatFormatting<'static>, TextColor> = { + let mut legacy_format_to_color = HashMap::new(); + for formatter in &ChatFormatting::FORMATTERS { + if !formatter.is_format && *formatter != ChatFormatting::RESET { + legacy_format_to_color.insert( + formatter, + TextColor { + value: formatter.color.unwrap(), + name: Some(formatter.name.to_string()), + }, + ); + } + } + legacy_format_to_color + }; + static ref NAMED_COLORS: HashMap = { + let mut named_colors = HashMap::new(); + for color in LEGACY_FORMAT_TO_COLOR.values() { + named_colors.insert(color.name.clone().unwrap(), color.clone()); + } + named_colors + }; +} + +#[derive(Clone, PartialEq, Eq, Hash, Debug)] +pub struct ChatFormatting<'a> { + pub name: &'a str, + pub code: char, + pub is_format: bool, + pub id: i32, + pub color: Option, +} + +pub struct Ansi {} +impl Ansi { + pub const BOLD: &'static str = "\u{1b}[1m"; + pub const ITALIC: &'static str = "\u{1b}[3m"; + pub const UNDERLINED: &'static str = "\u{1b}[4m"; + pub const STRIKETHROUGH: &'static str = "\u{1b}[9m"; + pub const OBFUSCATED: &'static str = "\u{1b}[8m"; + pub const RESET: &'static str = "\u{1b}[m"; + + pub fn rgb(value: u32) -> String { + format!( + "\u{1b}[38;2;{};{};{}m", + (value >> 16) & 0xFF, + (value >> 8) & 0xFF, + value & 0xFF + ) + } +} + +impl<'a> ChatFormatting<'a> { + pub const BLACK: ChatFormatting<'a> = ChatFormatting::new("BLACK", '0', false, 0, Some(0)); + pub const DARK_BLUE: ChatFormatting<'a> = + ChatFormatting::new("DARK_BLUE", '1', false, 1, Some(170)); + pub const DARK_GREEN: ChatFormatting<'a> = + ChatFormatting::new("DARK_GREEN", '2', false, 2, Some(43520)); + pub const DARK_AQUA: ChatFormatting<'a> = + ChatFormatting::new("DARK_AQUA", '3', false, 3, Some(43690)); + pub const DARK_RED: ChatFormatting<'a> = + ChatFormatting::new("DARK_RED", '4', false, 4, Some(1114112)); + pub const DARK_PURPLE: ChatFormatting<'a> = + ChatFormatting::new("DARK_PURPLE", '5', false, 5, Some(11141290)); + pub const GOLD: ChatFormatting<'a> = ChatFormatting::new("GOLD", '6', false, 6, Some(16755200)); + pub const GRAY: ChatFormatting<'a> = ChatFormatting::new("GRAY", '7', false, 7, Some(11184810)); + pub const DARK_GRAY: ChatFormatting<'a> = + ChatFormatting::new("DARK_GRAY", '8', false, 8, Some(5592405)); + pub const BLUE: ChatFormatting<'a> = ChatFormatting::new("BLUE", '9', false, 9, Some(5592575)); + pub const GREEN: ChatFormatting<'a> = + ChatFormatting::new("GREEN", 'a', false, 10, Some(5635925)); + pub const AQUA: ChatFormatting<'a> = ChatFormatting::new("AQUA", 'b', false, 11, Some(5636095)); + pub const RED: ChatFormatting<'a> = ChatFormatting::new("RED", 'c', false, 12, Some(16733525)); + pub const LIGHT_PURPLE: ChatFormatting<'a> = + ChatFormatting::new("LIGHT_PURPLE", 'd', false, 13, Some(16733695)); + pub const YELLOW: ChatFormatting<'a> = + ChatFormatting::new("YELLOW", 'e', false, 14, Some(16777045)); + pub const WHITE: ChatFormatting<'a> = + ChatFormatting::new("WHITE", 'f', false, 15, Some(16777215)); + pub const OBFUSCATED: ChatFormatting<'a> = + ChatFormatting::new("OBFUSCATED", 'k', true, -1, None); + pub const STRIKETHROUGH: ChatFormatting<'a> = + ChatFormatting::new("STRIKETHROUGH", 'm', true, -1, None); + pub const BOLD: ChatFormatting<'a> = ChatFormatting::new("BOLD", 'l', true, -1, None); + pub const UNDERLINE: ChatFormatting<'a> = ChatFormatting::new("UNDERLINE", 'n', true, -1, None); + pub const ITALIC: ChatFormatting<'a> = ChatFormatting::new("ITALIC", 'o', true, -1, None); + pub const RESET: ChatFormatting<'a> = ChatFormatting::new("RESET", 'r', true, -1, None); + + pub const FORMATTERS: [ChatFormatting<'a>; 22] = [ + ChatFormatting::BLACK, + ChatFormatting::DARK_BLUE, + ChatFormatting::DARK_GREEN, + ChatFormatting::DARK_AQUA, + ChatFormatting::DARK_RED, + ChatFormatting::DARK_PURPLE, + ChatFormatting::GOLD, + ChatFormatting::GRAY, + ChatFormatting::DARK_GRAY, + ChatFormatting::BLUE, + ChatFormatting::GREEN, + ChatFormatting::AQUA, + ChatFormatting::RED, + ChatFormatting::LIGHT_PURPLE, + ChatFormatting::YELLOW, + ChatFormatting::WHITE, + ChatFormatting::OBFUSCATED, + ChatFormatting::STRIKETHROUGH, + ChatFormatting::BOLD, + ChatFormatting::UNDERLINE, + ChatFormatting::ITALIC, + ChatFormatting::RESET, + ]; + + const fn new( + name: &str, + code: char, + is_format: bool, + id: i32, + color: Option, + ) -> ChatFormatting { + ChatFormatting { + name, + code, + is_format, + id, + color, + } + } + + pub fn from_code(code: char) -> Result<&'static ChatFormatting<'static>, String> { + for formatter in &ChatFormatting::FORMATTERS { + if formatter.code == code { + return Ok(formatter); + } + } + Err(format!("Invalid formatting code {}", code)) + } +} + +impl TextColor { + fn new(value: u32, name: Option) -> Self { + Self { value, name } + } + + pub fn format(&self) -> String { + format!("#{:06X}", self.value) + } +} + +impl fmt::Display for TextColor { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(name) = &self.name { + write!(f, "{}", name.clone()) + } else { + write!(f, "{}", self.format()) + } + } +} + +// from ChatFormatting to TextColor +impl TryFrom> for TextColor { + type Error = String; + + fn try_from(formatter: ChatFormatting<'_>) -> Result { + if formatter.is_format { + return Err(format!("{} is not a color", formatter.name)); + } + let color = formatter.color.unwrap_or(0); + Ok(Self::new(color, Some(formatter.name.to_string()))) + } +} + +#[derive(Clone, Debug)] +pub struct Style { + // these are options instead of just bools because None is different than false in this case + pub color: Option, + pub bold: Option, + pub italic: Option, + pub underlined: Option, + pub strikethrough: Option, + pub obfuscated: Option, + /// Whether it should reset the formatting before applying these styles + pub reset: bool, +} + +impl Style { + pub fn default() -> Self { + Self::empty() + } + + pub fn empty() -> Self { + Self { + color: None, + bold: None, + italic: None, + underlined: None, + strikethrough: None, + obfuscated: None, + reset: false, + } + } + + pub fn deserialize(json: &Value) -> Style { + return if json.is_object() { + let json_object = json.as_object().unwrap(); + let bold = json_object.get("bold").and_then(|v| v.as_bool()); + let italic = json_object.get("italic").and_then(|v| v.as_bool()); + let underlined = json_object.get("underlined").and_then(|v| v.as_bool()); + let strikethrough = json_object.get("strikethrough").and_then(|v| v.as_bool()); + let obfuscated = json_object.get("obfuscated").and_then(|v| v.as_bool()); + let color: Option = json_object + .get("color") + .and_then(|v| v.as_str()) + .and_then(|v| TextColor::parse(v.to_string()).ok()); + Style { + color, + bold, + italic, + underlined, + strikethrough, + obfuscated, + ..Style::default() + } + } else { + Style::default() + }; + } + + /// Check if a style has no attributes set + pub fn is_empty(&self) -> bool { + self.color.is_none() + && self.bold.is_none() + && self.italic.is_none() + && self.underlined.is_none() + && self.strikethrough.is_none() + && self.obfuscated.is_none() + } + + /// find the necessary ansi code to get from this style to another + pub fn compare_ansi(&self, after: &Style, default_style: &Style) -> String { + let should_reset = after.reset || + // if it used to be bold and now it's not, reset + (self.bold.unwrap_or(false) && !after.bold.unwrap_or(true)) || + // if it used to be italic and now it's not, reset + (self.italic.unwrap_or(false) && !after.italic.unwrap_or(true)) || + // if it used to be underlined and now it's not, reset + (self.underlined.unwrap_or(false) && !after.underlined.unwrap_or(true)) || + // if it used to be strikethrough and now it's not, reset + (self.strikethrough.unwrap_or(false) && !after.strikethrough.unwrap_or(true)) || + // if it used to be obfuscated and now it's not, reset + (self.obfuscated.unwrap_or(false) && !after.obfuscated.unwrap_or(true)); + + let mut ansi_codes = String::new(); + + let empty_style = Style::empty(); + + let (before, after) = if should_reset { + ansi_codes.push_str(Ansi::RESET); + let mut updated_after = if after.reset { + default_style.clone() + } else { + self.clone() + }; + updated_after.apply(after); + (&empty_style, updated_after) + } else { + (self, after.clone()) + }; + + // if bold used to be false/default and now it's true, set bold + if !before.bold.unwrap_or(false) && after.bold.unwrap_or(false) { + ansi_codes.push_str(Ansi::BOLD); + } + // if italic used to be false/default and now it's true, set italic + if !before.italic.unwrap_or(false) && after.italic.unwrap_or(false) { + ansi_codes.push_str(Ansi::ITALIC); + } + // if underlined used to be false/default and now it's true, set underlined + if !before.underlined.unwrap_or(false) && after.underlined.unwrap_or(false) { + ansi_codes.push_str(Ansi::UNDERLINED); + } + // if strikethrough used to be false/default and now it's true, set strikethrough + if !before.strikethrough.unwrap_or(false) && after.strikethrough.unwrap_or(false) { + ansi_codes.push_str(Ansi::STRIKETHROUGH); + } + // if obfuscated used to be false/default and now it's true, set obfuscated + if !before.obfuscated.unwrap_or(false) && after.obfuscated.unwrap_or(false) { + ansi_codes.push_str(Ansi::OBFUSCATED); + } + + // if the new color is different and not none, set color + let color_changed = { + if before.color.is_none() && after.color.is_some() { + true + } else if before.color.is_some() && after.color.is_some() { + before.color.clone().unwrap().value != after.color.as_ref().unwrap().value + } else { + false + } + }; + + if color_changed { + let after_color = after.color.as_ref().unwrap(); + ansi_codes.push_str(&Ansi::rgb(after_color.value)); + } + + ansi_codes + } + + /// Apply another style to this one + pub fn apply(&mut self, style: &Style) { + if let Some(color) = &style.color { + self.color = Some(color.clone()); + } + if let Some(bold) = &style.bold { + self.bold = Some(*bold); + } + if let Some(italic) = &style.italic { + self.italic = Some(*italic); + } + if let Some(underlined) = &style.underlined { + self.underlined = Some(*underlined); + } + if let Some(strikethrough) = &style.strikethrough { + self.strikethrough = Some(*strikethrough); + } + if let Some(obfuscated) = &style.obfuscated { + self.obfuscated = Some(*obfuscated); + } + } + + /// Apply a ChatFormatting to this style + pub fn apply_formatting(&mut self, formatting: &ChatFormatting) { + match *formatting { + ChatFormatting::BOLD => self.bold = Some(true), + ChatFormatting::ITALIC => self.italic = Some(true), + ChatFormatting::UNDERLINE => self.underlined = Some(true), + ChatFormatting::STRIKETHROUGH => self.strikethrough = Some(true), + ChatFormatting::OBFUSCATED => self.obfuscated = Some(true), + ChatFormatting::RESET => self.reset = true, + ChatFormatting { + name: _, + code: _, + is_format: _, + id: _, + color, + } => { + // if it's a color, set it + if let Some(color) = color { + self.color = Some(TextColor::from_rgb(color)); + } + } + } + } +} + +#[cfg(test)] +mod tests { + use crate::component::DEFAULT_STYLE; + + use super::*; + + #[test] + fn text_color_named_colors() { + assert_eq!(TextColor::parse("red".to_string()).unwrap().value, 16733525); + } + #[test] + fn text_color_hex_colors() { + assert_eq!( + TextColor::parse("#a1b2c3".to_string()).unwrap().value, + 10597059 + ); + } + + #[test] + fn ansi_difference_should_reset() { + let style_a = Style { + bold: Some(true), + italic: Some(true), + ..Style::default() + }; + let style_b = Style { + bold: Some(false), + ..Style::default() + }; + let ansi_difference = style_a.compare_ansi(&style_b, &Style::default()); + assert_eq!( + ansi_difference, + format!( + "{reset}{italic}", + reset = Ansi::RESET, + italic = Ansi::ITALIC + ) + ) + } + #[test] + fn ansi_difference_shouldnt_reset() { + let style_a = Style { + bold: Some(true), + ..Style::default() + }; + let style_b = Style { + italic: Some(true), + ..Style::default() + }; + let ansi_difference = style_a.compare_ansi(&style_b, &Style::default()); + assert_eq!(ansi_difference, Ansi::ITALIC) + } + + #[test] + fn ansi_difference_explicit_reset() { + let style_a = Style { + bold: Some(true), + ..Style::empty() + }; + let style_b = Style { + italic: Some(true), + reset: true, + ..Style::empty() + }; + let ansi_difference = style_a.compare_ansi(&style_b, &DEFAULT_STYLE); + assert_eq!( + ansi_difference, + format!( + "{reset}{italic}{white}", + reset = Ansi::RESET, + white = Ansi::rgb(ChatFormatting::WHITE.color.unwrap()), + italic = Ansi::ITALIC + ) + ) + } + + #[test] + fn test_from_code() { + assert_eq!( + ChatFormatting::from_code('a').unwrap(), + &ChatFormatting::GREEN + ); + } + + #[test] + fn test_apply_formatting() { + let mut style = Style::default(); + style.apply_formatting(&ChatFormatting::BOLD); + style.apply_formatting(&ChatFormatting::RED); + assert_eq!(style.color, Some(TextColor::from_rgb(16733525))); + } +} diff --git a/azalea-chat/src/text_component.rs b/azalea-chat/src/text_component.rs new file mode 100644 index 00000000..6c43f8b7 --- /dev/null +++ b/azalea-chat/src/text_component.rs @@ -0,0 +1,122 @@ +use std::fmt; + +use crate::{base_component::BaseComponent, component::Component, style::ChatFormatting}; + +#[derive(Clone, Debug)] +pub struct TextComponent { + pub base: BaseComponent, + pub text: String, +} + +const LEGACY_FORMATTING_CODE_SYMBOL: char = '§'; + +/// Convert a legacy color code string into a Component +/// Technically in Minecraft this is done when displaying the text, but AFAIK it's the same as just doing it in TextComponent +pub fn legacy_color_code_to_text_component(legacy_color_code: &str) -> TextComponent { + let mut components: Vec = Vec::with_capacity(1); + // iterate over legacy_color_code, if it starts with LEGACY_COLOR_CODE_SYMBOL then read the next character and get the style from that + // otherwise, add the character to the text + + // we don't use a normal for loop since we need to be able to skip after reading the formatter code symbol + let mut i = 0; + while i < legacy_color_code.chars().count() { + if legacy_color_code.chars().nth(i).unwrap() == LEGACY_FORMATTING_CODE_SYMBOL { + let formatting_code = legacy_color_code.chars().nth(i + 1).unwrap(); + if let Ok(formatter) = ChatFormatting::from_code(formatting_code) { + if components.is_empty() || !components.last().unwrap().text.is_empty() { + components.push(TextComponent::new("".to_string())); + } + + let style = &mut components.last_mut().unwrap().base.style; + // if the formatter is a reset, then we need to reset the style to the default + style.apply_formatting(formatter); + } + i += 1; + } else { + if components.is_empty() { + components.push(TextComponent::new("".to_string())); + } + components + .last_mut() + .unwrap() + .text + .push(legacy_color_code.chars().nth(i).unwrap()); + }; + i += 1; + } + + // create the final component by using the first one as the base, and then adding the rest as siblings + let mut final_component = components.remove(0); + for component in components { + final_component.base.siblings.push(component.get()); + } + + final_component +} + +impl<'a> TextComponent { + pub fn new(text: String) -> Self { + // if it contains a LEGACY_FORMATTING_CODE_SYMBOL, format it + if text.contains(LEGACY_FORMATTING_CODE_SYMBOL) { + legacy_color_code_to_text_component(&text) + } else { + Self { + base: BaseComponent::new(), + text, + } + } + } + + fn get(self) -> Component { + Component::Text(self) + } +} + +impl fmt::Display for TextComponent { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.text.clone()) + } +} + +#[cfg(test)] +mod tests { + use crate::style::Ansi; + + use super::*; + + #[test] + fn test_hypixel_motd() { + let component = + TextComponent::new("§aHypixel Network §c[1.8-1.18]\n§b§lHAPPY HOLIDAYS".to_string()) + .get(); + assert_eq!( + component.to_ansi(None), + format!( + "{GREEN}Hypixel Network {RED}[1.8-1.18]\n{BOLD}{AQUA}HAPPY HOLIDAYS{RESET}", + GREEN = Ansi::rgb(ChatFormatting::GREEN.color.unwrap()), + RED = Ansi::rgb(ChatFormatting::RED.color.unwrap()), + AQUA = Ansi::rgb(ChatFormatting::AQUA.color.unwrap()), + BOLD = Ansi::BOLD, + RESET = Ansi::RESET + ) + ); + } + + #[test] + fn test_legacy_color_code_to_component() { + let component = TextComponent::new("§lHello §r§1w§2o§3r§4l§5d".to_string()).get(); + assert_eq!( + component.to_ansi(None), + format!( + "{BOLD}Hello {RESET}{DARK_BLUE}w{DARK_GREEN}o{DARK_AQUA}r{DARK_RED}l{DARK_PURPLE}d{RESET}", + BOLD = Ansi::BOLD, + RESET = Ansi::RESET, + DARK_BLUE = Ansi::rgb(ChatFormatting::DARK_BLUE.color.unwrap()), + DARK_GREEN = Ansi::rgb(ChatFormatting::DARK_GREEN.color.unwrap()), + DARK_AQUA = Ansi::rgb(ChatFormatting::DARK_AQUA.color.unwrap()), + DARK_RED = Ansi::rgb(ChatFormatting::DARK_RED.color.unwrap()), + DARK_PURPLE = Ansi::rgb(ChatFormatting::DARK_PURPLE.color.unwrap()) + ) + ); + } +} diff --git a/azalea-chat/src/translatable_component.rs b/azalea-chat/src/translatable_component.rs new file mode 100644 index 00000000..0709f7bf --- /dev/null +++ b/azalea-chat/src/translatable_component.rs @@ -0,0 +1,24 @@ +use crate::{base_component::BaseComponent, component::Component}; + +#[derive(Clone, Debug)] +pub enum StringOrComponent { + String(String), + Component(Component), +} + +#[derive(Clone, Debug)] +pub struct TranslatableComponent { + pub base: BaseComponent, + pub key: String, + pub args: Vec, +} + +impl TranslatableComponent { + pub fn new(key: String, args: Vec) -> Self { + Self { + base: BaseComponent::new(), + key, + args, + } + } +} diff --git a/azalea-chat/tests/integration_test.rs b/azalea-chat/tests/integration_test.rs new file mode 100644 index 00000000..1278adfa --- /dev/null +++ b/azalea-chat/tests/integration_test.rs @@ -0,0 +1,75 @@ +use azalea_chat::{ + component::Component, + style::{Ansi, ChatFormatting, TextColor}, +}; +use serde::Deserialize; +use serde_json::Value; + +#[test] +fn basic_ansi_test() { + let j: Value = serde_json::from_str( + r#"{ + "text": "hello", + "color": "red", + "bold": true +}"#, + ) + .unwrap(); + let component = Component::deserialize(&j).unwrap(); + assert_eq!( + component.to_ansi(None), + "\u{1b}[1m\u{1b}[38;2;255;85;85mhello\u{1b}[m" + ); +} + +#[test] +fn complex_ansi_test() { + let j: Value = serde_json::from_str( + r##"[ + { + "text": "hello", + "color": "red", + "bold": true, + "italic": true, + "underlined": true, + "adsfsf": "this should be ignored", + "extra": [ + {"text": " ", "underlined": false}, + {"text": "world", "bold": false, "strikethrough": true, "color": "#abcdef"} + ] + }, + { + "text": " asdf", + "italic": false, + "obfuscated": "true", + "strikethrough": false + }, + { + "text": "!", + "bold": true + } +]"##, + ) + .unwrap(); + let component = Component::deserialize(&j).unwrap(); + assert_eq!( + component.to_ansi(None), + format!( + "{bold}{italic}{underlined}{red}hello{reset}{bold}{italic}{red} {reset}{italic}{strikethrough}{abcdef}world{reset}{abcdef} asdf{bold}!{reset}", + bold = Ansi::BOLD, + italic = Ansi::ITALIC, + underlined = Ansi::UNDERLINED, + red = Ansi::rgb(ChatFormatting::RED.color.unwrap()), + reset = Ansi::RESET, + strikethrough = Ansi::STRIKETHROUGH, + abcdef = Ansi::rgb(TextColor::parse("#abcdef".to_string()).unwrap().value), + ) + ); +} + +#[test] +fn component_from_string() { + let j: Value = serde_json::from_str("\"foo\"").unwrap(); + let component = Component::deserialize(&j).unwrap(); + assert_eq!(component.to_ansi(None), "foo"); +} diff --git a/azalea-client/Cargo.toml b/azalea-client/Cargo.toml new file mode 100644 index 00000000..7e22f727 --- /dev/null +++ b/azalea-client/Cargo.toml @@ -0,0 +1,9 @@ +[package] +edition = "2021" +name = "azalea-client" +version = "0.1.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +azalea-protocol = {path = "../azalea-protocol"} diff --git a/azalea-client/src/connect.rs b/azalea-client/src/connect.rs new file mode 100644 index 00000000..fad5fa92 --- /dev/null +++ b/azalea-client/src/connect.rs @@ -0,0 +1,53 @@ +///! Connect to Minecraft servers. +use azalea_protocol::{ + connect::HandshakeConnection, + packets::{ + handshake::client_intention_packet::ClientIntentionPacket, + login::{serverbound_hello_packet::ServerboundHelloPacket, LoginPacket}, + ConnectionProtocol, PROTOCOL_VERSION, + }, + resolver, ServerAddress, +}; + +pub async fn join_server(address: &ServerAddress) -> Result<(), String> { + let username = "bot".to_string(); + + let resolved_address = resolver::resolve_address(address).await?; + + let mut conn = HandshakeConnection::new(&resolved_address).await?; + + // handshake + conn.write( + ClientIntentionPacket { + protocol_version: PROTOCOL_VERSION, + hostname: address.host.clone(), + port: address.port, + intention: ConnectionProtocol::Login, + } + .get(), + ) + .await; + let mut conn = conn.login(); + + // login start + conn.write(ServerboundHelloPacket { username }.get()).await; + + // encryption request + loop { + match conn.read().await.unwrap() { + LoginPacket::ClientboundHelloPacket(encryption_request_packet) => { + println!( + "Got encryption request {:?} {:?}", + encryption_request_packet.nonce, encryption_request_packet.public_key + ); + } + _ => (), + } + } + + // TODO: client auth + + // TODO: encryption response + + Ok(()) +} diff --git a/azalea-client/src/crypt.rs b/azalea-client/src/crypt.rs new file mode 100644 index 00000000..e69de29b diff --git a/azalea-client/src/lib.rs b/azalea-client/src/lib.rs new file mode 100644 index 00000000..8c1bcfe9 --- /dev/null +++ b/azalea-client/src/lib.rs @@ -0,0 +1,13 @@ +//! Significantly abstract azalea-protocol so it's actually useable for bots. + +pub mod connect; +pub mod ping; + +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + let result = 2 + 2; + assert_eq!(result, 4); + } +} diff --git a/azalea-client/src/ping.rs b/azalea-client/src/ping.rs new file mode 100644 index 00000000..87ccdf66 --- /dev/null +++ b/azalea-client/src/ping.rs @@ -0,0 +1,44 @@ +///! Ping Minecraft servers. +use azalea_protocol::{ + connect::HandshakeConnection, + packets::{ + handshake::client_intention_packet::ClientIntentionPacket, + status::{ + clientbound_status_response_packet::ClientboundStatusResponsePacket, + serverbound_status_request_packet::ServerboundStatusRequestPacket, StatusPacket, + }, + ConnectionProtocol, PROTOCOL_VERSION, + }, + resolver, ServerAddress, +}; + +pub async fn ping_server( + address: &ServerAddress, +) -> Result { + let resolved_address = resolver::resolve_address(address).await?; + + let mut conn = HandshakeConnection::new(&resolved_address).await?; + + // send the client intention packet and switch to the status state + conn.write( + ClientIntentionPacket { + protocol_version: PROTOCOL_VERSION, + hostname: address.host.clone(), + port: address.port, + intention: ConnectionProtocol::Status, + } + .get(), + ) + .await; + let mut conn = conn.status(); + + // send the empty status request packet + conn.write(ServerboundStatusRequestPacket {}.get()).await; + + let packet = conn.read().await.unwrap(); + + match packet { + StatusPacket::ClientboundStatusResponsePacket(p) => Ok(*p), + _ => Err("Invalid packet type".to_string()), + } +} diff --git a/azalea-core/Cargo.toml b/azalea-core/Cargo.toml new file mode 100644 index 00000000..1aa59fe9 --- /dev/null +++ b/azalea-core/Cargo.toml @@ -0,0 +1,9 @@ +[package] +edition = "2021" +name = "azalea-core" +version = "0.1.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +uuid = "^0.8.2" diff --git a/azalea-core/src/lib.rs b/azalea-core/src/lib.rs new file mode 100644 index 00000000..592988a3 --- /dev/null +++ b/azalea-core/src/lib.rs @@ -0,0 +1,12 @@ +//! Random miscellaneous things like UUIDs + +mod serializable_uuid; + +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + let result = 2 + 2; + assert_eq!(result, 4); + } +} diff --git a/azalea-core/src/serializable_uuid.rs b/azalea-core/src/serializable_uuid.rs new file mode 100644 index 00000000..91b5f2d8 --- /dev/null +++ b/azalea-core/src/serializable_uuid.rs @@ -0,0 +1,54 @@ +use uuid::Uuid; + +pub trait SerializableUuid { + fn to_int_array(&self) -> [u32; 4]; + fn from_int_array(array: [u32; 4]) -> Self; +} + +// private static int[] leastMostToIntArray(long l, long l2) { +// return new int[]{(int)(l >> 32), (int)l, (int)(l2 >> 32), (int)l2}; +// } + +fn least_most_to_int_array(most: u64, least: u64) -> [u32; 4] { + [ + (most >> 32) as u32, + most as u32, + (least >> 32) as u32, + least as u32, + ] +} + +impl SerializableUuid for Uuid { + fn to_int_array(&self) -> [u32; 4] { + let most_significant_bits = (self.as_u128() >> 64) as u64; + let least_significant_bits = (self.as_u128() & 0xffffffffffffffff) as u64; + + least_most_to_int_array(most_significant_bits, least_significant_bits) + } + + fn from_int_array(array: [u32; 4]) -> Self { + let most = ((array[0] as u64) << 32) | ((array[1] as u64) & 0xFFFFFFFF); + let least = ((array[2] as u64) << 32) | ((array[3] as u64) & 0xFFFFFFFF); + + Uuid::from_u128((((most as u128) << 64) | least as u128).into()) + } +} + +mod tests { + use super::*; + + #[test] + fn to_int_array() { + let u = Uuid::parse_str("6536bfed-8695-48fd-83a1-ecd24cf2a0fd").unwrap(); + assert_eq!( + u.to_int_array(), + [0x6536bfed, 0x869548fd, 0x83a1ecd2, 0x4cf2a0fd] + ); + } + + #[test] + fn from_int_array() { + let u = Uuid::from_int_array([0x6536bfed, 0x869548fd, 0x83a1ecd2, 0x4cf2a0fd]); + assert_eq!(u.to_string(), "6536bfed-8695-48fd-83a1-ecd24cf2a0fd"); + } +} diff --git a/azalea-protocol/Cargo.toml b/azalea-protocol/Cargo.toml new file mode 100644 index 00000000..2b8a97f2 --- /dev/null +++ b/azalea-protocol/Cargo.toml @@ -0,0 +1,19 @@ +[package] +edition = "2021" +name = "azalea-protocol" +version = "0.1.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +async-recursion = "^0.3.2" +async-trait = "0.1.51" +azalea-chat = {path = "../azalea-chat"} +byteorder = "^1.4.3" +bytes = "^1.1.0" +serde = {version = "1.0.130", features = ["serde_derive"]} +serde_json = "^1.0.72" +thiserror = "^1.0.30" +tokio = {version = "^1.14.0", features = ["io-util", "net", "macros"]} +tokio-util = "^0.6.9" +trust-dns-resolver = "^0.20.3" diff --git a/azalea-protocol/src/connect.rs b/azalea-protocol/src/connect.rs new file mode 100644 index 00000000..f6dd9fe7 --- /dev/null +++ b/azalea-protocol/src/connect.rs @@ -0,0 +1,116 @@ +//! parse sending and receiving packets with a server. + +use crate::packets::game::GamePacket; +use crate::packets::handshake::HandshakePacket; +use crate::packets::login::LoginPacket; +use crate::packets::status::StatusPacket; +use crate::read::read_packet; +use crate::write::write_packet; +use crate::ServerIpAddress; +use tokio::net::TcpStream; + +pub enum PacketFlow { + ClientToServer, + ServerToClient, +} + +pub struct HandshakeConnection { + pub flow: PacketFlow, + /// The buffered writer + pub stream: TcpStream, +} + +pub struct GameConnection { + pub flow: PacketFlow, + /// The buffered writer + pub stream: TcpStream, +} + +pub struct StatusConnection { + pub flow: PacketFlow, + /// The buffered writer + pub stream: TcpStream, +} + +pub struct LoginConnection { + pub flow: PacketFlow, + /// The buffered writer + pub stream: TcpStream, +} + +impl HandshakeConnection { + pub async fn new(address: &ServerIpAddress) -> Result { + let ip = address.ip; + let port = address.port; + + let stream = TcpStream::connect(format!("{}:{}", ip, port)) + .await + .map_err(|_| "Failed to connect to server")?; + + // enable tcp_nodelay + stream + .set_nodelay(true) + .expect("Error enabling tcp_nodelay"); + + Ok(HandshakeConnection { + flow: PacketFlow::ServerToClient, + stream, + }) + } + + pub fn login(self) -> LoginConnection { + LoginConnection { + flow: self.flow, + stream: self.stream, + } + } + + pub fn status(self) -> StatusConnection { + StatusConnection { + flow: self.flow, + stream: self.stream, + } + } + + pub async fn read(&mut self) -> Result { + read_packet::(&self.flow, &mut self.stream).await + } + + /// Write a packet to the server + pub async fn write(&mut self, packet: HandshakePacket) { + write_packet(packet, &mut self.stream).await; + } +} + +impl GameConnection { + pub async fn read(&mut self) -> Result { + read_packet::(&self.flow, &mut self.stream).await + } + + /// Write a packet to the server + pub async fn write(&mut self, packet: GamePacket) { + write_packet(packet, &mut self.stream).await; + } +} + +impl StatusConnection { + pub async fn read(&mut self) -> Result { + read_packet::(&self.flow, &mut self.stream).await + } + + /// Write a packet to the server + pub async fn write(&mut self, packet: StatusPacket) { + write_packet(packet, &mut self.stream).await; + } +} + +impl LoginConnection { + pub async fn read(&mut self) -> Result { + read_packet::(&self.flow, &mut self.stream).await + } + + /// Write a packet to the server + pub async fn write(&mut self, packet: LoginPacket) { + write_packet(packet, &mut self.stream).await; + } +} diff --git a/azalea-protocol/src/lib.rs b/azalea-protocol/src/lib.rs new file mode 100644 index 00000000..684add45 --- /dev/null +++ b/azalea-protocol/src/lib.rs @@ -0,0 +1,56 @@ +//! This lib is responsible for parsing Minecraft packets. + +use std::net::IpAddr; +use std::str::FromStr; + +pub mod connect; +pub mod mc_buf; +pub mod packets; +pub mod read; +pub mod resolver; +pub mod write; + +#[derive(Debug)] +pub struct ServerAddress { + pub host: String, + pub port: u16, +} + +#[derive(Debug)] +pub struct ServerIpAddress { + pub ip: IpAddr, + pub port: u16, +} + +// impl try_from for ServerAddress +impl<'a> TryFrom<&'a 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 }) + } +} + +pub async fn connect(address: ServerAddress) -> Result<(), Box> { + let resolved_address = resolver::resolve_address(&address).await; + println!("Resolved address: {:?}", resolved_address); + Ok(()) +} + +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + let result = 2 + 2; + assert_eq!(result, 4); + } +} diff --git a/azalea-protocol/src/mc_buf.rs b/azalea-protocol/src/mc_buf.rs new file mode 100644 index 00000000..54ba1f7d --- /dev/null +++ b/azalea-protocol/src/mc_buf.rs @@ -0,0 +1,203 @@ +//! Utilities for reading and writing for the Minecraft protocol + +use std::io::Write; + +use async_trait::async_trait; +use byteorder::{BigEndian, WriteBytesExt}; +use tokio::io::{AsyncRead, AsyncReadExt}; + +// const DEFAULT_NBT_QUOTA: u32 = 2097152; +const MAX_STRING_LENGTH: u16 = 32767; +// const MAX_COMPONENT_STRING_LENGTH: u32 = 262144; + +#[async_trait] +pub trait Writable { + fn write_byte(&mut self, n: u8) -> Result<(), std::io::Error>; + fn write_bytes(&mut self, bytes: &[u8]) -> Result<(), std::io::Error>; + fn write_varint(&mut self, value: i32) -> Result<(), std::io::Error>; + fn write_utf_with_len(&mut self, string: &str, len: usize) -> Result<(), std::io::Error>; + fn write_utf(&mut self, string: &str) -> Result<(), std::io::Error>; + fn write_short(&mut self, n: u16) -> Result<(), std::io::Error>; + fn write_byte_array(&mut self, bytes: &[u8]) -> Result<(), std::io::Error>; +} + +#[async_trait] +impl Writable for Vec { + fn write_byte(&mut self, n: u8) -> Result<(), std::io::Error> { + WriteBytesExt::write_u8(self, n) + } + + fn write_bytes(&mut self, bytes: &[u8]) -> Result<(), std::io::Error> { + Ok(self.extend_from_slice(bytes)) + } + + fn write_varint(&mut self, mut value: i32) -> Result<(), std::io::Error> { + let mut buffer = [0]; + if value == 0 { + self.write_all(&buffer).unwrap(); + } + while value != 0 { + buffer[0] = (value & 0b0111_1111) as u8; + value = (value >> 7) & (i32::max_value() >> 6); + if value != 0 { + buffer[0] |= 0b1000_0000; + } + self.write_all(&buffer)?; + } + Ok(()) + } + + fn write_utf_with_len(&mut self, string: &str, len: usize) -> Result<(), std::io::Error> { + if string.len() > len { + panic!( + "String too big (was {} bytes encoded, max {})", + string.len(), + len + ); + } + self.write_varint(string.len() as i32); + self.write_bytes(string.as_bytes()) + } + + fn write_utf(&mut self, string: &str) -> Result<(), std::io::Error> { + self.write_utf_with_len(string, MAX_STRING_LENGTH.into()) + } + + fn write_short(&mut self, n: u16) -> Result<(), std::io::Error> { + WriteBytesExt::write_u16::(self, n) + } + + fn write_byte_array(&mut self, bytes: &[u8]) -> Result<(), std::io::Error> { + self.write_varint(bytes.len() as i32); + self.write_bytes(bytes) + } +} + +#[async_trait] +pub trait Readable { + async fn read_varint(&mut self) -> Result<(i32, u8), String>; + async fn read_byte_array(&mut self) -> Result, String>; + async fn read_bytes(&mut self, n: usize) -> Result, String>; + async fn read_utf(&mut self) -> Result; + async fn read_utf_with_len(&mut self, max_length: u32) -> Result; + async fn read_byte(&mut self) -> Result; +} + +#[async_trait] +impl Readable for R +where + R: AsyncRead + std::marker::Unpin + std::marker::Send, +{ + // fast varints stolen from https://github.com/luojia65/mc-varint/blob/master/src/lib.rs#L67 + /// Read a single varint from the reader and return the value, along with the number of bytes read + async fn read_varint(&mut self) -> Result<(i32, u8), String> { + let mut buffer = [0]; + let mut ans = 0; + for i in 0..4 { + self.read_exact(&mut buffer) + .await + .map_err(|_| "Invalid VarInt".to_string())?; + ans |= ((buffer[0] & 0b0111_1111) as i32) << (7 * i); + if buffer[0] & 0b1000_0000 == 0 { + return Ok((ans, i + 1)); + } + } + Ok((ans, 5)) + } + + async fn read_byte_array(&mut self) -> Result, String> { + let length = self.read_varint().await?.0 as usize; + Ok(self.read_bytes(length).await?) + } + + async fn read_bytes(&mut self, n: usize) -> Result, String> { + let mut bytes = vec![0; n]; + match AsyncReadExt::read_exact(self, &mut bytes).await { + Ok(_) => Ok(bytes), + Err(_) => Err("Error reading bytes".to_string()), + } + } + + async fn read_utf(&mut self) -> Result { + self.read_utf_with_len(MAX_STRING_LENGTH.into()).await + } + + async fn read_utf_with_len(&mut self, max_length: u32) -> Result { + let (length, _length_varint_length) = self.read_varint().await?; + // i don't know why it's multiplied by 4 but it's like that in mojang's code so + if length < 0 { + return Err( + "The received encoded string buffer length is less than zero! Weird string!" + .to_string(), + ); + } + if length as u32 > max_length * 4 { + return Err(format!( + "The received encoded string buffer length is longer than maximum allowed ({} > {})", + length, + max_length * 4 + )); + } + + // this is probably quite inefficient, idk how to do it better + let mut string = String::new(); + let mut buffer = vec![0; length as usize]; + self.read_exact(&mut buffer) + .await + .map_err(|_| "Invalid UTF-8".to_string())?; + + string.push_str(std::str::from_utf8(&buffer).unwrap()); + if string.len() > length as usize { + return Err(format!( + "The received string length is longer than maximum allowed ({} > {})", + length, max_length + )); + } + + Ok(string) + } + + /// Read a single byte from the reader + async fn read_byte(&mut self) -> Result { + match AsyncReadExt::read_u8(self).await { + Ok(r) => Ok(r), + Err(_) => Err("Error reading byte".to_string()), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Cursor; + use tokio::io::BufReader; + + #[test] + fn test_write_varint() { + let mut buf = Vec::new(); + buf.write_varint(123456); + assert_eq!(buf, vec![192, 196, 7]); + + let mut buf = Vec::new(); + buf.write_varint(0); + assert_eq!(buf, vec![0]); + } + + #[tokio::test] + async fn test_read_varint() { + let mut buf = BufReader::new(Cursor::new(vec![192, 196, 7])); + assert_eq!(buf.read_varint().await.unwrap(), (123456, 3)); + + let mut buf = BufReader::new(Cursor::new(vec![0])); + assert_eq!(buf.read_varint().await.unwrap(), (0, 1)); + + let mut buf = BufReader::new(Cursor::new(vec![1])); + assert_eq!(buf.read_varint().await.unwrap(), (1, 1)); + } + + #[tokio::test] + async fn test_read_varint_longer() { + let mut buf = BufReader::new(Cursor::new(vec![138, 56, 0, 135, 56, 123])); + assert_eq!(buf.read_varint().await.unwrap(), (7178, 2)); + } +} diff --git a/azalea-protocol/src/packets/game/mod.rs b/azalea-protocol/src/packets/game/mod.rs new file mode 100644 index 00000000..a3ef2541 --- /dev/null +++ b/azalea-protocol/src/packets/game/mod.rs @@ -0,0 +1,35 @@ +use async_trait::async_trait; +use tokio::io::BufReader; + +use crate::connect::PacketFlow; + +use super::ProtocolPacket; + +#[derive(Clone, Debug)] +pub enum GamePacket +where + Self: Sized, {} + +#[async_trait] +impl ProtocolPacket for GamePacket { + fn id(&self) -> u32 { + 0x00 + } + + fn write(&self, _buf: &mut Vec) {} + + /// Read a packet by its id, ConnectionProtocol, and flow + async fn read( + _id: u32, + flow: &PacketFlow, + _buf: &mut BufReader, + ) -> Result + where + Self: Sized, + { + match flow { + PacketFlow::ServerToClient => Err("HandshakePacket::read not implemented".to_string()), + PacketFlow::ClientToServer => Err("HandshakePacket::read not implemented".to_string()), + } + } +} diff --git a/azalea-protocol/src/packets/handshake/client_intention_packet.rs b/azalea-protocol/src/packets/handshake/client_intention_packet.rs new file mode 100644 index 00000000..868626b3 --- /dev/null +++ b/azalea-protocol/src/packets/handshake/client_intention_packet.rs @@ -0,0 +1,36 @@ +use std::hash::Hash; + +use tokio::io::BufReader; + +use crate::{mc_buf::Writable, packets::ConnectionProtocol}; + +use super::HandshakePacket; + +#[derive(Hash, Clone, Debug)] +pub struct ClientIntentionPacket { + pub protocol_version: u32, + pub hostname: String, + pub port: u16, + /// 1 for status, 2 for login + pub intention: ConnectionProtocol, +} + +impl ClientIntentionPacket { + pub fn get(self) -> HandshakePacket { + HandshakePacket::ClientIntentionPacket(self) + } + + pub fn write(&self, buf: &mut Vec) { + buf.write_varint(self.protocol_version as i32).unwrap(); + buf.write_utf(&self.hostname).unwrap(); + buf.write_short(self.port).unwrap(); + buf.write_varint(self.intention.clone() as i32).unwrap(); + } + + pub async fn read( + _buf: &mut BufReader, + ) -> Result { + Err("ClientIntentionPacket::parse not implemented".to_string()) + // Ok(ClientIntentionPacket {}.get()) + } +} diff --git a/azalea-protocol/src/packets/handshake/mod.rs b/azalea-protocol/src/packets/handshake/mod.rs new file mode 100644 index 00000000..01010e1e --- /dev/null +++ b/azalea-protocol/src/packets/handshake/mod.rs @@ -0,0 +1,49 @@ +pub mod client_intention_packet; + +use async_trait::async_trait; +use tokio::io::BufReader; + +use crate::connect::PacketFlow; + +use super::ProtocolPacket; + +#[derive(Clone, Debug)] +pub enum HandshakePacket +where + Self: Sized, +{ + ClientIntentionPacket(client_intention_packet::ClientIntentionPacket), +} + +#[async_trait] +impl ProtocolPacket for HandshakePacket { + fn id(&self) -> u32 { + match self { + HandshakePacket::ClientIntentionPacket(_packet) => 0x00, + } + } + + fn write(&self, buf: &mut Vec) { + match self { + HandshakePacket::ClientIntentionPacket(packet) => packet.write(buf), + } + } + + /// Read a packet by its id, ConnectionProtocol, and flow + async fn read( + id: u32, + flow: &PacketFlow, + buf: &mut BufReader, + ) -> Result + where + Self: Sized, + { + match flow { + PacketFlow::ServerToClient => Err("HandshakePacket::read not implemented".to_string()), + PacketFlow::ClientToServer => match id { + 0x00 => Ok(client_intention_packet::ClientIntentionPacket::read(buf).await?), + _ => Err(format!("Unknown ClientToServer status packet id: {}", id)), + }, + } + } +} diff --git a/azalea-protocol/src/packets/login/clientbound_custom_query_packet.rs b/azalea-protocol/src/packets/login/clientbound_custom_query_packet.rs new file mode 100644 index 00000000..093176eb --- /dev/null +++ b/azalea-protocol/src/packets/login/clientbound_custom_query_packet.rs @@ -0,0 +1,41 @@ +use std::hash::Hash; +use tokio::io::BufReader; + +use crate::mc_buf::{self, Readable, Writable}; + +use super::LoginPacket; + +#[derive(Hash, Clone, Debug)] +pub struct ClientboundCustomQueryPacket { + pub transaction_id: u32, + // TODO: this should be a resource location + pub identifier: String, + pub data: Vec, +} + +impl ClientboundCustomQueryPacket { + pub fn get(self) -> LoginPacket { + LoginPacket::ClientboundCustomQueryPacket(self) + } + + pub fn write(&self, buf: &mut Vec) { + buf.write_varint(self.transaction_id as i32).unwrap(); + buf.write_utf(&self.identifier).unwrap(); + buf.write_bytes(&self.data).unwrap(); + } + + pub async fn read( + buf: &mut BufReader, + ) -> Result { + let transaction_id = buf.read_varint().await?.0 as u32; + // TODO: this should be a resource location + let identifier = buf.read_utf().await?; + let data = buf.read_bytes(1048576).await?; + Ok(ClientboundCustomQueryPacket { + transaction_id, + identifier, + data, + } + .get()) + } +} diff --git a/azalea-protocol/src/packets/login/clientbound_hello_packet.rs b/azalea-protocol/src/packets/login/clientbound_hello_packet.rs new file mode 100644 index 00000000..36a48706 --- /dev/null +++ b/azalea-protocol/src/packets/login/clientbound_hello_packet.rs @@ -0,0 +1,38 @@ +use std::hash::Hash; +use tokio::io::BufReader; + +use crate::mc_buf::Readable; + +use super::LoginPacket; + +#[derive(Hash, Clone, Debug)] +pub struct ClientboundHelloPacket { + pub server_id: String, + pub public_key: Vec, + pub nonce: Vec, +} + +impl ClientboundHelloPacket { + pub fn get(self) -> LoginPacket { + LoginPacket::ClientboundHelloPacket(self) + } + + pub fn write(&self, _buf: &mut Vec) { + panic!("ClientboundHelloPacket::write not implemented") + } + + pub async fn read( + buf: &mut BufReader, + ) -> Result { + let server_id = buf.read_utf_with_len(20).await?; + let public_key = buf.read_byte_array().await?; + let nonce = buf.read_byte_array().await?; + + Ok(ClientboundHelloPacket { + server_id, + public_key, + nonce, + } + .get()) + } +} diff --git a/azalea-protocol/src/packets/login/mod.rs b/azalea-protocol/src/packets/login/mod.rs new file mode 100644 index 00000000..f0ed6717 --- /dev/null +++ b/azalea-protocol/src/packets/login/mod.rs @@ -0,0 +1,63 @@ +pub mod clientbound_custom_query_packet; +pub mod clientbound_hello_packet; +pub mod serverbound_hello_packet; + +use async_trait::async_trait; +use tokio::io::BufReader; + +use crate::connect::PacketFlow; + +use super::ProtocolPacket; + +#[derive(Clone, Debug)] +pub enum LoginPacket +where + Self: Sized, +{ + ClientboundCustomQueryPacket(clientbound_custom_query_packet::ClientboundCustomQueryPacket), + ServerboundHelloPacket(serverbound_hello_packet::ServerboundHelloPacket), + ClientboundHelloPacket(clientbound_hello_packet::ClientboundHelloPacket), +} + +#[async_trait] +impl ProtocolPacket for LoginPacket { + fn id(&self) -> u32 { + match self { + LoginPacket::ClientboundCustomQueryPacket(_packet) => 0x04, + LoginPacket::ServerboundHelloPacket(_packet) => 0x00, + LoginPacket::ClientboundHelloPacket(_packet) => 0x01, + } + } + + fn write(&self, buf: &mut Vec) { + match self { + LoginPacket::ClientboundCustomQueryPacket(packet) => packet.write(buf), + LoginPacket::ServerboundHelloPacket(packet) => packet.write(buf), + LoginPacket::ClientboundHelloPacket(packet) => packet.write(buf), + } + } + + /// Read a packet by its id, ConnectionProtocol, and flow + async fn read( + id: u32, + flow: &PacketFlow, + buf: &mut BufReader, + ) -> Result + where + Self: Sized, + { + Ok(match flow { + PacketFlow::ServerToClient => match id { + 0x01 => clientbound_hello_packet::ClientboundHelloPacket::read(buf).await?, + 0x04 => { + clientbound_custom_query_packet::ClientboundCustomQueryPacket::read(buf).await? + } + _ => return Err(format!("Unknown ServerToClient status packet id: {}", id)), + }, + PacketFlow::ClientToServer => match id { + 0x00 => serverbound_hello_packet::ServerboundHelloPacket::read(buf).await?, + _ => return Err(format!("Unknown ClientToServer status packet id: {}", id)), + }, + }) + } +} diff --git a/azalea-protocol/src/packets/login/serverbound_hello_packet.rs b/azalea-protocol/src/packets/login/serverbound_hello_packet.rs new file mode 100644 index 00000000..32a6dadc --- /dev/null +++ b/azalea-protocol/src/packets/login/serverbound_hello_packet.rs @@ -0,0 +1,27 @@ +use std::hash::Hash; +use tokio::io::BufReader; + +use crate::mc_buf::Writable; + +use super::LoginPacket; + +#[derive(Hash, Clone, Debug)] +pub struct ServerboundHelloPacket { + pub username: String, +} + +impl ServerboundHelloPacket { + pub fn get(self) -> LoginPacket { + LoginPacket::ServerboundHelloPacket(self) + } + + pub fn write(&self, buf: &mut Vec) { + buf.write_utf(&self.username).unwrap(); + } + + pub async fn read( + _buf: &mut BufReader, + ) -> Result { + Err("ServerboundHelloPacket::read not implemented".to_string()) + } +} diff --git a/azalea-protocol/src/packets/mod.rs b/azalea-protocol/src/packets/mod.rs new file mode 100644 index 00000000..a074b570 --- /dev/null +++ b/azalea-protocol/src/packets/mod.rs @@ -0,0 +1,146 @@ +pub mod game; +pub mod handshake; +pub mod login; +pub mod status; + +use async_trait::async_trait; +use tokio::io::BufReader; + +use crate::connect::PacketFlow; + +pub const PROTOCOL_VERSION: u32 = 757; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum ConnectionProtocol { + Handshake = -1, + Game = 0, + Status = 1, + Login = 2, +} + +#[derive(Clone, Debug)] +pub enum Packet { + Game(game::GamePacket), + Handshake(handshake::HandshakePacket), + Login(login::LoginPacket), + Status(Box), +} + +/// An enum of packets for a certain protocol +#[async_trait] +pub trait ProtocolPacket +where + Self: Sized, +{ + fn id(&self) -> u32; + + /// Read a packet by its id, ConnectionProtocol, and flow + async fn read( + id: u32, + flow: &PacketFlow, + buf: &mut BufReader, + ) -> Result + where + Self: Sized; + + fn write(&self, buf: &mut Vec); +} + +// impl Packet { +// fn get_inner_packet(&self) -> &dyn PacketTrait { +// match self { +// Packet::ClientIntentionPacket(packet) => packet, +// Packet::ServerboundStatusRequestPacket(packet) => packet, +// Packet::ClientboundStatusResponsePacket(packet) => packet, +// Packet::ServerboundHelloPacket(packet) => packet, +// Packet::ClientboundHelloPacket(packet) => packet, +// } +// } + +// pub fn id(&self) -> u32 { +// match self { +// Packet::ClientIntentionPacket(_packet) => 0x00, +// Packet::ServerboundStatusRequestPacket(_packet) => 0x00, +// Packet::ClientboundStatusResponsePacket(_packet) => 0x00, +// Packet::ServerboundHelloPacket(_packet) => 0x00, +// Packet::ClientboundHelloPacket(_packet) => 0x01, +// } +// } + +// /// Read a packet by its id, ConnectionProtocol, and flow +// pub async fn read( +// id: u32, +// protocol: &ConnectionProtocol, +// flow: &PacketFlow, +// buf: &mut BufReader, +// ) -> Result { +// match protocol { +// ConnectionProtocol::Handshake => match flow { +// PacketFlow::ClientToServer => match id { +// 0x00 => Ok( +// handshake::client_intention_packet::ClientIntentionPacket::read(buf).await?, +// ), +// _ => Err(format!("Unknown ClientToServer handshake packet id: {}", id)), +// } +// PacketFlow::ServerToClient => Err("ServerToClient handshake packets not implemented".to_string()), +// }, + +// ConnectionProtocol::Game => Err("Game protocol not implemented yet".to_string()), + +// ConnectionProtocol::Status => match flow { +// PacketFlow::ServerToClient => match id { +// 0x00 => Ok( +// status::clientbound_status_response_packet::ClientboundStatusResponsePacket +// ::read(buf) +// .await?, +// ), +// _ => Err(format!("Unknown ServerToClient status packet id: {}", id)), +// }, +// PacketFlow::ClientToServer => match id { +// 0x00 => Ok( +// status::serverbound_status_request_packet::ServerboundStatusRequestPacket +// ::read(buf) +// .await?, +// ), +// _ => Err(format!("Unknown ClientToServer status packet id: {}", id)), +// }, +// }, + +// ConnectionProtocol::Login => match flow { +// PacketFlow::ServerToClient => match id { +// 0x01 => Ok( +// login::clientbound_hello_packet::ClientboundHelloPacket::read(buf).await?, +// ), +// _ => Err(format!("Unknown ServerToClient login packet id: {}", id)), +// }, +// PacketFlow::ClientToServer => match id { +// 0x00 => Ok( +// login::serverbound_hello_packet::ServerboundHelloPacket::read(buf).await?, +// ), +// _ => Err(format!("Unknown ClientToServer login packet id: {}", id)), +// }, +// }, +// } +// } + +// pub fn write(&self, buf: &mut Vec) { +// self.get_inner_packet().write(buf); +// } +// } + +// #[async_trait] +// pub trait PacketTrait +// where +// Self: Sized, +// { +// /// Return a version of the packet that you can actually use for stuff +// fn get(self) -> dyn ProtocolPacket; + +// fn write(&self, buf: &mut Vec); + +// async fn read( +// buf: &mut BufReader, +// ) -> Result +// where +// Self: Sized; +// } diff --git a/azalea-protocol/src/packets/status/clientbound_status_response_packet.rs b/azalea-protocol/src/packets/status/clientbound_status_response_packet.rs new file mode 100644 index 00000000..1d8a3aa4 --- /dev/null +++ b/azalea-protocol/src/packets/status/clientbound_status_response_packet.rs @@ -0,0 +1,58 @@ +use azalea_chat::component::Component; +use serde::Deserialize; +use serde_json::Value; +use tokio::io::BufReader; + +use crate::mc_buf::Readable; + +use super::StatusPacket; + +#[derive(Clone, Debug, Deserialize)] +pub struct Version { + pub name: Component, + pub protocol: u32, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct SamplePlayer { + pub id: String, + pub name: String, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct Players { + pub max: u32, + pub online: u32, + pub sample: Vec, +} + +// the entire packet is just json, which is why it has deserialize +#[derive(Clone, Debug, Deserialize)] +pub struct ClientboundStatusResponsePacket { + pub description: Component, + pub favicon: Option, + pub players: Players, + pub version: Version, +} + +impl ClientboundStatusResponsePacket { + pub fn get(self) -> StatusPacket { + StatusPacket::ClientboundStatusResponsePacket(Box::new(self)) + } + + pub fn write(&self, _buf: &mut Vec) {} + + pub async fn read( + buf: &mut BufReader, + ) -> Result { + let status_string = buf.read_utf().await?; + let status_json: Value = + serde_json::from_str(status_string.as_str()).expect("Server status isn't valid JSON"); + + let packet = ClientboundStatusResponsePacket::deserialize(status_json) + .map_err(|e| e.to_string())? + .get(); + + Ok(packet) + } +} diff --git a/azalea-protocol/src/packets/status/mod.rs b/azalea-protocol/src/packets/status/mod.rs new file mode 100644 index 00000000..ac6a34e1 --- /dev/null +++ b/azalea-protocol/src/packets/status/mod.rs @@ -0,0 +1,66 @@ +pub mod clientbound_status_response_packet; +pub mod serverbound_status_request_packet; + +use async_trait::async_trait; +use tokio::io::BufReader; + +use crate::connect::PacketFlow; + +use super::ProtocolPacket; + +#[derive(Clone, Debug)] +pub enum StatusPacket +where + Self: Sized, +{ + ServerboundStatusRequestPacket( + serverbound_status_request_packet::ServerboundStatusRequestPacket, + ), + ClientboundStatusResponsePacket( + Box, + ), +} + +#[async_trait] +impl ProtocolPacket for StatusPacket { + fn id(&self) -> u32 { + match self { + StatusPacket::ServerboundStatusRequestPacket(_packet) => 0x00, + StatusPacket::ClientboundStatusResponsePacket(_packet) => 0x00, + } + } + + fn write(&self, buf: &mut Vec) { + match self { + StatusPacket::ServerboundStatusRequestPacket(packet) => packet.write(buf), + StatusPacket::ClientboundStatusResponsePacket(packet) => packet.write(buf), + } + } + + /// Read a packet by its id, ConnectionProtocol, and flow + async fn read( + id: u32, + flow: &PacketFlow, + buf: &mut BufReader, + ) -> Result + where + Self: Sized, + { + match flow { + PacketFlow::ServerToClient => match id { + 0x00 => Ok( + clientbound_status_response_packet::ClientboundStatusResponsePacket::read(buf) + .await?, + ), + _ => Err(format!("Unknown ServerToClient status packet id: {}", id)), + }, + PacketFlow::ClientToServer => match id { + 0x00 => Ok( + serverbound_status_request_packet::ServerboundStatusRequestPacket::read(buf) + .await?, + ), + _ => Err(format!("Unknown ClientToServer status packet id: {}", id)), + }, + } + } +} diff --git a/azalea-protocol/src/packets/status/serverbound_status_request_packet.rs b/azalea-protocol/src/packets/status/serverbound_status_request_packet.rs new file mode 100644 index 00000000..6a58da1f --- /dev/null +++ b/azalea-protocol/src/packets/status/serverbound_status_request_packet.rs @@ -0,0 +1,23 @@ +use std::hash::Hash; +use tokio::io::BufReader; + +use super::StatusPacket; + +#[derive(Hash, Clone, Debug)] +pub struct ServerboundStatusRequestPacket {} + +impl ServerboundStatusRequestPacket { + pub fn get(self) -> StatusPacket { + StatusPacket::ServerboundStatusRequestPacket(self) + } + + pub fn write(&self, _buf: &mut Vec) { + panic!("ServerboundStatusRequestPacket::write not implemented") + } + + pub async fn read( + _buf: &mut BufReader, + ) -> Result { + Err("ServerboundStatusRequestPacket::read not implemented".to_string()) + } +} diff --git a/azalea-protocol/src/read.rs b/azalea-protocol/src/read.rs new file mode 100644 index 00000000..6f242e8b --- /dev/null +++ b/azalea-protocol/src/read.rs @@ -0,0 +1,28 @@ +use tokio::{io::BufReader, net::TcpStream}; + +use crate::{connect::PacketFlow, mc_buf::Readable, packets::ProtocolPacket}; + +pub async fn read_packet( + flow: &PacketFlow, + stream: &mut TcpStream, +) -> Result { + // what this does: + // 1. reads the first 5 bytes, probably only some of this will be used to get the packet length + // 2. how much we should read = packet length - 5 + // 3. read the rest of the packet and add it to the cursor + // 4. figure out what packet this is and parse it + + // the first thing minecraft sends us is the length as a varint, which can be up to 5 bytes long + let mut buf = BufReader::with_capacity(4 * 1024 * 1024, stream); + + let (_packet_size, _packet_size_varint_size) = buf.read_varint().await?; + + // then, minecraft tells us the packet id as a varint + let (packet_id, _packet_id_size) = buf.read_varint().await?; + + // if we recognize the packet id, parse it + + let packet = P::read(packet_id.try_into().unwrap(), flow, &mut buf).await?; + + Ok(packet) +} diff --git a/azalea-protocol/src/resolver.rs b/azalea-protocol/src/resolver.rs new file mode 100644 index 00000000..24687a6e --- /dev/null +++ b/azalea-protocol/src/resolver.rs @@ -0,0 +1,55 @@ +use std::net::IpAddr; + +use crate::{ServerAddress, ServerIpAddress}; +use async_recursion::async_recursion; +use trust_dns_resolver::{ + config::{ResolverConfig, ResolverOpts}, + TokioAsyncResolver, +}; + +/// Resolve a Minecraft server address into an IP address and port. +/// If it's already an IP address, it's returned as-is. +#[async_recursion] +pub async fn resolve_address(address: &ServerAddress) -> Result { + // If the address.host is already in the format of an ip address, return it. + if let Ok(ip) = address.host.parse::() { + return Ok(ServerIpAddress { + ip, + port: address.port, + }); + } + + // we specify Cloudflare instead of the default resolver because trust_dns_resolver has an issue on Windows where it's really slow using the default resolver + let resolver = + TokioAsyncResolver::tokio(ResolverConfig::cloudflare(), ResolverOpts::default()).unwrap(); + + // first, we do a srv lookup for _minecraft._tcp. + let srv_redirect_result = resolver + .srv_lookup(format!("_minecraft._tcp.{}", address.host).as_str()) + .await; + + // if it resolves that means it's a redirect so we call resolve_address again with the new host + if let Ok(redirect_result) = srv_redirect_result { + let redirect_srv = redirect_result + .iter() + .next() + .ok_or_else(|| "No SRV record found".to_string())?; + let redirect_address = ServerAddress { + host: redirect_srv.target().to_utf8(), + port: redirect_srv.port(), + }; + + println!("redirecting to {:?}", redirect_address); + + return resolve_address(&redirect_address).await; + } + + // there's no redirect, try to resolve this as an ip address + let lookup_ip_result = resolver.lookup_ip(address.host.clone()).await; + let lookup_ip = lookup_ip_result.map_err(|_| "No IP found".to_string())?; + + Ok(ServerIpAddress { + ip: lookup_ip.iter().next().unwrap(), + port: address.port, + }) +} diff --git a/azalea-protocol/src/write.rs b/azalea-protocol/src/write.rs new file mode 100644 index 00000000..3d8540eb --- /dev/null +++ b/azalea-protocol/src/write.rs @@ -0,0 +1,27 @@ +use tokio::{io::AsyncWriteExt, net::TcpStream}; + +use crate::{mc_buf::Writable, packets::ProtocolPacket}; + +pub async fn write_packet(packet: impl ProtocolPacket, stream: &mut TcpStream) { + // TODO: implement compression + + // packet structure: + // length (varint) + id (varint) + data + + // write the packet id + let mut id_and_data_buf = vec![]; + id_and_data_buf.write_varint(packet.id() as i32); + packet.write(&mut id_and_data_buf); + + // write the packet data + + // make a new buffer that has the length at the beginning + // and id+data at the end + let mut complete_buf: Vec = Vec::new(); + complete_buf.write_varint(id_and_data_buf.len() as i32); + complete_buf.append(&mut id_and_data_buf); + + // finally, write and flush to the stream + stream.write_all(&complete_buf).await.unwrap(); + stream.flush().await.unwrap(); +} diff --git a/bot/Cargo.toml b/bot/Cargo.toml index f1c89a99..fd6ad067 100644 --- a/bot/Cargo.toml +++ b/bot/Cargo.toml @@ -1,11 +1,11 @@ [package] +edition = "2021" name = "bot" version = "0.1.0" -edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -minecraft-client = { path = "../minecraft-client" } -minecraft-protocol = { path = "../minecraft-protocol" } +azalea-client = {path = "../azalea-client"} +azalea-protocol = {path = "../azalea-protocol"} tokio = "^1.14.0" diff --git a/bot/src/main.rs b/bot/src/main.rs index 19959ce5..9cfd7f9a 100644 --- a/bot/src/main.rs +++ b/bot/src/main.rs @@ -1,4 +1,4 @@ -use minecraft_client::connect::join_server; +use azalea_client::connect::join_server; use tokio::runtime::Runtime; async fn bot() { diff --git a/minecraft-chat/Cargo.toml b/minecraft-chat/Cargo.toml deleted file mode 100644 index aa803864..00000000 --- a/minecraft-chat/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -edition = "2021" -name = "minecraft-chat" -version = "0.1.0" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -lazy_static = "1.4.0" -serde = "^1.0.130" -serde_json = "^1.0.72" diff --git a/minecraft-chat/src/base_component.rs b/minecraft-chat/src/base_component.rs deleted file mode 100644 index fa39a11c..00000000 --- a/minecraft-chat/src/base_component.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::{component::Component, style::Style}; - -#[derive(Clone, Debug)] -pub struct BaseComponent { - // implements mutablecomponent - pub siblings: Vec, - pub style: Style, -} - -impl BaseComponent { - pub fn new() -> Self { - Self { - siblings: Vec::new(), - style: Style::default(), - } - } -} - -impl Default for BaseComponent { - fn default() -> Self { - Self::new() - } -} diff --git a/minecraft-chat/src/component.rs b/minecraft-chat/src/component.rs deleted file mode 100644 index 1ed1f836..00000000 --- a/minecraft-chat/src/component.rs +++ /dev/null @@ -1,262 +0,0 @@ -use serde::{de, Deserialize, Deserializer}; - -use crate::{ - base_component::BaseComponent, - style::{ChatFormatting, Style}, - text_component::TextComponent, - translatable_component::{StringOrComponent, TranslatableComponent}, -}; - -#[derive(Clone, Debug)] -pub enum Component { - Text(TextComponent), - Translatable(TranslatableComponent), -} - -lazy_static! { - pub static ref DEFAULT_STYLE: Style = Style { - color: Some(ChatFormatting::WHITE.try_into().unwrap()), - ..Style::default() - }; -} - -/// A chat component -impl Component { - // TODO: is it possible to use a macro so this doesn't have to be duplicated? - - pub fn get_base_mut(&mut self) -> &mut BaseComponent { - match self { - Self::Text(c) => &mut c.base, - Self::Translatable(c) => &mut c.base, - } - } - - pub fn get_base(&self) -> &BaseComponent { - match self { - Self::Text(c) => &c.base, - Self::Translatable(c) => &c.base, - } - } - - /// Add a component as a sibling of this one - fn append(&mut self, sibling: Component) { - self.get_base_mut().siblings.push(sibling); - } - - /// Get the "separator" component from the json - fn parse_separator(json: &serde_json::Value) -> Result, serde_json::Error> { - if json.get("separator").is_some() { - return Ok(Some(Component::deserialize( - json.get("separator").unwrap(), - )?)); - } - Ok(None) - } - - /// Convert this component into an ansi string - pub fn to_ansi(&self, default_style: Option<&Style>) -> String { - // default the default_style to white if it's not set - let default_style: &Style = default_style.unwrap_or(&DEFAULT_STYLE); - - // this contains the final string will all the ansi escape codes - let mut built_string = String::new(); - // this style will update as we visit components - let mut running_style = Style::default(); - - for component in self.clone().into_iter() { - let component_text = match &component { - Self::Text(c) => &c.text, - Self::Translatable(c) => &c.key, - }; - let component_style = &component.get_base().style; - - let ansi_text = running_style.compare_ansi(component_style, default_style); - built_string.push_str(&ansi_text); - built_string.push_str(component_text); - - running_style.apply(component_style); - } - - if !running_style.is_empty() { - built_string.push_str("\u{1b}[m"); - } - - built_string - } -} - -impl IntoIterator for Component { - /// Recursively call the function for every component in this component - fn into_iter(self) -> Self::IntoIter { - let base = self.get_base(); - let siblings = base.siblings.clone(); - let mut v: Vec = Vec::with_capacity(siblings.len() + 1); - v.push(self); - for sibling in siblings { - v.extend(sibling.into_iter()); - } - - v.into_iter() - } - - type Item = Component; - type IntoIter = std::vec::IntoIter; -} - -impl<'de> Deserialize<'de> for Component { - fn deserialize(de: D) -> Result - where - D: Deserializer<'de>, - { - let json: serde_json::Value = serde::Deserialize::deserialize(de)?; - - // we create a component that we might add siblings to - let mut component: Component; - - // if it's primitive, make it a text component - if !json.is_array() && !json.is_object() { - return Ok(Component::Text(TextComponent::new( - json.as_str().unwrap_or("").to_string(), - ))); - } - // if it's an object, do things with { text } and stuff - else if json.is_object() { - if json.get("text").is_some() { - let text = json.get("text").unwrap().as_str().unwrap_or("").to_string(); - component = Component::Text(TextComponent::new(text)); - } else if json.get("translate").is_some() { - let translate = json.get("translate").unwrap().to_string(); - if json.get("with").is_some() { - let with = json.get("with").unwrap().as_array().unwrap(); - let mut with_array = Vec::with_capacity(with.len()); - for item in with { - // if it's a string component with no styling and no siblings, just add a string to with_array - // otherwise add the component to the array - let c = Component::deserialize(item).map_err(de::Error::custom)?; - if let Component::Text(text_component) = c { - if text_component.base.siblings.is_empty() - && text_component.base.style.is_empty() - { - with_array.push(StringOrComponent::String(text_component.text)); - break; - } - } - with_array.push(StringOrComponent::Component( - Component::deserialize(item).map_err(de::Error::custom)?, - )); - } - component = - Component::Translatable(TranslatableComponent::new(translate, with_array)); - } else { - // if it doesn't have a "with", just have the with_array be empty - component = - Component::Translatable(TranslatableComponent::new(translate, Vec::new())); - } - } else if json.get("score").is_some() { - // object = GsonHelper.getAsJsonObject(jsonObject, "score"); - let score_json = json.get("score").unwrap(); - // if (!object.has("name") || !object.has("objective")) throw new JsonParseException("A score component needs a least a name and an objective"); - // ScoreComponent scoreComponent = new ScoreComponent(GsonHelper.getAsString((JsonObject)object, "name"), GsonHelper.getAsString((JsonObject)object, "objective")); - if score_json.get("name").is_none() || score_json.get("objective").is_none() { - return Err(de::Error::missing_field( - "A score component needs at least a name and an objective", - )); - } - // TODO - return Err(de::Error::custom( - "score text components aren't yet supported", - )); - // component = ScoreComponent - } else if json.get("selector").is_some() { - // } else if (jsonObject.has("selector")) { - // object = this.parseSeparator(type, jsonDeserializationContext, jsonObject); - // SelectorComponent selectorComponent = new SelectorComponent(GsonHelper.getAsString(jsonObject, "selector"), (Optional)object); - - return Err(de::Error::custom( - "selector text components aren't yet supported", - )); - // } else if (jsonObject.has("keybind")) { - // KeybindComponent keybindComponent = new KeybindComponent(GsonHelper.getAsString(jsonObject, "keybind")); - } else if json.get("keybind").is_some() { - return Err(de::Error::custom( - "keybind text components aren't yet supported", - )); - } else { - // } else { - // if (!jsonObject.has("nbt")) throw new JsonParseException("Don't know how to turn " + jsonElement + " into a Component"); - if json.get("nbt").is_none() { - return Err(de::Error::custom( - format!("Don't know how to turn {} into a Component", json).as_str(), - )); - } - // object = GsonHelper.getAsString(jsonObject, "nbt"); - let _nbt = json.get("nbt").unwrap().to_string(); - // Optional optional = this.parseSeparator(type, jsonDeserializationContext, jsonObject); - let _separator = Component::parse_separator(&json).map_err(de::Error::custom)?; - - let _interpret = match json.get("interpret") { - Some(v) => v.as_bool().ok_or(Some(false)).unwrap(), - None => false, - }; - // boolean bl = GsonHelper.getAsBoolean(jsonObject, "interpret", false); - // if (jsonObject.has("block")) { - if json.get("block").is_some() {} - return Err(de::Error::custom( - "nbt text components aren't yet supported", - )); - // NbtComponent.BlockNbtComponent blockNbtComponent = new NbtComponent.BlockNbtComponent((String)object, bl, GsonHelper.getAsString(jsonObject, "block"), optional); - // } else if (jsonObject.has("entity")) { - // NbtComponent.EntityNbtComponent entityNbtComponent = new NbtComponent.EntityNbtComponent((String)object, bl, GsonHelper.getAsString(jsonObject, "entity"), optional); - // } else { - // if (!jsonObject.has("storage")) throw new JsonParseException("Don't know how to turn " + jsonElement + " into a Component"); - // NbtComponent.StorageNbtComponent storageNbtComponent = new NbtComponent.StorageNbtComponent((String)object, bl, new ResourceLocation(GsonHelper.getAsString(jsonObject, "storage")), optional); - // } - // } - } - // if (jsonObject.has("extra")) { - // object = GsonHelper.getAsJsonArray(jsonObject, "extra"); - // if (object.size() <= 0) throw new JsonParseException("Unexpected empty array of components"); - // for (int i = 0; i < object.size(); ++i) { - // var5_17.append(this.deserialize(object.get(i), type, jsonDeserializationContext)); - // } - // } - // var5_17.setStyle((Style)jsonDeserializationContext.deserialize(jsonElement, Style.class)); - // return var5_17; - // } - if json.get("extra").is_some() { - let extra = match json.get("extra").unwrap().as_array() { - Some(r) => r, - None => return Err(de::Error::custom("Extra isn't an array")), - }; - if extra.is_empty() { - return Err(de::Error::custom("Unexpected empty array of components")); - } - for extra_component in extra { - let sibling = - Component::deserialize(extra_component).map_err(de::Error::custom)?; - component.append(sibling); - } - } - - let style = Style::deserialize(&json); - component.get_base_mut().style = style; - - return Ok(component); - } - // ok so it's not an object, if it's an array deserialize every item - else if !json.is_array() { - return Err(de::Error::custom( - format!("Don't know how to turn {} into a Component", json).as_str(), - )); - } - let json_array = json.as_array().unwrap(); - // the first item in the array is the one that we're gonna return, the others are siblings - let mut component = Component::deserialize(&json_array[0]).map_err(de::Error::custom)?; - for i in 1..json_array.len() { - component.append( - Component::deserialize(json_array.get(i).unwrap()).map_err(de::Error::custom)?, - ); - } - Ok(component) - } -} diff --git a/minecraft-chat/src/events.rs b/minecraft-chat/src/events.rs deleted file mode 100644 index a547169e..00000000 --- a/minecraft-chat/src/events.rs +++ /dev/null @@ -1,26 +0,0 @@ -enum ClickAction { - OPEN_URL = Action::new("open_url", true), - OPEN_FILE = Action::new("open_file", false), - RUN_COMMAND = Action::new("run_command", true), - SUGGEST_COMMAND = Action::new("suggest_command", true), - CHANGE_PAGE = Action::new("change_page", true), - COPY_TO_CLIPBOARD = Action::new("copy_to_clipboard", true), -} - -struct ClickAction { - pub name: String, - pub allow_from_server: bool, -} - -impl ClickAction { - fn new(name: &str, allow_from_server: bool) -> Self { - Self { - name: name.to_string(), - allow_from_server, - } - } -} - -struct ClickEvent { - action: ClickAction, -} diff --git a/minecraft-chat/src/lib.rs b/minecraft-chat/src/lib.rs deleted file mode 100644 index b7035e13..00000000 --- a/minecraft-chat/src/lib.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! Things for working with Minecraft chat messages. -//! This was inspired by Minecraft and prismarine-chat. - -#[macro_use] -extern crate lazy_static; - -pub mod base_component; -pub mod component; -pub mod style; -pub mod text_component; -pub mod translatable_component; diff --git a/minecraft-chat/src/style.rs b/minecraft-chat/src/style.rs deleted file mode 100644 index 4e3b24de..00000000 --- a/minecraft-chat/src/style.rs +++ /dev/null @@ -1,478 +0,0 @@ -use std::{collections::HashMap, fmt}; - -use serde_json::Value; - -#[derive(Clone, PartialEq, Debug)] -pub struct TextColor { - pub value: u32, - pub name: Option, -} - -impl TextColor { - pub fn parse(value: String) -> Result { - if value.starts_with('#') { - let n = value.chars().skip(1).collect::(); - let n = u32::from_str_radix(&n, 16).unwrap(); - return Ok(TextColor::from_rgb(n)); - } - let color_option = NAMED_COLORS.get(&value.to_ascii_uppercase()); - if let Some(color) = color_option { - return Ok(color.clone()); - } - Err(format!("Invalid color {}", value)) - } - - fn from_rgb(value: u32) -> TextColor { - TextColor { value, name: None } - } -} - -lazy_static! { - static ref LEGACY_FORMAT_TO_COLOR: HashMap<&'static ChatFormatting<'static>, TextColor> = { - let mut legacy_format_to_color = HashMap::new(); - for formatter in &ChatFormatting::FORMATTERS { - if !formatter.is_format && *formatter != ChatFormatting::RESET { - legacy_format_to_color.insert( - formatter, - TextColor { - value: formatter.color.unwrap(), - name: Some(formatter.name.to_string()), - }, - ); - } - } - legacy_format_to_color - }; - static ref NAMED_COLORS: HashMap = { - let mut named_colors = HashMap::new(); - for color in LEGACY_FORMAT_TO_COLOR.values() { - named_colors.insert(color.name.clone().unwrap(), color.clone()); - } - named_colors - }; -} - -#[derive(Clone, PartialEq, Eq, Hash, Debug)] -pub struct ChatFormatting<'a> { - pub name: &'a str, - pub code: char, - pub is_format: bool, - pub id: i32, - pub color: Option, -} - -pub struct Ansi {} -impl Ansi { - pub const BOLD: &'static str = "\u{1b}[1m"; - pub const ITALIC: &'static str = "\u{1b}[3m"; - pub const UNDERLINED: &'static str = "\u{1b}[4m"; - pub const STRIKETHROUGH: &'static str = "\u{1b}[9m"; - pub const OBFUSCATED: &'static str = "\u{1b}[8m"; - pub const RESET: &'static str = "\u{1b}[m"; - - pub fn rgb(value: u32) -> String { - format!( - "\u{1b}[38;2;{};{};{}m", - (value >> 16) & 0xFF, - (value >> 8) & 0xFF, - value & 0xFF - ) - } -} - -impl<'a> ChatFormatting<'a> { - pub const BLACK: ChatFormatting<'a> = ChatFormatting::new("BLACK", '0', false, 0, Some(0)); - pub const DARK_BLUE: ChatFormatting<'a> = - ChatFormatting::new("DARK_BLUE", '1', false, 1, Some(170)); - pub const DARK_GREEN: ChatFormatting<'a> = - ChatFormatting::new("DARK_GREEN", '2', false, 2, Some(43520)); - pub const DARK_AQUA: ChatFormatting<'a> = - ChatFormatting::new("DARK_AQUA", '3', false, 3, Some(43690)); - pub const DARK_RED: ChatFormatting<'a> = - ChatFormatting::new("DARK_RED", '4', false, 4, Some(1114112)); - pub const DARK_PURPLE: ChatFormatting<'a> = - ChatFormatting::new("DARK_PURPLE", '5', false, 5, Some(11141290)); - pub const GOLD: ChatFormatting<'a> = ChatFormatting::new("GOLD", '6', false, 6, Some(16755200)); - pub const GRAY: ChatFormatting<'a> = ChatFormatting::new("GRAY", '7', false, 7, Some(11184810)); - pub const DARK_GRAY: ChatFormatting<'a> = - ChatFormatting::new("DARK_GRAY", '8', false, 8, Some(5592405)); - pub const BLUE: ChatFormatting<'a> = ChatFormatting::new("BLUE", '9', false, 9, Some(5592575)); - pub const GREEN: ChatFormatting<'a> = - ChatFormatting::new("GREEN", 'a', false, 10, Some(5635925)); - pub const AQUA: ChatFormatting<'a> = ChatFormatting::new("AQUA", 'b', false, 11, Some(5636095)); - pub const RED: ChatFormatting<'a> = ChatFormatting::new("RED", 'c', false, 12, Some(16733525)); - pub const LIGHT_PURPLE: ChatFormatting<'a> = - ChatFormatting::new("LIGHT_PURPLE", 'd', false, 13, Some(16733695)); - pub const YELLOW: ChatFormatting<'a> = - ChatFormatting::new("YELLOW", 'e', false, 14, Some(16777045)); - pub const WHITE: ChatFormatting<'a> = - ChatFormatting::new("WHITE", 'f', false, 15, Some(16777215)); - pub const OBFUSCATED: ChatFormatting<'a> = - ChatFormatting::new("OBFUSCATED", 'k', true, -1, None); - pub const STRIKETHROUGH: ChatFormatting<'a> = - ChatFormatting::new("STRIKETHROUGH", 'm', true, -1, None); - pub const BOLD: ChatFormatting<'a> = ChatFormatting::new("BOLD", 'l', true, -1, None); - pub const UNDERLINE: ChatFormatting<'a> = ChatFormatting::new("UNDERLINE", 'n', true, -1, None); - pub const ITALIC: ChatFormatting<'a> = ChatFormatting::new("ITALIC", 'o', true, -1, None); - pub const RESET: ChatFormatting<'a> = ChatFormatting::new("RESET", 'r', true, -1, None); - - pub const FORMATTERS: [ChatFormatting<'a>; 22] = [ - ChatFormatting::BLACK, - ChatFormatting::DARK_BLUE, - ChatFormatting::DARK_GREEN, - ChatFormatting::DARK_AQUA, - ChatFormatting::DARK_RED, - ChatFormatting::DARK_PURPLE, - ChatFormatting::GOLD, - ChatFormatting::GRAY, - ChatFormatting::DARK_GRAY, - ChatFormatting::BLUE, - ChatFormatting::GREEN, - ChatFormatting::AQUA, - ChatFormatting::RED, - ChatFormatting::LIGHT_PURPLE, - ChatFormatting::YELLOW, - ChatFormatting::WHITE, - ChatFormatting::OBFUSCATED, - ChatFormatting::STRIKETHROUGH, - ChatFormatting::BOLD, - ChatFormatting::UNDERLINE, - ChatFormatting::ITALIC, - ChatFormatting::RESET, - ]; - - const fn new( - name: &str, - code: char, - is_format: bool, - id: i32, - color: Option, - ) -> ChatFormatting { - ChatFormatting { - name, - code, - is_format, - id, - color, - } - } - - pub fn from_code(code: char) -> Result<&'static ChatFormatting<'static>, String> { - for formatter in &ChatFormatting::FORMATTERS { - if formatter.code == code { - return Ok(formatter); - } - } - Err(format!("Invalid formatting code {}", code)) - } -} - -impl TextColor { - fn new(value: u32, name: Option) -> Self { - Self { value, name } - } - - pub fn format(&self) -> String { - format!("#{:06X}", self.value) - } -} - -impl fmt::Display for TextColor { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if let Some(name) = &self.name { - write!(f, "{}", name.clone()) - } else { - write!(f, "{}", self.format()) - } - } -} - -// from ChatFormatting to TextColor -impl TryFrom> for TextColor { - type Error = String; - - fn try_from(formatter: ChatFormatting<'_>) -> Result { - if formatter.is_format { - return Err(format!("{} is not a color", formatter.name)); - } - let color = formatter.color.unwrap_or(0); - Ok(Self::new(color, Some(formatter.name.to_string()))) - } -} - -#[derive(Clone, Debug)] -pub struct Style { - // these are options instead of just bools because None is different than false in this case - pub color: Option, - pub bold: Option, - pub italic: Option, - pub underlined: Option, - pub strikethrough: Option, - pub obfuscated: Option, - /// Whether it should reset the formatting before applying these styles - pub reset: bool, -} - -impl Style { - pub fn default() -> Self { - Self::empty() - } - - pub fn empty() -> Self { - Self { - color: None, - bold: None, - italic: None, - underlined: None, - strikethrough: None, - obfuscated: None, - reset: false, - } - } - - pub fn deserialize(json: &Value) -> Style { - return if json.is_object() { - let json_object = json.as_object().unwrap(); - let bold = json_object.get("bold").and_then(|v| v.as_bool()); - let italic = json_object.get("italic").and_then(|v| v.as_bool()); - let underlined = json_object.get("underlined").and_then(|v| v.as_bool()); - let strikethrough = json_object.get("strikethrough").and_then(|v| v.as_bool()); - let obfuscated = json_object.get("obfuscated").and_then(|v| v.as_bool()); - let color: Option = json_object - .get("color") - .and_then(|v| v.as_str()) - .and_then(|v| TextColor::parse(v.to_string()).ok()); - Style { - color, - bold, - italic, - underlined, - strikethrough, - obfuscated, - ..Style::default() - } - } else { - Style::default() - }; - } - - /// Check if a style has no attributes set - pub fn is_empty(&self) -> bool { - self.color.is_none() - && self.bold.is_none() - && self.italic.is_none() - && self.underlined.is_none() - && self.strikethrough.is_none() - && self.obfuscated.is_none() - } - - /// find the necessary ansi code to get from this style to another - pub fn compare_ansi(&self, after: &Style, default_style: &Style) -> String { - let should_reset = after.reset || - // if it used to be bold and now it's not, reset - (self.bold.unwrap_or(false) && !after.bold.unwrap_or(true)) || - // if it used to be italic and now it's not, reset - (self.italic.unwrap_or(false) && !after.italic.unwrap_or(true)) || - // if it used to be underlined and now it's not, reset - (self.underlined.unwrap_or(false) && !after.underlined.unwrap_or(true)) || - // if it used to be strikethrough and now it's not, reset - (self.strikethrough.unwrap_or(false) && !after.strikethrough.unwrap_or(true)) || - // if it used to be obfuscated and now it's not, reset - (self.obfuscated.unwrap_or(false) && !after.obfuscated.unwrap_or(true)); - - let mut ansi_codes = String::new(); - - let empty_style = Style::empty(); - - let (before, after) = if should_reset { - ansi_codes.push_str(Ansi::RESET); - let mut updated_after = if after.reset { - default_style.clone() - } else { - self.clone() - }; - updated_after.apply(after); - (&empty_style, updated_after) - } else { - (self, after.clone()) - }; - - // if bold used to be false/default and now it's true, set bold - if !before.bold.unwrap_or(false) && after.bold.unwrap_or(false) { - ansi_codes.push_str(Ansi::BOLD); - } - // if italic used to be false/default and now it's true, set italic - if !before.italic.unwrap_or(false) && after.italic.unwrap_or(false) { - ansi_codes.push_str(Ansi::ITALIC); - } - // if underlined used to be false/default and now it's true, set underlined - if !before.underlined.unwrap_or(false) && after.underlined.unwrap_or(false) { - ansi_codes.push_str(Ansi::UNDERLINED); - } - // if strikethrough used to be false/default and now it's true, set strikethrough - if !before.strikethrough.unwrap_or(false) && after.strikethrough.unwrap_or(false) { - ansi_codes.push_str(Ansi::STRIKETHROUGH); - } - // if obfuscated used to be false/default and now it's true, set obfuscated - if !before.obfuscated.unwrap_or(false) && after.obfuscated.unwrap_or(false) { - ansi_codes.push_str(Ansi::OBFUSCATED); - } - - // if the new color is different and not none, set color - let color_changed = { - if before.color.is_none() && after.color.is_some() { - true - } else if before.color.is_some() && after.color.is_some() { - before.color.clone().unwrap().value != after.color.as_ref().unwrap().value - } else { - false - } - }; - - if color_changed { - let after_color = after.color.as_ref().unwrap(); - ansi_codes.push_str(&Ansi::rgb(after_color.value)); - } - - ansi_codes - } - - /// Apply another style to this one - pub fn apply(&mut self, style: &Style) { - if let Some(color) = &style.color { - self.color = Some(color.clone()); - } - if let Some(bold) = &style.bold { - self.bold = Some(*bold); - } - if let Some(italic) = &style.italic { - self.italic = Some(*italic); - } - if let Some(underlined) = &style.underlined { - self.underlined = Some(*underlined); - } - if let Some(strikethrough) = &style.strikethrough { - self.strikethrough = Some(*strikethrough); - } - if let Some(obfuscated) = &style.obfuscated { - self.obfuscated = Some(*obfuscated); - } - } - - /// Apply a ChatFormatting to this style - pub fn apply_formatting(&mut self, formatting: &ChatFormatting) { - match *formatting { - ChatFormatting::BOLD => self.bold = Some(true), - ChatFormatting::ITALIC => self.italic = Some(true), - ChatFormatting::UNDERLINE => self.underlined = Some(true), - ChatFormatting::STRIKETHROUGH => self.strikethrough = Some(true), - ChatFormatting::OBFUSCATED => self.obfuscated = Some(true), - ChatFormatting::RESET => self.reset = true, - ChatFormatting { - name: _, - code: _, - is_format: _, - id: _, - color, - } => { - // if it's a color, set it - if let Some(color) = color { - self.color = Some(TextColor::from_rgb(color)); - } - } - } - } -} - -#[cfg(test)] -mod tests { - use crate::component::DEFAULT_STYLE; - - use super::*; - - #[test] - fn text_color_named_colors() { - assert_eq!(TextColor::parse("red".to_string()).unwrap().value, 16733525); - } - #[test] - fn text_color_hex_colors() { - assert_eq!( - TextColor::parse("#a1b2c3".to_string()).unwrap().value, - 10597059 - ); - } - - #[test] - fn ansi_difference_should_reset() { - let style_a = Style { - bold: Some(true), - italic: Some(true), - ..Style::default() - }; - let style_b = Style { - bold: Some(false), - ..Style::default() - }; - let ansi_difference = style_a.compare_ansi(&style_b, &Style::default()); - assert_eq!( - ansi_difference, - format!( - "{reset}{italic}", - reset = Ansi::RESET, - italic = Ansi::ITALIC - ) - ) - } - #[test] - fn ansi_difference_shouldnt_reset() { - let style_a = Style { - bold: Some(true), - ..Style::default() - }; - let style_b = Style { - italic: Some(true), - ..Style::default() - }; - let ansi_difference = style_a.compare_ansi(&style_b, &Style::default()); - assert_eq!(ansi_difference, Ansi::ITALIC) - } - - #[test] - fn ansi_difference_explicit_reset() { - let style_a = Style { - bold: Some(true), - ..Style::empty() - }; - let style_b = Style { - italic: Some(true), - reset: true, - ..Style::empty() - }; - let ansi_difference = style_a.compare_ansi(&style_b, &DEFAULT_STYLE); - assert_eq!( - ansi_difference, - format!( - "{reset}{italic}{white}", - reset = Ansi::RESET, - white = Ansi::rgb(ChatFormatting::WHITE.color.unwrap()), - italic = Ansi::ITALIC - ) - ) - } - - #[test] - fn test_from_code() { - assert_eq!( - ChatFormatting::from_code('a').unwrap(), - &ChatFormatting::GREEN - ); - } - - #[test] - fn test_apply_formatting() { - let mut style = Style::default(); - style.apply_formatting(&ChatFormatting::BOLD); - style.apply_formatting(&ChatFormatting::RED); - assert_eq!(style.color, Some(TextColor::from_rgb(16733525))); - } -} diff --git a/minecraft-chat/src/text_component.rs b/minecraft-chat/src/text_component.rs deleted file mode 100644 index 6c43f8b7..00000000 --- a/minecraft-chat/src/text_component.rs +++ /dev/null @@ -1,122 +0,0 @@ -use std::fmt; - -use crate::{base_component::BaseComponent, component::Component, style::ChatFormatting}; - -#[derive(Clone, Debug)] -pub struct TextComponent { - pub base: BaseComponent, - pub text: String, -} - -const LEGACY_FORMATTING_CODE_SYMBOL: char = '§'; - -/// Convert a legacy color code string into a Component -/// Technically in Minecraft this is done when displaying the text, but AFAIK it's the same as just doing it in TextComponent -pub fn legacy_color_code_to_text_component(legacy_color_code: &str) -> TextComponent { - let mut components: Vec = Vec::with_capacity(1); - // iterate over legacy_color_code, if it starts with LEGACY_COLOR_CODE_SYMBOL then read the next character and get the style from that - // otherwise, add the character to the text - - // we don't use a normal for loop since we need to be able to skip after reading the formatter code symbol - let mut i = 0; - while i < legacy_color_code.chars().count() { - if legacy_color_code.chars().nth(i).unwrap() == LEGACY_FORMATTING_CODE_SYMBOL { - let formatting_code = legacy_color_code.chars().nth(i + 1).unwrap(); - if let Ok(formatter) = ChatFormatting::from_code(formatting_code) { - if components.is_empty() || !components.last().unwrap().text.is_empty() { - components.push(TextComponent::new("".to_string())); - } - - let style = &mut components.last_mut().unwrap().base.style; - // if the formatter is a reset, then we need to reset the style to the default - style.apply_formatting(formatter); - } - i += 1; - } else { - if components.is_empty() { - components.push(TextComponent::new("".to_string())); - } - components - .last_mut() - .unwrap() - .text - .push(legacy_color_code.chars().nth(i).unwrap()); - }; - i += 1; - } - - // create the final component by using the first one as the base, and then adding the rest as siblings - let mut final_component = components.remove(0); - for component in components { - final_component.base.siblings.push(component.get()); - } - - final_component -} - -impl<'a> TextComponent { - pub fn new(text: String) -> Self { - // if it contains a LEGACY_FORMATTING_CODE_SYMBOL, format it - if text.contains(LEGACY_FORMATTING_CODE_SYMBOL) { - legacy_color_code_to_text_component(&text) - } else { - Self { - base: BaseComponent::new(), - text, - } - } - } - - fn get(self) -> Component { - Component::Text(self) - } -} - -impl fmt::Display for TextComponent { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.text.clone()) - } -} - -#[cfg(test)] -mod tests { - use crate::style::Ansi; - - use super::*; - - #[test] - fn test_hypixel_motd() { - let component = - TextComponent::new("§aHypixel Network §c[1.8-1.18]\n§b§lHAPPY HOLIDAYS".to_string()) - .get(); - assert_eq!( - component.to_ansi(None), - format!( - "{GREEN}Hypixel Network {RED}[1.8-1.18]\n{BOLD}{AQUA}HAPPY HOLIDAYS{RESET}", - GREEN = Ansi::rgb(ChatFormatting::GREEN.color.unwrap()), - RED = Ansi::rgb(ChatFormatting::RED.color.unwrap()), - AQUA = Ansi::rgb(ChatFormatting::AQUA.color.unwrap()), - BOLD = Ansi::BOLD, - RESET = Ansi::RESET - ) - ); - } - - #[test] - fn test_legacy_color_code_to_component() { - let component = TextComponent::new("§lHello §r§1w§2o§3r§4l§5d".to_string()).get(); - assert_eq!( - component.to_ansi(None), - format!( - "{BOLD}Hello {RESET}{DARK_BLUE}w{DARK_GREEN}o{DARK_AQUA}r{DARK_RED}l{DARK_PURPLE}d{RESET}", - BOLD = Ansi::BOLD, - RESET = Ansi::RESET, - DARK_BLUE = Ansi::rgb(ChatFormatting::DARK_BLUE.color.unwrap()), - DARK_GREEN = Ansi::rgb(ChatFormatting::DARK_GREEN.color.unwrap()), - DARK_AQUA = Ansi::rgb(ChatFormatting::DARK_AQUA.color.unwrap()), - DARK_RED = Ansi::rgb(ChatFormatting::DARK_RED.color.unwrap()), - DARK_PURPLE = Ansi::rgb(ChatFormatting::DARK_PURPLE.color.unwrap()) - ) - ); - } -} diff --git a/minecraft-chat/src/translatable_component.rs b/minecraft-chat/src/translatable_component.rs deleted file mode 100644 index 0709f7bf..00000000 --- a/minecraft-chat/src/translatable_component.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::{base_component::BaseComponent, component::Component}; - -#[derive(Clone, Debug)] -pub enum StringOrComponent { - String(String), - Component(Component), -} - -#[derive(Clone, Debug)] -pub struct TranslatableComponent { - pub base: BaseComponent, - pub key: String, - pub args: Vec, -} - -impl TranslatableComponent { - pub fn new(key: String, args: Vec) -> Self { - Self { - base: BaseComponent::new(), - key, - args, - } - } -} diff --git a/minecraft-chat/tests/integration_test.rs b/minecraft-chat/tests/integration_test.rs deleted file mode 100644 index aac12875..00000000 --- a/minecraft-chat/tests/integration_test.rs +++ /dev/null @@ -1,75 +0,0 @@ -use minecraft_chat::{ - component::Component, - style::{Ansi, ChatFormatting, TextColor}, -}; -use serde::Deserialize; -use serde_json::Value; - -#[test] -fn basic_ansi_test() { - let j: Value = serde_json::from_str( - r#"{ - "text": "hello", - "color": "red", - "bold": true -}"#, - ) - .unwrap(); - let component = Component::deserialize(&j).unwrap(); - assert_eq!( - component.to_ansi(None), - "\u{1b}[1m\u{1b}[38;2;255;85;85mhello\u{1b}[m" - ); -} - -#[test] -fn complex_ansi_test() { - let j: Value = serde_json::from_str( - r##"[ - { - "text": "hello", - "color": "red", - "bold": true, - "italic": true, - "underlined": true, - "adsfsf": "this should be ignored", - "extra": [ - {"text": " ", "underlined": false}, - {"text": "world", "bold": false, "strikethrough": true, "color": "#abcdef"} - ] - }, - { - "text": " asdf", - "italic": false, - "obfuscated": "true", - "strikethrough": false - }, - { - "text": "!", - "bold": true - } -]"##, - ) - .unwrap(); - let component = Component::deserialize(&j).unwrap(); - assert_eq!( - component.to_ansi(None), - format!( - "{bold}{italic}{underlined}{red}hello{reset}{bold}{italic}{red} {reset}{italic}{strikethrough}{abcdef}world{reset}{abcdef} asdf{bold}!{reset}", - bold = Ansi::BOLD, - italic = Ansi::ITALIC, - underlined = Ansi::UNDERLINED, - red = Ansi::rgb(ChatFormatting::RED.color.unwrap()), - reset = Ansi::RESET, - strikethrough = Ansi::STRIKETHROUGH, - abcdef = Ansi::rgb(TextColor::parse("#abcdef".to_string()).unwrap().value), - ) - ); -} - -#[test] -fn component_from_string() { - let j: Value = serde_json::from_str("\"foo\"").unwrap(); - let component = Component::deserialize(&j).unwrap(); - assert_eq!(component.to_ansi(None), "foo"); -} diff --git a/minecraft-client/Cargo.toml b/minecraft-client/Cargo.toml deleted file mode 100644 index 10a0ecbf..00000000 --- a/minecraft-client/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "minecraft-client" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -minecraft-protocol = { path = "../minecraft-protocol" } \ No newline at end of file diff --git a/minecraft-client/src/connect.rs b/minecraft-client/src/connect.rs deleted file mode 100644 index 5eedbf96..00000000 --- a/minecraft-client/src/connect.rs +++ /dev/null @@ -1,53 +0,0 @@ -///! Connect to Minecraft servers. -use minecraft_protocol::{ - connect::HandshakeConnection, - packets::{ - handshake::client_intention_packet::ClientIntentionPacket, - login::{serverbound_hello_packet::ServerboundHelloPacket, LoginPacket}, - ConnectionProtocol, PROTOCOL_VERSION, - }, - resolver, ServerAddress, -}; - -pub async fn join_server(address: &ServerAddress) -> Result<(), String> { - let username = "bot".to_string(); - - let resolved_address = resolver::resolve_address(address).await?; - - let mut conn = HandshakeConnection::new(&resolved_address).await?; - - // handshake - conn.write( - ClientIntentionPacket { - protocol_version: PROTOCOL_VERSION, - hostname: address.host.clone(), - port: address.port, - intention: ConnectionProtocol::Login, - } - .get(), - ) - .await; - let mut conn = conn.login(); - - // login start - conn.write(ServerboundHelloPacket { username }.get()).await; - - // encryption request - loop { - match conn.read().await.unwrap() { - LoginPacket::ClientboundHelloPacket(encryption_request_packet) => { - println!( - "Got encryption request {:?} {:?}", - encryption_request_packet.nonce, encryption_request_packet.public_key - ); - } - _ => (), - } - } - - // TODO: client auth - - // TODO: encryption response - - Ok(()) -} diff --git a/minecraft-client/src/crypt.rs b/minecraft-client/src/crypt.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/minecraft-client/src/lib.rs b/minecraft-client/src/lib.rs deleted file mode 100644 index d4ceebde..00000000 --- a/minecraft-client/src/lib.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! Significantly abstract minecraft-protocol so it's actually useable for bots. - -pub mod connect; -pub mod ping; - -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - let result = 2 + 2; - assert_eq!(result, 4); - } -} diff --git a/minecraft-client/src/ping.rs b/minecraft-client/src/ping.rs deleted file mode 100644 index ff869d4e..00000000 --- a/minecraft-client/src/ping.rs +++ /dev/null @@ -1,44 +0,0 @@ -///! Ping Minecraft servers. -use minecraft_protocol::{ - connect::HandshakeConnection, - packets::{ - handshake::client_intention_packet::ClientIntentionPacket, - status::{ - clientbound_status_response_packet::ClientboundStatusResponsePacket, - serverbound_status_request_packet::ServerboundStatusRequestPacket, StatusPacket, - }, - ConnectionProtocol, PROTOCOL_VERSION, - }, - resolver, ServerAddress, -}; - -pub async fn ping_server( - address: &ServerAddress, -) -> Result { - let resolved_address = resolver::resolve_address(address).await?; - - let mut conn = HandshakeConnection::new(&resolved_address).await?; - - // send the client intention packet and switch to the status state - conn.write( - ClientIntentionPacket { - protocol_version: PROTOCOL_VERSION, - hostname: address.host.clone(), - port: address.port, - intention: ConnectionProtocol::Status, - } - .get(), - ) - .await; - let mut conn = conn.status(); - - // send the empty status request packet - conn.write(ServerboundStatusRequestPacket {}.get()).await; - - let packet = conn.read().await.unwrap(); - - match packet { - StatusPacket::ClientboundStatusResponsePacket(p) => Ok(*p), - _ => Err("Invalid packet type".to_string()), - } -} diff --git a/minecraft-core/Cargo.toml b/minecraft-core/Cargo.toml deleted file mode 100644 index e36cc03e..00000000 --- a/minecraft-core/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -edition = "2021" -name = "minecraft-core" -version = "0.1.0" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -uuid = "^0.8.2" diff --git a/minecraft-core/src/lib.rs b/minecraft-core/src/lib.rs deleted file mode 100644 index 592988a3..00000000 --- a/minecraft-core/src/lib.rs +++ /dev/null @@ -1,12 +0,0 @@ -//! Random miscellaneous things like UUIDs - -mod serializable_uuid; - -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - let result = 2 + 2; - assert_eq!(result, 4); - } -} diff --git a/minecraft-core/src/serializable_uuid.rs b/minecraft-core/src/serializable_uuid.rs deleted file mode 100644 index 91b5f2d8..00000000 --- a/minecraft-core/src/serializable_uuid.rs +++ /dev/null @@ -1,54 +0,0 @@ -use uuid::Uuid; - -pub trait SerializableUuid { - fn to_int_array(&self) -> [u32; 4]; - fn from_int_array(array: [u32; 4]) -> Self; -} - -// private static int[] leastMostToIntArray(long l, long l2) { -// return new int[]{(int)(l >> 32), (int)l, (int)(l2 >> 32), (int)l2}; -// } - -fn least_most_to_int_array(most: u64, least: u64) -> [u32; 4] { - [ - (most >> 32) as u32, - most as u32, - (least >> 32) as u32, - least as u32, - ] -} - -impl SerializableUuid for Uuid { - fn to_int_array(&self) -> [u32; 4] { - let most_significant_bits = (self.as_u128() >> 64) as u64; - let least_significant_bits = (self.as_u128() & 0xffffffffffffffff) as u64; - - least_most_to_int_array(most_significant_bits, least_significant_bits) - } - - fn from_int_array(array: [u32; 4]) -> Self { - let most = ((array[0] as u64) << 32) | ((array[1] as u64) & 0xFFFFFFFF); - let least = ((array[2] as u64) << 32) | ((array[3] as u64) & 0xFFFFFFFF); - - Uuid::from_u128((((most as u128) << 64) | least as u128).into()) - } -} - -mod tests { - use super::*; - - #[test] - fn to_int_array() { - let u = Uuid::parse_str("6536bfed-8695-48fd-83a1-ecd24cf2a0fd").unwrap(); - assert_eq!( - u.to_int_array(), - [0x6536bfed, 0x869548fd, 0x83a1ecd2, 0x4cf2a0fd] - ); - } - - #[test] - fn from_int_array() { - let u = Uuid::from_int_array([0x6536bfed, 0x869548fd, 0x83a1ecd2, 0x4cf2a0fd]); - assert_eq!(u.to_string(), "6536bfed-8695-48fd-83a1-ecd24cf2a0fd"); - } -} diff --git a/minecraft-protocol/Cargo.toml b/minecraft-protocol/Cargo.toml deleted file mode 100644 index 3cbf663b..00000000 --- a/minecraft-protocol/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -edition = "2021" -name = "minecraft-protocol" -version = "0.1.0" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -async-recursion = "^0.3.2" -async-trait = "0.1.51" -byteorder = "^1.4.3" -bytes = "^1.1.0" -minecraft-chat = {path = "../minecraft-chat"} -serde = {version = "1.0.130", features = ["serde_derive"]} -serde_json = "^1.0.72" -thiserror = "^1.0.30" -tokio = {version = "^1.14.0", features = ["io-util", "net", "macros"]} -tokio-util = "^0.6.9" -trust-dns-resolver = "^0.20.3" diff --git a/minecraft-protocol/src/connect.rs b/minecraft-protocol/src/connect.rs deleted file mode 100644 index f6dd9fe7..00000000 --- a/minecraft-protocol/src/connect.rs +++ /dev/null @@ -1,116 +0,0 @@ -//! parse sending and receiving packets with a server. - -use crate::packets::game::GamePacket; -use crate::packets::handshake::HandshakePacket; -use crate::packets::login::LoginPacket; -use crate::packets::status::StatusPacket; -use crate::read::read_packet; -use crate::write::write_packet; -use crate::ServerIpAddress; -use tokio::net::TcpStream; - -pub enum PacketFlow { - ClientToServer, - ServerToClient, -} - -pub struct HandshakeConnection { - pub flow: PacketFlow, - /// The buffered writer - pub stream: TcpStream, -} - -pub struct GameConnection { - pub flow: PacketFlow, - /// The buffered writer - pub stream: TcpStream, -} - -pub struct StatusConnection { - pub flow: PacketFlow, - /// The buffered writer - pub stream: TcpStream, -} - -pub struct LoginConnection { - pub flow: PacketFlow, - /// The buffered writer - pub stream: TcpStream, -} - -impl HandshakeConnection { - pub async fn new(address: &ServerIpAddress) -> Result { - let ip = address.ip; - let port = address.port; - - let stream = TcpStream::connect(format!("{}:{}", ip, port)) - .await - .map_err(|_| "Failed to connect to server")?; - - // enable tcp_nodelay - stream - .set_nodelay(true) - .expect("Error enabling tcp_nodelay"); - - Ok(HandshakeConnection { - flow: PacketFlow::ServerToClient, - stream, - }) - } - - pub fn login(self) -> LoginConnection { - LoginConnection { - flow: self.flow, - stream: self.stream, - } - } - - pub fn status(self) -> StatusConnection { - StatusConnection { - flow: self.flow, - stream: self.stream, - } - } - - pub async fn read(&mut self) -> Result { - read_packet::(&self.flow, &mut self.stream).await - } - - /// Write a packet to the server - pub async fn write(&mut self, packet: HandshakePacket) { - write_packet(packet, &mut self.stream).await; - } -} - -impl GameConnection { - pub async fn read(&mut self) -> Result { - read_packet::(&self.flow, &mut self.stream).await - } - - /// Write a packet to the server - pub async fn write(&mut self, packet: GamePacket) { - write_packet(packet, &mut self.stream).await; - } -} - -impl StatusConnection { - pub async fn read(&mut self) -> Result { - read_packet::(&self.flow, &mut self.stream).await - } - - /// Write a packet to the server - pub async fn write(&mut self, packet: StatusPacket) { - write_packet(packet, &mut self.stream).await; - } -} - -impl LoginConnection { - pub async fn read(&mut self) -> Result { - read_packet::(&self.flow, &mut self.stream).await - } - - /// Write a packet to the server - pub async fn write(&mut self, packet: LoginPacket) { - write_packet(packet, &mut self.stream).await; - } -} diff --git a/minecraft-protocol/src/lib.rs b/minecraft-protocol/src/lib.rs deleted file mode 100644 index 684add45..00000000 --- a/minecraft-protocol/src/lib.rs +++ /dev/null @@ -1,56 +0,0 @@ -//! This lib is responsible for parsing Minecraft packets. - -use std::net::IpAddr; -use std::str::FromStr; - -pub mod connect; -pub mod mc_buf; -pub mod packets; -pub mod read; -pub mod resolver; -pub mod write; - -#[derive(Debug)] -pub struct ServerAddress { - pub host: String, - pub port: u16, -} - -#[derive(Debug)] -pub struct ServerIpAddress { - pub ip: IpAddr, - pub port: u16, -} - -// impl try_from for ServerAddress -impl<'a> TryFrom<&'a 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 }) - } -} - -pub async fn connect(address: ServerAddress) -> Result<(), Box> { - let resolved_address = resolver::resolve_address(&address).await; - println!("Resolved address: {:?}", resolved_address); - Ok(()) -} - -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - let result = 2 + 2; - assert_eq!(result, 4); - } -} diff --git a/minecraft-protocol/src/mc_buf.rs b/minecraft-protocol/src/mc_buf.rs deleted file mode 100644 index 54ba1f7d..00000000 --- a/minecraft-protocol/src/mc_buf.rs +++ /dev/null @@ -1,203 +0,0 @@ -//! Utilities for reading and writing for the Minecraft protocol - -use std::io::Write; - -use async_trait::async_trait; -use byteorder::{BigEndian, WriteBytesExt}; -use tokio::io::{AsyncRead, AsyncReadExt}; - -// const DEFAULT_NBT_QUOTA: u32 = 2097152; -const MAX_STRING_LENGTH: u16 = 32767; -// const MAX_COMPONENT_STRING_LENGTH: u32 = 262144; - -#[async_trait] -pub trait Writable { - fn write_byte(&mut self, n: u8) -> Result<(), std::io::Error>; - fn write_bytes(&mut self, bytes: &[u8]) -> Result<(), std::io::Error>; - fn write_varint(&mut self, value: i32) -> Result<(), std::io::Error>; - fn write_utf_with_len(&mut self, string: &str, len: usize) -> Result<(), std::io::Error>; - fn write_utf(&mut self, string: &str) -> Result<(), std::io::Error>; - fn write_short(&mut self, n: u16) -> Result<(), std::io::Error>; - fn write_byte_array(&mut self, bytes: &[u8]) -> Result<(), std::io::Error>; -} - -#[async_trait] -impl Writable for Vec { - fn write_byte(&mut self, n: u8) -> Result<(), std::io::Error> { - WriteBytesExt::write_u8(self, n) - } - - fn write_bytes(&mut self, bytes: &[u8]) -> Result<(), std::io::Error> { - Ok(self.extend_from_slice(bytes)) - } - - fn write_varint(&mut self, mut value: i32) -> Result<(), std::io::Error> { - let mut buffer = [0]; - if value == 0 { - self.write_all(&buffer).unwrap(); - } - while value != 0 { - buffer[0] = (value & 0b0111_1111) as u8; - value = (value >> 7) & (i32::max_value() >> 6); - if value != 0 { - buffer[0] |= 0b1000_0000; - } - self.write_all(&buffer)?; - } - Ok(()) - } - - fn write_utf_with_len(&mut self, string: &str, len: usize) -> Result<(), std::io::Error> { - if string.len() > len { - panic!( - "String too big (was {} bytes encoded, max {})", - string.len(), - len - ); - } - self.write_varint(string.len() as i32); - self.write_bytes(string.as_bytes()) - } - - fn write_utf(&mut self, string: &str) -> Result<(), std::io::Error> { - self.write_utf_with_len(string, MAX_STRING_LENGTH.into()) - } - - fn write_short(&mut self, n: u16) -> Result<(), std::io::Error> { - WriteBytesExt::write_u16::(self, n) - } - - fn write_byte_array(&mut self, bytes: &[u8]) -> Result<(), std::io::Error> { - self.write_varint(bytes.len() as i32); - self.write_bytes(bytes) - } -} - -#[async_trait] -pub trait Readable { - async fn read_varint(&mut self) -> Result<(i32, u8), String>; - async fn read_byte_array(&mut self) -> Result, String>; - async fn read_bytes(&mut self, n: usize) -> Result, String>; - async fn read_utf(&mut self) -> Result; - async fn read_utf_with_len(&mut self, max_length: u32) -> Result; - async fn read_byte(&mut self) -> Result; -} - -#[async_trait] -impl Readable for R -where - R: AsyncRead + std::marker::Unpin + std::marker::Send, -{ - // fast varints stolen from https://github.com/luojia65/mc-varint/blob/master/src/lib.rs#L67 - /// Read a single varint from the reader and return the value, along with the number of bytes read - async fn read_varint(&mut self) -> Result<(i32, u8), String> { - let mut buffer = [0]; - let mut ans = 0; - for i in 0..4 { - self.read_exact(&mut buffer) - .await - .map_err(|_| "Invalid VarInt".to_string())?; - ans |= ((buffer[0] & 0b0111_1111) as i32) << (7 * i); - if buffer[0] & 0b1000_0000 == 0 { - return Ok((ans, i + 1)); - } - } - Ok((ans, 5)) - } - - async fn read_byte_array(&mut self) -> Result, String> { - let length = self.read_varint().await?.0 as usize; - Ok(self.read_bytes(length).await?) - } - - async fn read_bytes(&mut self, n: usize) -> Result, String> { - let mut bytes = vec![0; n]; - match AsyncReadExt::read_exact(self, &mut bytes).await { - Ok(_) => Ok(bytes), - Err(_) => Err("Error reading bytes".to_string()), - } - } - - async fn read_utf(&mut self) -> Result { - self.read_utf_with_len(MAX_STRING_LENGTH.into()).await - } - - async fn read_utf_with_len(&mut self, max_length: u32) -> Result { - let (length, _length_varint_length) = self.read_varint().await?; - // i don't know why it's multiplied by 4 but it's like that in mojang's code so - if length < 0 { - return Err( - "The received encoded string buffer length is less than zero! Weird string!" - .to_string(), - ); - } - if length as u32 > max_length * 4 { - return Err(format!( - "The received encoded string buffer length is longer than maximum allowed ({} > {})", - length, - max_length * 4 - )); - } - - // this is probably quite inefficient, idk how to do it better - let mut string = String::new(); - let mut buffer = vec![0; length as usize]; - self.read_exact(&mut buffer) - .await - .map_err(|_| "Invalid UTF-8".to_string())?; - - string.push_str(std::str::from_utf8(&buffer).unwrap()); - if string.len() > length as usize { - return Err(format!( - "The received string length is longer than maximum allowed ({} > {})", - length, max_length - )); - } - - Ok(string) - } - - /// Read a single byte from the reader - async fn read_byte(&mut self) -> Result { - match AsyncReadExt::read_u8(self).await { - Ok(r) => Ok(r), - Err(_) => Err("Error reading byte".to_string()), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::io::Cursor; - use tokio::io::BufReader; - - #[test] - fn test_write_varint() { - let mut buf = Vec::new(); - buf.write_varint(123456); - assert_eq!(buf, vec![192, 196, 7]); - - let mut buf = Vec::new(); - buf.write_varint(0); - assert_eq!(buf, vec![0]); - } - - #[tokio::test] - async fn test_read_varint() { - let mut buf = BufReader::new(Cursor::new(vec![192, 196, 7])); - assert_eq!(buf.read_varint().await.unwrap(), (123456, 3)); - - let mut buf = BufReader::new(Cursor::new(vec![0])); - assert_eq!(buf.read_varint().await.unwrap(), (0, 1)); - - let mut buf = BufReader::new(Cursor::new(vec![1])); - assert_eq!(buf.read_varint().await.unwrap(), (1, 1)); - } - - #[tokio::test] - async fn test_read_varint_longer() { - let mut buf = BufReader::new(Cursor::new(vec![138, 56, 0, 135, 56, 123])); - assert_eq!(buf.read_varint().await.unwrap(), (7178, 2)); - } -} diff --git a/minecraft-protocol/src/packets/game/mod.rs b/minecraft-protocol/src/packets/game/mod.rs deleted file mode 100644 index a3ef2541..00000000 --- a/minecraft-protocol/src/packets/game/mod.rs +++ /dev/null @@ -1,35 +0,0 @@ -use async_trait::async_trait; -use tokio::io::BufReader; - -use crate::connect::PacketFlow; - -use super::ProtocolPacket; - -#[derive(Clone, Debug)] -pub enum GamePacket -where - Self: Sized, {} - -#[async_trait] -impl ProtocolPacket for GamePacket { - fn id(&self) -> u32 { - 0x00 - } - - fn write(&self, _buf: &mut Vec) {} - - /// Read a packet by its id, ConnectionProtocol, and flow - async fn read( - _id: u32, - flow: &PacketFlow, - _buf: &mut BufReader, - ) -> Result - where - Self: Sized, - { - match flow { - PacketFlow::ServerToClient => Err("HandshakePacket::read not implemented".to_string()), - PacketFlow::ClientToServer => Err("HandshakePacket::read not implemented".to_string()), - } - } -} diff --git a/minecraft-protocol/src/packets/handshake/client_intention_packet.rs b/minecraft-protocol/src/packets/handshake/client_intention_packet.rs deleted file mode 100644 index 868626b3..00000000 --- a/minecraft-protocol/src/packets/handshake/client_intention_packet.rs +++ /dev/null @@ -1,36 +0,0 @@ -use std::hash::Hash; - -use tokio::io::BufReader; - -use crate::{mc_buf::Writable, packets::ConnectionProtocol}; - -use super::HandshakePacket; - -#[derive(Hash, Clone, Debug)] -pub struct ClientIntentionPacket { - pub protocol_version: u32, - pub hostname: String, - pub port: u16, - /// 1 for status, 2 for login - pub intention: ConnectionProtocol, -} - -impl ClientIntentionPacket { - pub fn get(self) -> HandshakePacket { - HandshakePacket::ClientIntentionPacket(self) - } - - pub fn write(&self, buf: &mut Vec) { - buf.write_varint(self.protocol_version as i32).unwrap(); - buf.write_utf(&self.hostname).unwrap(); - buf.write_short(self.port).unwrap(); - buf.write_varint(self.intention.clone() as i32).unwrap(); - } - - pub async fn read( - _buf: &mut BufReader, - ) -> Result { - Err("ClientIntentionPacket::parse not implemented".to_string()) - // Ok(ClientIntentionPacket {}.get()) - } -} diff --git a/minecraft-protocol/src/packets/handshake/mod.rs b/minecraft-protocol/src/packets/handshake/mod.rs deleted file mode 100644 index 01010e1e..00000000 --- a/minecraft-protocol/src/packets/handshake/mod.rs +++ /dev/null @@ -1,49 +0,0 @@ -pub mod client_intention_packet; - -use async_trait::async_trait; -use tokio::io::BufReader; - -use crate::connect::PacketFlow; - -use super::ProtocolPacket; - -#[derive(Clone, Debug)] -pub enum HandshakePacket -where - Self: Sized, -{ - ClientIntentionPacket(client_intention_packet::ClientIntentionPacket), -} - -#[async_trait] -impl ProtocolPacket for HandshakePacket { - fn id(&self) -> u32 { - match self { - HandshakePacket::ClientIntentionPacket(_packet) => 0x00, - } - } - - fn write(&self, buf: &mut Vec) { - match self { - HandshakePacket::ClientIntentionPacket(packet) => packet.write(buf), - } - } - - /// Read a packet by its id, ConnectionProtocol, and flow - async fn read( - id: u32, - flow: &PacketFlow, - buf: &mut BufReader, - ) -> Result - where - Self: Sized, - { - match flow { - PacketFlow::ServerToClient => Err("HandshakePacket::read not implemented".to_string()), - PacketFlow::ClientToServer => match id { - 0x00 => Ok(client_intention_packet::ClientIntentionPacket::read(buf).await?), - _ => Err(format!("Unknown ClientToServer status packet id: {}", id)), - }, - } - } -} diff --git a/minecraft-protocol/src/packets/login/clientbound_custom_query_packet.rs b/minecraft-protocol/src/packets/login/clientbound_custom_query_packet.rs deleted file mode 100644 index 093176eb..00000000 --- a/minecraft-protocol/src/packets/login/clientbound_custom_query_packet.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::hash::Hash; -use tokio::io::BufReader; - -use crate::mc_buf::{self, Readable, Writable}; - -use super::LoginPacket; - -#[derive(Hash, Clone, Debug)] -pub struct ClientboundCustomQueryPacket { - pub transaction_id: u32, - // TODO: this should be a resource location - pub identifier: String, - pub data: Vec, -} - -impl ClientboundCustomQueryPacket { - pub fn get(self) -> LoginPacket { - LoginPacket::ClientboundCustomQueryPacket(self) - } - - pub fn write(&self, buf: &mut Vec) { - buf.write_varint(self.transaction_id as i32).unwrap(); - buf.write_utf(&self.identifier).unwrap(); - buf.write_bytes(&self.data).unwrap(); - } - - pub async fn read( - buf: &mut BufReader, - ) -> Result { - let transaction_id = buf.read_varint().await?.0 as u32; - // TODO: this should be a resource location - let identifier = buf.read_utf().await?; - let data = buf.read_bytes(1048576).await?; - Ok(ClientboundCustomQueryPacket { - transaction_id, - identifier, - data, - } - .get()) - } -} diff --git a/minecraft-protocol/src/packets/login/clientbound_hello_packet.rs b/minecraft-protocol/src/packets/login/clientbound_hello_packet.rs deleted file mode 100644 index 36a48706..00000000 --- a/minecraft-protocol/src/packets/login/clientbound_hello_packet.rs +++ /dev/null @@ -1,38 +0,0 @@ -use std::hash::Hash; -use tokio::io::BufReader; - -use crate::mc_buf::Readable; - -use super::LoginPacket; - -#[derive(Hash, Clone, Debug)] -pub struct ClientboundHelloPacket { - pub server_id: String, - pub public_key: Vec, - pub nonce: Vec, -} - -impl ClientboundHelloPacket { - pub fn get(self) -> LoginPacket { - LoginPacket::ClientboundHelloPacket(self) - } - - pub fn write(&self, _buf: &mut Vec) { - panic!("ClientboundHelloPacket::write not implemented") - } - - pub async fn read( - buf: &mut BufReader, - ) -> Result { - let server_id = buf.read_utf_with_len(20).await?; - let public_key = buf.read_byte_array().await?; - let nonce = buf.read_byte_array().await?; - - Ok(ClientboundHelloPacket { - server_id, - public_key, - nonce, - } - .get()) - } -} diff --git a/minecraft-protocol/src/packets/login/mod.rs b/minecraft-protocol/src/packets/login/mod.rs deleted file mode 100644 index f0ed6717..00000000 --- a/minecraft-protocol/src/packets/login/mod.rs +++ /dev/null @@ -1,63 +0,0 @@ -pub mod clientbound_custom_query_packet; -pub mod clientbound_hello_packet; -pub mod serverbound_hello_packet; - -use async_trait::async_trait; -use tokio::io::BufReader; - -use crate::connect::PacketFlow; - -use super::ProtocolPacket; - -#[derive(Clone, Debug)] -pub enum LoginPacket -where - Self: Sized, -{ - ClientboundCustomQueryPacket(clientbound_custom_query_packet::ClientboundCustomQueryPacket), - ServerboundHelloPacket(serverbound_hello_packet::ServerboundHelloPacket), - ClientboundHelloPacket(clientbound_hello_packet::ClientboundHelloPacket), -} - -#[async_trait] -impl ProtocolPacket for LoginPacket { - fn id(&self) -> u32 { - match self { - LoginPacket::ClientboundCustomQueryPacket(_packet) => 0x04, - LoginPacket::ServerboundHelloPacket(_packet) => 0x00, - LoginPacket::ClientboundHelloPacket(_packet) => 0x01, - } - } - - fn write(&self, buf: &mut Vec) { - match self { - LoginPacket::ClientboundCustomQueryPacket(packet) => packet.write(buf), - LoginPacket::ServerboundHelloPacket(packet) => packet.write(buf), - LoginPacket::ClientboundHelloPacket(packet) => packet.write(buf), - } - } - - /// Read a packet by its id, ConnectionProtocol, and flow - async fn read( - id: u32, - flow: &PacketFlow, - buf: &mut BufReader, - ) -> Result - where - Self: Sized, - { - Ok(match flow { - PacketFlow::ServerToClient => match id { - 0x01 => clientbound_hello_packet::ClientboundHelloPacket::read(buf).await?, - 0x04 => { - clientbound_custom_query_packet::ClientboundCustomQueryPacket::read(buf).await? - } - _ => return Err(format!("Unknown ServerToClient status packet id: {}", id)), - }, - PacketFlow::ClientToServer => match id { - 0x00 => serverbound_hello_packet::ServerboundHelloPacket::read(buf).await?, - _ => return Err(format!("Unknown ClientToServer status packet id: {}", id)), - }, - }) - } -} diff --git a/minecraft-protocol/src/packets/login/serverbound_hello_packet.rs b/minecraft-protocol/src/packets/login/serverbound_hello_packet.rs deleted file mode 100644 index 32a6dadc..00000000 --- a/minecraft-protocol/src/packets/login/serverbound_hello_packet.rs +++ /dev/null @@ -1,27 +0,0 @@ -use std::hash::Hash; -use tokio::io::BufReader; - -use crate::mc_buf::Writable; - -use super::LoginPacket; - -#[derive(Hash, Clone, Debug)] -pub struct ServerboundHelloPacket { - pub username: String, -} - -impl ServerboundHelloPacket { - pub fn get(self) -> LoginPacket { - LoginPacket::ServerboundHelloPacket(self) - } - - pub fn write(&self, buf: &mut Vec) { - buf.write_utf(&self.username).unwrap(); - } - - pub async fn read( - _buf: &mut BufReader, - ) -> Result { - Err("ServerboundHelloPacket::read not implemented".to_string()) - } -} diff --git a/minecraft-protocol/src/packets/mod.rs b/minecraft-protocol/src/packets/mod.rs deleted file mode 100644 index a074b570..00000000 --- a/minecraft-protocol/src/packets/mod.rs +++ /dev/null @@ -1,146 +0,0 @@ -pub mod game; -pub mod handshake; -pub mod login; -pub mod status; - -use async_trait::async_trait; -use tokio::io::BufReader; - -use crate::connect::PacketFlow; - -pub const PROTOCOL_VERSION: u32 = 757; - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum ConnectionProtocol { - Handshake = -1, - Game = 0, - Status = 1, - Login = 2, -} - -#[derive(Clone, Debug)] -pub enum Packet { - Game(game::GamePacket), - Handshake(handshake::HandshakePacket), - Login(login::LoginPacket), - Status(Box), -} - -/// An enum of packets for a certain protocol -#[async_trait] -pub trait ProtocolPacket -where - Self: Sized, -{ - fn id(&self) -> u32; - - /// Read a packet by its id, ConnectionProtocol, and flow - async fn read( - id: u32, - flow: &PacketFlow, - buf: &mut BufReader, - ) -> Result - where - Self: Sized; - - fn write(&self, buf: &mut Vec); -} - -// impl Packet { -// fn get_inner_packet(&self) -> &dyn PacketTrait { -// match self { -// Packet::ClientIntentionPacket(packet) => packet, -// Packet::ServerboundStatusRequestPacket(packet) => packet, -// Packet::ClientboundStatusResponsePacket(packet) => packet, -// Packet::ServerboundHelloPacket(packet) => packet, -// Packet::ClientboundHelloPacket(packet) => packet, -// } -// } - -// pub fn id(&self) -> u32 { -// match self { -// Packet::ClientIntentionPacket(_packet) => 0x00, -// Packet::ServerboundStatusRequestPacket(_packet) => 0x00, -// Packet::ClientboundStatusResponsePacket(_packet) => 0x00, -// Packet::ServerboundHelloPacket(_packet) => 0x00, -// Packet::ClientboundHelloPacket(_packet) => 0x01, -// } -// } - -// /// Read a packet by its id, ConnectionProtocol, and flow -// pub async fn read( -// id: u32, -// protocol: &ConnectionProtocol, -// flow: &PacketFlow, -// buf: &mut BufReader, -// ) -> Result { -// match protocol { -// ConnectionProtocol::Handshake => match flow { -// PacketFlow::ClientToServer => match id { -// 0x00 => Ok( -// handshake::client_intention_packet::ClientIntentionPacket::read(buf).await?, -// ), -// _ => Err(format!("Unknown ClientToServer handshake packet id: {}", id)), -// } -// PacketFlow::ServerToClient => Err("ServerToClient handshake packets not implemented".to_string()), -// }, - -// ConnectionProtocol::Game => Err("Game protocol not implemented yet".to_string()), - -// ConnectionProtocol::Status => match flow { -// PacketFlow::ServerToClient => match id { -// 0x00 => Ok( -// status::clientbound_status_response_packet::ClientboundStatusResponsePacket -// ::read(buf) -// .await?, -// ), -// _ => Err(format!("Unknown ServerToClient status packet id: {}", id)), -// }, -// PacketFlow::ClientToServer => match id { -// 0x00 => Ok( -// status::serverbound_status_request_packet::ServerboundStatusRequestPacket -// ::read(buf) -// .await?, -// ), -// _ => Err(format!("Unknown ClientToServer status packet id: {}", id)), -// }, -// }, - -// ConnectionProtocol::Login => match flow { -// PacketFlow::ServerToClient => match id { -// 0x01 => Ok( -// login::clientbound_hello_packet::ClientboundHelloPacket::read(buf).await?, -// ), -// _ => Err(format!("Unknown ServerToClient login packet id: {}", id)), -// }, -// PacketFlow::ClientToServer => match id { -// 0x00 => Ok( -// login::serverbound_hello_packet::ServerboundHelloPacket::read(buf).await?, -// ), -// _ => Err(format!("Unknown ClientToServer login packet id: {}", id)), -// }, -// }, -// } -// } - -// pub fn write(&self, buf: &mut Vec) { -// self.get_inner_packet().write(buf); -// } -// } - -// #[async_trait] -// pub trait PacketTrait -// where -// Self: Sized, -// { -// /// Return a version of the packet that you can actually use for stuff -// fn get(self) -> dyn ProtocolPacket; - -// fn write(&self, buf: &mut Vec); - -// async fn read( -// buf: &mut BufReader, -// ) -> Result -// where -// Self: Sized; -// } diff --git a/minecraft-protocol/src/packets/status/clientbound_status_response_packet.rs b/minecraft-protocol/src/packets/status/clientbound_status_response_packet.rs deleted file mode 100644 index 920e3484..00000000 --- a/minecraft-protocol/src/packets/status/clientbound_status_response_packet.rs +++ /dev/null @@ -1,58 +0,0 @@ -use minecraft_chat::component::Component; -use serde::Deserialize; -use serde_json::Value; -use tokio::io::BufReader; - -use crate::mc_buf::Readable; - -use super::StatusPacket; - -#[derive(Clone, Debug, Deserialize)] -pub struct Version { - pub name: Component, - pub protocol: u32, -} - -#[derive(Clone, Debug, Deserialize)] -pub struct SamplePlayer { - pub id: String, - pub name: String, -} - -#[derive(Clone, Debug, Deserialize)] -pub struct Players { - pub max: u32, - pub online: u32, - pub sample: Vec, -} - -// the entire packet is just json, which is why it has deserialize -#[derive(Clone, Debug, Deserialize)] -pub struct ClientboundStatusResponsePacket { - pub description: Component, - pub favicon: Option, - pub players: Players, - pub version: Version, -} - -impl ClientboundStatusResponsePacket { - pub fn get(self) -> StatusPacket { - StatusPacket::ClientboundStatusResponsePacket(Box::new(self)) - } - - pub fn write(&self, _buf: &mut Vec) {} - - pub async fn read( - buf: &mut BufReader, - ) -> Result { - let status_string = buf.read_utf().await?; - let status_json: Value = - serde_json::from_str(status_string.as_str()).expect("Server status isn't valid JSON"); - - let packet = ClientboundStatusResponsePacket::deserialize(status_json) - .map_err(|e| e.to_string())? - .get(); - - Ok(packet) - } -} diff --git a/minecraft-protocol/src/packets/status/mod.rs b/minecraft-protocol/src/packets/status/mod.rs deleted file mode 100644 index ac6a34e1..00000000 --- a/minecraft-protocol/src/packets/status/mod.rs +++ /dev/null @@ -1,66 +0,0 @@ -pub mod clientbound_status_response_packet; -pub mod serverbound_status_request_packet; - -use async_trait::async_trait; -use tokio::io::BufReader; - -use crate::connect::PacketFlow; - -use super::ProtocolPacket; - -#[derive(Clone, Debug)] -pub enum StatusPacket -where - Self: Sized, -{ - ServerboundStatusRequestPacket( - serverbound_status_request_packet::ServerboundStatusRequestPacket, - ), - ClientboundStatusResponsePacket( - Box, - ), -} - -#[async_trait] -impl ProtocolPacket for StatusPacket { - fn id(&self) -> u32 { - match self { - StatusPacket::ServerboundStatusRequestPacket(_packet) => 0x00, - StatusPacket::ClientboundStatusResponsePacket(_packet) => 0x00, - } - } - - fn write(&self, buf: &mut Vec) { - match self { - StatusPacket::ServerboundStatusRequestPacket(packet) => packet.write(buf), - StatusPacket::ClientboundStatusResponsePacket(packet) => packet.write(buf), - } - } - - /// Read a packet by its id, ConnectionProtocol, and flow - async fn read( - id: u32, - flow: &PacketFlow, - buf: &mut BufReader, - ) -> Result - where - Self: Sized, - { - match flow { - PacketFlow::ServerToClient => match id { - 0x00 => Ok( - clientbound_status_response_packet::ClientboundStatusResponsePacket::read(buf) - .await?, - ), - _ => Err(format!("Unknown ServerToClient status packet id: {}", id)), - }, - PacketFlow::ClientToServer => match id { - 0x00 => Ok( - serverbound_status_request_packet::ServerboundStatusRequestPacket::read(buf) - .await?, - ), - _ => Err(format!("Unknown ClientToServer status packet id: {}", id)), - }, - } - } -} diff --git a/minecraft-protocol/src/packets/status/serverbound_status_request_packet.rs b/minecraft-protocol/src/packets/status/serverbound_status_request_packet.rs deleted file mode 100644 index 6a58da1f..00000000 --- a/minecraft-protocol/src/packets/status/serverbound_status_request_packet.rs +++ /dev/null @@ -1,23 +0,0 @@ -use std::hash::Hash; -use tokio::io::BufReader; - -use super::StatusPacket; - -#[derive(Hash, Clone, Debug)] -pub struct ServerboundStatusRequestPacket {} - -impl ServerboundStatusRequestPacket { - pub fn get(self) -> StatusPacket { - StatusPacket::ServerboundStatusRequestPacket(self) - } - - pub fn write(&self, _buf: &mut Vec) { - panic!("ServerboundStatusRequestPacket::write not implemented") - } - - pub async fn read( - _buf: &mut BufReader, - ) -> Result { - Err("ServerboundStatusRequestPacket::read not implemented".to_string()) - } -} diff --git a/minecraft-protocol/src/read.rs b/minecraft-protocol/src/read.rs deleted file mode 100644 index 6f242e8b..00000000 --- a/minecraft-protocol/src/read.rs +++ /dev/null @@ -1,28 +0,0 @@ -use tokio::{io::BufReader, net::TcpStream}; - -use crate::{connect::PacketFlow, mc_buf::Readable, packets::ProtocolPacket}; - -pub async fn read_packet( - flow: &PacketFlow, - stream: &mut TcpStream, -) -> Result { - // what this does: - // 1. reads the first 5 bytes, probably only some of this will be used to get the packet length - // 2. how much we should read = packet length - 5 - // 3. read the rest of the packet and add it to the cursor - // 4. figure out what packet this is and parse it - - // the first thing minecraft sends us is the length as a varint, which can be up to 5 bytes long - let mut buf = BufReader::with_capacity(4 * 1024 * 1024, stream); - - let (_packet_size, _packet_size_varint_size) = buf.read_varint().await?; - - // then, minecraft tells us the packet id as a varint - let (packet_id, _packet_id_size) = buf.read_varint().await?; - - // if we recognize the packet id, parse it - - let packet = P::read(packet_id.try_into().unwrap(), flow, &mut buf).await?; - - Ok(packet) -} diff --git a/minecraft-protocol/src/resolver.rs b/minecraft-protocol/src/resolver.rs deleted file mode 100644 index 24687a6e..00000000 --- a/minecraft-protocol/src/resolver.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::net::IpAddr; - -use crate::{ServerAddress, ServerIpAddress}; -use async_recursion::async_recursion; -use trust_dns_resolver::{ - config::{ResolverConfig, ResolverOpts}, - TokioAsyncResolver, -}; - -/// Resolve a Minecraft server address into an IP address and port. -/// If it's already an IP address, it's returned as-is. -#[async_recursion] -pub async fn resolve_address(address: &ServerAddress) -> Result { - // If the address.host is already in the format of an ip address, return it. - if let Ok(ip) = address.host.parse::() { - return Ok(ServerIpAddress { - ip, - port: address.port, - }); - } - - // we specify Cloudflare instead of the default resolver because trust_dns_resolver has an issue on Windows where it's really slow using the default resolver - let resolver = - TokioAsyncResolver::tokio(ResolverConfig::cloudflare(), ResolverOpts::default()).unwrap(); - - // first, we do a srv lookup for _minecraft._tcp. - let srv_redirect_result = resolver - .srv_lookup(format!("_minecraft._tcp.{}", address.host).as_str()) - .await; - - // if it resolves that means it's a redirect so we call resolve_address again with the new host - if let Ok(redirect_result) = srv_redirect_result { - let redirect_srv = redirect_result - .iter() - .next() - .ok_or_else(|| "No SRV record found".to_string())?; - let redirect_address = ServerAddress { - host: redirect_srv.target().to_utf8(), - port: redirect_srv.port(), - }; - - println!("redirecting to {:?}", redirect_address); - - return resolve_address(&redirect_address).await; - } - - // there's no redirect, try to resolve this as an ip address - let lookup_ip_result = resolver.lookup_ip(address.host.clone()).await; - let lookup_ip = lookup_ip_result.map_err(|_| "No IP found".to_string())?; - - Ok(ServerIpAddress { - ip: lookup_ip.iter().next().unwrap(), - port: address.port, - }) -} diff --git a/minecraft-protocol/src/write.rs b/minecraft-protocol/src/write.rs deleted file mode 100644 index 3d8540eb..00000000 --- a/minecraft-protocol/src/write.rs +++ /dev/null @@ -1,27 +0,0 @@ -use tokio::{io::AsyncWriteExt, net::TcpStream}; - -use crate::{mc_buf::Writable, packets::ProtocolPacket}; - -pub async fn write_packet(packet: impl ProtocolPacket, stream: &mut TcpStream) { - // TODO: implement compression - - // packet structure: - // length (varint) + id (varint) + data - - // write the packet id - let mut id_and_data_buf = vec![]; - id_and_data_buf.write_varint(packet.id() as i32); - packet.write(&mut id_and_data_buf); - - // write the packet data - - // make a new buffer that has the length at the beginning - // and id+data at the end - let mut complete_buf: Vec = Vec::new(); - complete_buf.write_varint(id_and_data_buf.len() as i32); - complete_buf.append(&mut id_and_data_buf); - - // finally, write and flush to the stream - stream.write_all(&complete_buf).await.unwrap(); - stream.flush().await.unwrap(); -} -- cgit v1.2.3