valence/src/text.rs

613 lines
16 KiB
Rust
Raw Normal View History

2022-07-14 23:18:20 -07:00
//! Formatted text.
2022-04-14 14:55:45 -07:00
use std::borrow::Cow;
use std::fmt;
use std::io::Write;
2022-04-14 14:55:45 -07:00
use serde::de::Visitor;
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
2022-04-14 14:55:45 -07:00
use crate::ident::Ident;
use crate::protocol::{BoundedString, Decode, Encode};
2022-04-14 14:55:45 -07:00
/// Represents formatted text in Minecraft's JSON text format.
///
/// Text is used in various places such as chat, window titles,
/// disconnect messages, written books, signs, and more.
///
2022-07-11 05:08:02 -07:00
/// For more information, see the relevant [Minecraft Wiki article].
2022-04-14 14:55:45 -07:00
///
2022-09-02 00:06:45 -07:00
/// Note that the current [`Deserialize`] implementation on this type recognizes
2022-04-14 14:55:45 -07:00
/// only a subset of the full JSON chat component format.
///
2022-07-11 05:08:02 -07:00
/// [Minecraft Wiki article]: https://minecraft.fandom.com/wiki/Raw_JSON_text_format
///
/// # Examples
///
2022-04-14 14:55:45 -07:00
/// With [`TextFormat`] in scope, you can write the following:
/// ```
2022-04-17 17:05:13 -07:00
/// use valence::text::{Color, Text, TextFormat};
2022-04-14 14:55:45 -07:00
///
/// let txt = "The text is ".into_text()
/// + "Red".color(Color::RED)
/// + ", "
/// + "Green".color(Color::GREEN)
/// + ", and also "
/// + "Blue".color(Color::BLUE)
/// + "!\nAnd maybe even "
/// + "Italic".italic()
/// + ".";
///
/// assert_eq!(
/// txt.to_plain(),
/// "The text is Red, Green, and also Blue!\nAnd maybe even Italic."
/// );
/// ```
#[derive(Clone, PartialEq, Default, Debug, Serialize, Deserialize)]
2022-04-17 17:05:13 -07:00
#[serde(rename_all = "camelCase")]
2022-04-14 14:55:45 -07:00
pub struct Text {
#[serde(flatten)]
content: TextContent,
#[serde(default, skip_serializing_if = "Option::is_none")]
color: Option<Color>,
#[serde(default, skip_serializing_if = "Option::is_none")]
font: Option<Cow<'static, str>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
bold: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
italic: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
underlined: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
strikethrough: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
obfuscated: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
insertion: Option<Cow<'static, str>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
click_event: Option<ClickEvent>,
#[serde(default, skip_serializing_if = "Option::is_none")]
hover_event: Option<HoverEvent>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
extra: Vec<Text>,
}
2022-08-09 14:44:04 -07:00
#[allow(clippy::self_named_constructors)]
2022-07-06 00:16:07 -07:00
impl Text {
2022-08-09 14:44:04 -07:00
/// Constructs a new plain text object.
pub fn text(plain: impl Into<Cow<'static, str>>) -> Self {
Self {
content: TextContent::Text { text: plain.into() },
..Self::default()
}
}
/// Create translated text based on the given translation key.
pub fn translate(key: impl Into<Cow<'static, str>>) -> Self {
Self {
content: TextContent::Translate {
translate: key.into(),
},
..Self::default()
}
}
/// Gets this text object as plain text without any formatting.
pub fn to_plain(&self) -> String {
let mut res = String::new();
self.write_plain(&mut res)
.expect("failed to write plain text");
res
}
/// Writes this text object as plain text to the provided writer.
pub fn write_plain(&self, mut w: impl fmt::Write) -> fmt::Result {
fn write_plain_impl(this: &Text, w: &mut impl fmt::Write) -> fmt::Result {
match &this.content {
TextContent::Text { text } => w.write_str(text.as_ref())?,
TextContent::Translate { translate } => w.write_str(translate.as_ref())?,
}
2022-08-09 14:44:04 -07:00
for child in &this.extra {
write_plain_impl(child, w)?;
}
Ok(())
2022-08-09 14:44:04 -07:00
}
write_plain_impl(self, &mut w)
2022-08-09 14:44:04 -07:00
}
2022-07-11 05:08:02 -07:00
/// Returns `true` if the text contains no characters. Returns `false`
/// otherwise.
2022-07-06 00:16:07 -07:00
pub fn is_empty(&self) -> bool {
for extra in &self.extra {
if !extra.is_empty() {
return false;
}
}
match &self.content {
TextContent::Text { text } => text.is_empty(),
TextContent::Translate { translate } => translate.is_empty(),
}
}
}
2022-04-14 14:55:45 -07:00
/// Provides the methods necessary for working with [`Text`] objects.
///
2022-04-29 00:48:41 -07:00
/// This trait exists to allow using `Into<Text>` types without having to first
2022-07-11 05:08:02 -07:00
/// convert the type into [`Text`]. A blanket implementation exists for all
2022-04-14 14:55:45 -07:00
/// `Into<Text>` types, including [`Text`] itself.
pub trait TextFormat: Into<Text> {
2022-07-14 23:18:20 -07:00
/// Converts this type into a [`Text`] object.
2022-04-14 14:55:45 -07:00
fn into_text(self) -> Text {
self.into()
}
fn color(self, color: Color) -> Text {
let mut t = self.into();
t.color = Some(color);
t
}
fn clear_color(self) -> Text {
let mut t = self.into();
t.color = None;
t
}
fn font(self, font: impl Into<Cow<'static, str>>) -> Text {
let mut t = self.into();
t.font = Some(font.into());
t
}
fn clear_font(self) -> Text {
let mut t = self.into();
t.font = None;
t
}
fn bold(self) -> Text {
let mut t = self.into();
t.bold = Some(true);
t
}
fn not_bold(self) -> Text {
let mut t = self.into();
t.bold = Some(false);
t
}
fn clear_bold(self) -> Text {
let mut t = self.into();
t.bold = None;
t
}
fn italic(self) -> Text {
let mut t = self.into();
t.italic = Some(true);
t
}
fn not_italic(self) -> Text {
let mut t = self.into();
t.italic = Some(false);
t
}
fn clear_italic(self) -> Text {
let mut t = self.into();
t.italic = None;
t
}
fn underlined(self) -> Text {
let mut t = self.into();
t.underlined = Some(true);
t
}
fn not_underlined(self) -> Text {
let mut t = self.into();
t.underlined = Some(false);
t
}
fn clear_underlined(self) -> Text {
let mut t = self.into();
t.underlined = None;
t
}
fn strikethrough(self) -> Text {
let mut t = self.into();
t.strikethrough = Some(true);
t
}
fn not_strikethrough(self) -> Text {
let mut t = self.into();
t.strikethrough = Some(false);
t
}
fn clear_strikethrough(self) -> Text {
let mut t = self.into();
t.strikethrough = None;
t
}
fn obfuscated(self) -> Text {
let mut t = self.into();
t.obfuscated = Some(true);
t
}
fn not_obfuscated(self) -> Text {
let mut t = self.into();
t.obfuscated = Some(false);
t
}
fn clear_obfuscated(self) -> Text {
let mut t = self.into();
t.obfuscated = None;
t
}
fn insertion(self, insertion: impl Into<Cow<'static, str>>) -> Text {
let mut t = self.into();
t.insertion = Some(insertion.into());
t
}
fn clear_insertion(self) -> Text {
let mut t = self.into();
t.insertion = None;
t
}
fn on_click_open_url(self, url: impl Into<Cow<'static, str>>) -> Text {
let mut t = self.into();
t.click_event = Some(ClickEvent::OpenUrl(url.into()));
t
}
fn on_click_run_command(self, command: impl Into<Cow<'static, str>>) -> Text {
let mut t = self.into();
t.click_event = Some(ClickEvent::RunCommand(command.into()));
t
}
fn on_click_suggest_command(self, command: impl Into<Cow<'static, str>>) -> Text {
let mut t = self.into();
t.click_event = Some(ClickEvent::SuggestCommand(command.into()));
t
}
fn on_click_change_page(self, page: impl Into<i32>) -> Text {
let mut t = self.into();
t.click_event = Some(ClickEvent::ChangePage(page.into()));
t
}
fn on_click_copy_to_clipboard(self, text: impl Into<Cow<'static, str>>) -> Text {
let mut t = self.into();
t.click_event = Some(ClickEvent::CopyToClipboard(text.into()));
t
}
fn clear_click_event(self) -> Text {
let mut t = self.into();
t.click_event = None;
t
}
fn on_hover_show_text(self, text: impl Into<Text>) -> Text {
let mut t = self.into();
t.hover_event = Some(HoverEvent::ShowText(Box::new(text.into())));
t
}
fn clear_hover_event(self) -> Text {
let mut t = self.into();
t.hover_event = None;
t
}
fn add_child(self, text: impl Into<Text>) -> Text {
let mut t = self.into();
t.extra.push(text.into());
t
}
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
#[serde(untagged)]
enum TextContent {
2022-07-01 15:29:31 -07:00
Text {
text: Cow<'static, str>,
},
Translate {
translate: Cow<'static, str>,
// TODO: 'with' field
},
2022-04-14 14:55:45 -07:00
// TODO: score
// TODO: entity names
// TODO: keybind
// TODO: nbt
}
2022-07-14 23:18:20 -07:00
/// Text color
2022-04-17 17:05:13 -07:00
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
2022-04-14 14:55:45 -07:00
pub struct Color {
2022-07-14 23:18:20 -07:00
/// Red channel
2022-04-14 14:55:45 -07:00
pub r: u8,
2022-07-14 23:18:20 -07:00
/// Green channel
2022-04-14 14:55:45 -07:00
pub g: u8,
2022-07-14 23:18:20 -07:00
/// Blue channel
2022-04-14 14:55:45 -07:00
pub b: u8,
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
#[serde(tag = "action", content = "value", rename_all = "snake_case")]
enum ClickEvent {
OpenUrl(Cow<'static, str>),
/// Only usable by internal servers for security reasons.
OpenFile(Cow<'static, str>),
RunCommand(Cow<'static, str>),
SuggestCommand(Cow<'static, str>),
ChangePage(i32),
CopyToClipboard(Cow<'static, str>),
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
#[serde(tag = "action", content = "contents", rename_all = "snake_case")]
#[allow(clippy::enum_variant_names)]
enum HoverEvent {
ShowText(Box<Text>),
ShowItem {
id: Ident<String>,
2022-04-14 14:55:45 -07:00
count: Option<i32>,
// TODO: tag
},
ShowEntity {
name: Box<Text>,
#[serde(rename = "type")]
kind: Ident<String>,
2022-04-14 14:55:45 -07:00
// TODO: id (hyphenated entity UUID as a string)
},
}
impl<T: Into<Text>> TextFormat for T {}
impl<T: Into<Text>> std::ops::Add<T> for Text {
type Output = Self;
fn add(self, rhs: T) -> Self::Output {
self.add_child(rhs)
}
}
impl<T: Into<Text>> std::ops::AddAssign<T> for Text {
fn add_assign(&mut self, rhs: T) {
self.extra.push(rhs.into());
}
}
impl From<char> for Text {
fn from(c: char) -> Self {
Text::text(String::from(c))
}
}
2022-04-14 14:55:45 -07:00
impl From<String> for Text {
fn from(s: String) -> Self {
Text::text(s)
}
}
impl From<&'static str> for Text {
fn from(s: &'static str) -> Self {
Text::text(s)
}
}
impl From<Cow<'static, str>> for Text {
fn from(s: Cow<'static, str>) -> Self {
Text::text(s)
}
}
impl<'a> From<&'a Text> for String {
fn from(t: &'a Text) -> Self {
t.to_plain()
}
}
impl<'a, 'b> From<&'a Text> for Cow<'b, str> {
fn from(t: &'a Text) -> Self {
String::from(t).into()
}
}
impl fmt::Display for Text {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.write_plain(f)
2022-04-14 14:55:45 -07:00
}
}
impl Encode for Text {
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
BoundedString::<0, 262144>(serde_json::to_string(self)?).encode(w)
}
fn encoded_len(&self) -> usize {
Redesign packet processing and improve `Client` update procedure. (#146) Closes #82 Closes #43 Closes #64 # Changes and Improvements - Packet encoding/decoding happens within `Client` instead of being sent over a channel first. This is better for performance and lays the groundwork for #83. - Reduce the amount of copying necessary by leveraging the `bytes` crate and recent changes to `EncodePacket`. Performance is noticeably improved with maximum players in the `rust-mc-bot` test going from 750 to 1050. - Packet encoding/decoding code is decoupled from IO. This is easier to understand and more suitable for a future protocol lib. - Precise control over the number of bytes that are buffered for sending/receiving. This is important for limiting maximum memory usage correctly. - "packet controllers" are introduced, which are convenient structures for managing packet IO before and during the play state. - `byte_channel` module is created to help implement the `PlayPacketController`. This is essentially a channel of bytes implemented with an `Arc<Mutex<BytesMut>>`. - Error handling in the update procedure for clients was improved using `anyhow::Result<()>` to exit as early as possible. The `client` module is a bit cleaner as a result. - The `LoginPlay` packet is always sent before all other play packets. We no longer have to worry about the behavior of packets sent before that packet. Most packet deferring performed currently can be eliminated. - The packet_inspector was rewritten in response to the above changes. - Timeouts on IO operations behave better. # Known Issues - The packet_inspector now re-encodes packets rather than just decoding them. This will cause problems when trying to use it with the vanilla server because there are missing clientbound packets and other issues. This will be fixed when the protocol module is moved to a separate crate.
2022-11-01 03:11:51 -07:00
// TODO: This is obviously not ideal. This will be fixed later.
serde_json::to_string(self).map_or(0, |s| s.encoded_len())
}
2022-04-14 14:55:45 -07:00
}
impl Decode for Text {
fn decode(r: &mut &[u8]) -> anyhow::Result<Self> {
2022-04-14 14:55:45 -07:00
let string = BoundedString::<0, 262144>::decode(r)?;
Ok(serde_json::from_str(&string.0)?)
}
}
impl Default for TextContent {
fn default() -> Self {
Self::Text { text: "".into() }
}
}
impl Color {
pub const AQUA: Color = Color::new(85, 255, 255);
pub const BLACK: Color = Color::new(0, 0, 0);
pub const BLUE: Color = Color::new(85, 85, 255);
pub const DARK_AQUA: Color = Color::new(0, 170, 170);
pub const DARK_BLUE: Color = Color::new(0, 0, 170);
pub const DARK_GRAY: Color = Color::new(85, 85, 85);
pub const DARK_GREEN: Color = Color::new(0, 170, 0);
pub const DARK_PURPLE: Color = Color::new(170, 0, 170);
pub const DARK_RED: Color = Color::new(170, 0, 0);
pub const GOLD: Color = Color::new(255, 170, 0);
pub const GRAY: Color = Color::new(170, 170, 170);
pub const GREEN: Color = Color::new(85, 255, 85);
pub const LIGHT_PURPLE: Color = Color::new(255, 85, 255);
pub const RED: Color = Color::new(255, 85, 85);
pub const WHITE: Color = Color::new(255, 255, 255);
pub const YELLOW: Color = Color::new(255, 255, 85);
2022-07-14 23:18:20 -07:00
/// Constructs a new color from red, green, and blue components.
2022-04-14 14:55:45 -07:00
pub const fn new(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b }
}
}
impl Serialize for Color {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b).serialize(serializer)
}
}
impl<'de> Deserialize<'de> for Color {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_str(ColorVisitor)
}
}
struct ColorVisitor;
impl<'de> Visitor<'de> for ColorVisitor {
type Value = Color;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "a hex color of the form #rrggbb")
}
fn visit_str<E: de::Error>(self, s: &str) -> Result<Self::Value, E> {
2022-04-14 14:55:45 -07:00
color_from_str(s).ok_or_else(|| E::custom("invalid hex color"))
}
}
fn color_from_str(s: &str) -> Option<Color> {
let to_num = |d| match d {
b'0'..=b'9' => Some(d - b'0'),
b'a'..=b'f' => Some(d - b'a' + 0xa),
b'A'..=b'F' => Some(d - b'A' + 0xa),
_ => None,
};
match s.as_bytes() {
[b'#', r0, r1, g0, g1, b0, b1] => Some(Color {
r: to_num(*r0)? << 4 | to_num(*r1)?,
g: to_num(*g0)? << 4 | to_num(*g1)?,
b: to_num(*b0)? << 4 | to_num(*b1)?,
}),
2022-09-06 02:24:33 +03:00
_ => match s {
"aqua" => Some(Color::AQUA),
"black" => Some(Color::BLACK),
"blue" => Some(Color::BLUE),
"dark_aqua" => Some(Color::DARK_AQUA),
"dark_blue" => Some(Color::DARK_BLUE),
"dark_gray" => Some(Color::DARK_GRAY),
"dark_green" => Some(Color::DARK_GREEN),
"dark_purple" => Some(Color::DARK_PURPLE),
"dark_red" => Some(Color::DARK_RED),
"gold" => Some(Color::GOLD),
"gray" => Some(Color::GRAY),
"green" => Some(Color::GREEN),
"light_purple" => Some(Color::LIGHT_PURPLE),
"red" => Some(Color::RED),
"white" => Some(Color::WHITE),
"yellow" => Some(Color::YELLOW),
_ => None,
},
2022-04-14 14:55:45 -07:00
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serialize_deserialize() {
let before = "foo".color(Color::RED).bold()
+ ("bar".obfuscated().color(Color::YELLOW)
+ "baz".underlined().not_bold().italic().color(Color::BLACK));
assert_eq!(before.to_plain(), "foobarbaz");
let json = serde_json::to_string_pretty(&before).unwrap();
let after: Text = serde_json::from_str(&json).unwrap();
println!("==== Before ====\n");
println!("{before:#?}");
println!("==== After ====\n");
println!("{after:#?}");
assert_eq!(before, after);
assert_eq!(before.to_plain(), after.to_plain());
}
#[test]
fn color() {
assert_eq!(
color_from_str("#aBcDeF"),
Some(Color::new(0xab, 0xcd, 0xef))
);
assert_eq!(color_from_str("#fFfFfF"), Some(Color::new(255, 255, 255)));
assert_eq!(color_from_str("#00000000"), None);
assert_eq!(color_from_str("#000000"), Some(Color::BLACK));
assert_eq!(color_from_str("#"), None);
2022-09-06 02:24:33 +03:00
assert_eq!(color_from_str("red"), Some(Color::RED));
assert_eq!(color_from_str("blue"), Some(Color::BLUE));
2022-04-14 14:55:45 -07:00
}
2022-07-06 00:16:07 -07:00
#[test]
fn empty() {
assert!("".into_text().is_empty());
let txt = "".into_text() + Text::translate("") + ("".italic().color(Color::RED) + "");
assert!(txt.is_empty());
assert!(txt.to_plain().is_empty());
}
2022-04-14 14:55:45 -07:00
}