mirror of
https://github.com/italicsjenga/valence.git
synced 2025-01-11 07:11:30 +11:00
Add the packet inspector proxy
This commit is contained in:
parent
9a87fda211
commit
a259bdf840
|
@ -36,7 +36,7 @@ sha1 = "0.10"
|
|||
sha2 = "0.10"
|
||||
thiserror = "1"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
url = {version = "2.2.2", features = ["serde"] }
|
||||
url = { version = "2.2.2", features = ["serde"] }
|
||||
uuid = "1"
|
||||
vek = "0.15"
|
||||
|
||||
|
@ -55,9 +55,12 @@ anyhow = "1"
|
|||
heck = "0.4"
|
||||
proc-macro2 = "1"
|
||||
quote = "1"
|
||||
serde = {version = "1", features = ["derive"]}
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
[features]
|
||||
# Exposes the raw protocol API
|
||||
protocol = []
|
||||
|
||||
[workspace]
|
||||
members = ["packet-inspector"]
|
||||
|
|
12
packet-inspector/Cargo.toml
Normal file
12
packet-inspector/Cargo.toml
Normal file
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "packet-inspector"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "A simple Minecraft proxy for inspecting packets."
|
||||
|
||||
[dependencies]
|
||||
valence = { path = "..", features = ["protocol"] }
|
||||
clap = { version = "3.2.8", features = ["derive"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
anyhow = "1"
|
||||
chrono = "0.4.19"
|
197
packet-inspector/src/main.rs
Normal file
197
packet-inspector/src/main.rs
Normal file
|
@ -0,0 +1,197 @@
|
|||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use anyhow::bail;
|
||||
use chrono::{Utc, DateTime};
|
||||
use clap::Parser;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio::sync::Semaphore;
|
||||
use valence::protocol::codec::{Decoder, Encoder};
|
||||
use valence::protocol::packets::handshake::{Handshake, HandshakeNextState};
|
||||
use valence::protocol::packets::login::c2s::{EncryptionResponse, LoginStart};
|
||||
use valence::protocol::packets::login::s2c::{LoginSuccess, S2cLoginPacket};
|
||||
use valence::protocol::packets::play::c2s::C2sPlayPacket;
|
||||
use valence::protocol::packets::play::s2c::S2cPlayPacket;
|
||||
use valence::protocol::packets::status::c2s::{PingRequest, StatusRequest};
|
||||
use valence::protocol::packets::status::s2c::{PongResponse, StatusResponse};
|
||||
use valence::protocol::packets::{DecodePacket, EncodePacket};
|
||||
|
||||
#[derive(Parser, Clone, Debug)]
|
||||
#[clap(author, version, about)]
|
||||
struct Cli {
|
||||
/// The socket address to listen for connections on. This is the address
|
||||
/// clients should connect to.
|
||||
client: SocketAddr,
|
||||
/// The socket address the proxy will connect to.
|
||||
server: SocketAddr,
|
||||
|
||||
/// The maximum number of connections allowed to the proxy. By default,
|
||||
/// there is no limit.
|
||||
#[clap(short, long)]
|
||||
max_connections: Option<usize>,
|
||||
|
||||
/// When enabled, prints a timestamp before each packet.
|
||||
#[clap(short, long)]
|
||||
timestamp: bool,
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
fn print(&self, d: &impl fmt::Debug) {
|
||||
if self.timestamp {
|
||||
let now: DateTime<Utc> = Utc::now();
|
||||
println!("{now} {d:?}");
|
||||
} else {
|
||||
println!("{d:?}");
|
||||
}
|
||||
}
|
||||
|
||||
async fn rw_packet<P: DecodePacket + EncodePacket>(
|
||||
&self,
|
||||
read: &mut Decoder<OwnedReadHalf>,
|
||||
write: &mut Encoder<OwnedWriteHalf>,
|
||||
) -> anyhow::Result<P> {
|
||||
let pkt = read.read_packet().await?;
|
||||
self.print(&pkt);
|
||||
write.write_packet(&pkt).await?;
|
||||
Ok(pkt)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
let sema = Arc::new(Semaphore::new(
|
||||
cli.max_connections.unwrap_or(usize::MAX).min(100_000),
|
||||
));
|
||||
|
||||
eprintln!("Waiting for connections on {}", cli.client);
|
||||
let listen = TcpListener::bind(cli.client).await?;
|
||||
|
||||
while let Ok(permit) = sema.clone().acquire_owned().await {
|
||||
let (client, remote_client_addr) = listen.accept().await?;
|
||||
eprintln!("Accepted connection to {remote_client_addr}");
|
||||
|
||||
let cli = cli.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle_connection(client, cli).await {
|
||||
eprintln!("Connection to {remote_client_addr} ended with: {e:#}");
|
||||
} else {
|
||||
eprintln!("Connection to {remote_client_addr} ended.");
|
||||
}
|
||||
drop(permit);
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_connection(client: TcpStream, cli: Cli) -> anyhow::Result<()> {
|
||||
eprintln!("Connecting to {}", cli.server);
|
||||
|
||||
let server = TcpStream::connect(cli.server).await?;
|
||||
|
||||
let (client_read, client_write) = client.into_split();
|
||||
let (server_read, server_write) = server.into_split();
|
||||
|
||||
let timeout = Duration::from_secs(10);
|
||||
|
||||
let mut client_read = Decoder::new(client_read, timeout);
|
||||
let mut client_write = Encoder::new(client_write, timeout);
|
||||
|
||||
let mut server_read = Decoder::new(server_read, timeout);
|
||||
let mut server_write = Encoder::new(server_write, timeout);
|
||||
|
||||
let handshake: Handshake = cli.rw_packet(&mut client_read, &mut server_write).await?;
|
||||
|
||||
match handshake.next_state {
|
||||
HandshakeNextState::Status => {
|
||||
cli.rw_packet::<StatusRequest>(&mut client_read, &mut server_write)
|
||||
.await?;
|
||||
cli.rw_packet::<StatusResponse>(&mut server_read, &mut client_write)
|
||||
.await?;
|
||||
|
||||
cli.rw_packet::<PingRequest>(&mut client_read, &mut server_write)
|
||||
.await?;
|
||||
cli.rw_packet::<PongResponse>(&mut server_read, &mut client_write)
|
||||
.await?;
|
||||
}
|
||||
HandshakeNextState::Login => {
|
||||
cli.rw_packet::<LoginStart>(&mut client_read, &mut server_write)
|
||||
.await?;
|
||||
|
||||
match cli
|
||||
.rw_packet::<S2cLoginPacket>(&mut server_read, &mut client_write)
|
||||
.await?
|
||||
{
|
||||
S2cLoginPacket::EncryptionRequest(_) => {
|
||||
cli.rw_packet::<EncryptionResponse>(&mut client_read, &mut server_write)
|
||||
.await?;
|
||||
|
||||
eprintln!("Encryption was enabled! I can't see what's going on anymore.");
|
||||
|
||||
return tokio::select! {
|
||||
c2s = passthrough(client_read.into_inner(), server_write.into_inner()) => c2s,
|
||||
s2c = passthrough(server_read.into_inner(), client_write.into_inner()) => s2c,
|
||||
};
|
||||
}
|
||||
S2cLoginPacket::SetCompression(pkt) => {
|
||||
let threshold = pkt.threshold.0 as u32;
|
||||
client_read.enable_compression(threshold);
|
||||
client_write.enable_compression(threshold);
|
||||
server_read.enable_compression(threshold);
|
||||
server_write.enable_compression(threshold);
|
||||
|
||||
cli.rw_packet::<LoginSuccess>(&mut server_read, &mut client_write)
|
||||
.await?;
|
||||
}
|
||||
S2cLoginPacket::LoginSuccess(_) => {}
|
||||
S2cLoginPacket::Disconnect(_) => return Ok(()),
|
||||
S2cLoginPacket::LoginPluginRequest(_) => {
|
||||
bail!("got login plugin request. Don't know how to proceed.")
|
||||
}
|
||||
}
|
||||
|
||||
let c2s = async {
|
||||
loop {
|
||||
cli.rw_packet::<C2sPlayPacket>(&mut client_read, &mut server_write)
|
||||
.await?;
|
||||
}
|
||||
};
|
||||
|
||||
let s2c = async {
|
||||
loop {
|
||||
cli.rw_packet::<S2cPlayPacket>(&mut server_read, &mut client_write)
|
||||
.await?;
|
||||
}
|
||||
};
|
||||
|
||||
return tokio::select! {
|
||||
c2s = c2s => c2s,
|
||||
s2c = s2c => s2c,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn passthrough(mut read: OwnedReadHalf, mut write: OwnedWriteHalf) -> anyhow::Result<()> {
|
||||
let mut buf = vec![0u8; 4096].into_boxed_slice();
|
||||
loop {
|
||||
let bytes_read = read.read(&mut buf).await?;
|
||||
let bytes = &mut buf[..bytes_read];
|
||||
|
||||
if bytes.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
write.write_all(bytes).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
|
@ -21,6 +21,7 @@ pub mod ident;
|
|||
mod player_list;
|
||||
pub mod player_textures;
|
||||
#[cfg(not(feature = "protocol"))]
|
||||
#[allow(unused)]
|
||||
mod protocol;
|
||||
#[cfg(feature = "protocol")]
|
||||
pub mod protocol;
|
||||
|
|
|
@ -99,6 +99,10 @@ impl<W: AsyncWrite + Unpin> Encoder<W> {
|
|||
pub fn enable_compression(&mut self, threshold: u32) {
|
||||
self.compression_threshold = Some(threshold);
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> W {
|
||||
self.write
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Decoder<R> {
|
||||
|
@ -222,6 +226,10 @@ impl<R: AsyncRead + Unpin> Decoder<R> {
|
|||
pub fn enable_compression(&mut self, threshold: u32) {
|
||||
self.compression_threshold = Some(threshold);
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> R {
|
||||
self.read
|
||||
}
|
||||
}
|
||||
|
||||
/// The AES block cipher with a 128 bit key, using the CFB-8 mode of
|
||||
|
|
|
@ -321,6 +321,66 @@ macro_rules! def_bitfield {
|
|||
}
|
||||
}
|
||||
|
||||
macro_rules! def_packet_group {
|
||||
(
|
||||
$(#[$attrs:meta])*
|
||||
$group_name:ident {
|
||||
$($packet:ident),* $(,)?
|
||||
}
|
||||
) => {
|
||||
#[derive(Clone, Debug)]
|
||||
$(#[$attrs])*
|
||||
pub enum $group_name {
|
||||
$($packet($packet)),*
|
||||
}
|
||||
|
||||
$(
|
||||
impl From<$packet> for $group_name {
|
||||
fn from(p: $packet) -> Self {
|
||||
Self::$packet(p)
|
||||
}
|
||||
}
|
||||
)*
|
||||
|
||||
impl DecodePacket for $group_name {
|
||||
fn decode_packet(r: &mut impl Read) -> anyhow::Result<Self> {
|
||||
let packet_id = VarInt::decode(r)
|
||||
.context(concat!("failed to read ", stringify!($group_name), " packet ID"))?.0;
|
||||
|
||||
match packet_id {
|
||||
$(
|
||||
$packet::PACKET_ID => {
|
||||
let pkt = $packet::decode(r)?;
|
||||
Ok(Self::$packet(pkt))
|
||||
}
|
||||
)*
|
||||
id => bail!(concat!("unknown ", stringify!($group_name), " packet ID {:#04x}"), id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EncodePacket for $group_name {
|
||||
fn encode_packet(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||
match self {
|
||||
$(
|
||||
Self::$packet(pkt) => {
|
||||
VarInt($packet::PACKET_ID)
|
||||
.encode(w)
|
||||
.context(concat!(
|
||||
"failed to write ",
|
||||
stringify!($group_name),
|
||||
" packet ID for ",
|
||||
stringify!($packet_name)
|
||||
))?;
|
||||
pkt.encode(w)
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def_struct! {
|
||||
#[derive(PartialEq, Serialize, Deserialize)]
|
||||
Property {
|
||||
|
@ -366,7 +426,7 @@ pub mod status {
|
|||
use super::super::*;
|
||||
|
||||
def_struct! {
|
||||
Response 0x00 {
|
||||
StatusResponse 0x00 {
|
||||
json_response: String
|
||||
}
|
||||
}
|
||||
|
@ -428,6 +488,24 @@ pub mod login {
|
|||
threshold: VarInt
|
||||
}
|
||||
}
|
||||
|
||||
def_struct! {
|
||||
LoginPluginRequest 0x04 {
|
||||
message_id: VarInt,
|
||||
channel: Ident,
|
||||
data: RawBytes,
|
||||
}
|
||||
}
|
||||
|
||||
def_packet_group! {
|
||||
S2cLoginPacket {
|
||||
Disconnect,
|
||||
EncryptionRequest,
|
||||
LoginSuccess,
|
||||
SetCompression,
|
||||
LoginPluginRequest,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod c2s {
|
||||
|
@ -460,6 +538,21 @@ pub mod login {
|
|||
sig: Vec<u8>, // TODO: bounds?
|
||||
}
|
||||
}
|
||||
|
||||
def_struct! {
|
||||
LoginPluginResponse 0x02 {
|
||||
message_id: VarInt,
|
||||
data: Option<RawBytes>,
|
||||
}
|
||||
}
|
||||
|
||||
def_packet_group! {
|
||||
C2sLoginPacket {
|
||||
LoginStart,
|
||||
EncryptionResponse,
|
||||
LoginPluginResponse,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1081,99 +1174,46 @@ pub mod play {
|
|||
}
|
||||
}
|
||||
|
||||
macro_rules! def_s2c_play_packet_enum {
|
||||
{
|
||||
$($packet:ident),* $(,)?
|
||||
} => {
|
||||
/// An enum of all s2c play packets.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum S2cPlayPacket {
|
||||
$($packet($packet)),*
|
||||
}
|
||||
|
||||
$(
|
||||
impl From<$packet> for S2cPlayPacket {
|
||||
fn from(p: $packet) -> S2cPlayPacket {
|
||||
S2cPlayPacket::$packet(p)
|
||||
}
|
||||
}
|
||||
)*
|
||||
|
||||
impl EncodePacket for S2cPlayPacket {
|
||||
fn encode_packet(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
||||
match self {
|
||||
$(
|
||||
Self::$packet(p) => {
|
||||
VarInt($packet::PACKET_ID)
|
||||
.encode(w)
|
||||
.context(concat!("failed to write s2c play packet ID for `", stringify!($packet), "`"))?;
|
||||
p.encode(w)
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
fn s2c_play_packet_order() {
|
||||
let ids = [
|
||||
$(
|
||||
(stringify!($packet), $packet::PACKET_ID),
|
||||
)*
|
||||
];
|
||||
|
||||
if let Some(w) = ids.windows(2).find(|w| w[0].1 >= w[1].1) {
|
||||
panic!(
|
||||
"the {} (ID {:#x}) and {} (ID {:#x}) variants of the s2c play packet enum are not properly sorted by their packet ID",
|
||||
w[0].0,
|
||||
w[0].1,
|
||||
w[1].0,
|
||||
w[1].1
|
||||
);
|
||||
}
|
||||
}
|
||||
def_packet_group! {
|
||||
S2cPlayPacket {
|
||||
AddEntity,
|
||||
AddExperienceOrb,
|
||||
AddPlayer,
|
||||
Animate,
|
||||
BlockChangeAck,
|
||||
BlockDestruction,
|
||||
BlockEntityData,
|
||||
BlockEvent,
|
||||
BlockUpdate,
|
||||
BossEvent,
|
||||
Disconnect,
|
||||
EntityEvent,
|
||||
ForgetLevelChunk,
|
||||
GameEvent,
|
||||
KeepAlive,
|
||||
LevelChunkWithLight,
|
||||
Login,
|
||||
MoveEntityPosition,
|
||||
MoveEntityPositionAndRotation,
|
||||
MoveEntityRotation,
|
||||
PlayerChat,
|
||||
PlayerInfo,
|
||||
PlayerPosition,
|
||||
RemoveEntities,
|
||||
RotateHead,
|
||||
SectionBlocksUpdate,
|
||||
SetCarriedItem,
|
||||
SetChunkCacheCenter,
|
||||
SetChunkCacheRadius,
|
||||
SpawnPosition,
|
||||
SetEntityMetadata,
|
||||
SetEntityMotion,
|
||||
SetTime,
|
||||
SystemChat,
|
||||
TabList,
|
||||
TeleportEntity,
|
||||
}
|
||||
}
|
||||
|
||||
def_s2c_play_packet_enum! {
|
||||
AddEntity,
|
||||
AddExperienceOrb,
|
||||
AddPlayer,
|
||||
Animate,
|
||||
BlockChangeAck,
|
||||
BlockDestruction,
|
||||
BlockEntityData,
|
||||
BlockEvent,
|
||||
BlockUpdate,
|
||||
BossEvent,
|
||||
Disconnect,
|
||||
EntityEvent,
|
||||
ForgetLevelChunk,
|
||||
GameEvent,
|
||||
KeepAlive,
|
||||
LevelChunkWithLight,
|
||||
Login,
|
||||
MoveEntityPosition,
|
||||
MoveEntityPositionAndRotation,
|
||||
MoveEntityRotation,
|
||||
PlayerChat,
|
||||
PlayerInfo,
|
||||
PlayerPosition,
|
||||
RemoveEntities,
|
||||
RotateHead,
|
||||
SectionBlocksUpdate,
|
||||
SetCarriedItem,
|
||||
SetChunkCacheCenter,
|
||||
SetChunkCacheRadius,
|
||||
SpawnPosition,
|
||||
SetEntityMetadata,
|
||||
SetEntityMotion,
|
||||
SetTime,
|
||||
SystemChat,
|
||||
TabList,
|
||||
TeleportEntity,
|
||||
}
|
||||
}
|
||||
|
||||
pub mod c2s {
|
||||
|
@ -1737,104 +1777,58 @@ pub mod play {
|
|||
}
|
||||
}
|
||||
|
||||
macro_rules! def_c2s_play_packet_enum {
|
||||
{
|
||||
$($packet:ident),* $(,)?
|
||||
} => {
|
||||
/// An enum of all client-to-server play packets.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum C2sPlayPacket {
|
||||
$($packet($packet)),*
|
||||
def_packet_group! {
|
||||
C2sPlayPacket {
|
||||
AcceptTeleportation,
|
||||
BlockEntityTagQuery,
|
||||
ChangeDifficulty,
|
||||
ChatCommand,
|
||||
Chat,
|
||||
ChatPreview,
|
||||
ClientCommand,
|
||||
ClientInformation,
|
||||
CommandSuggestion,
|
||||
ContainerButtonClick,
|
||||
ContainerClose,
|
||||
CustomPayload,
|
||||
EditBook,
|
||||
EntityTagQuery,
|
||||
Interact,
|
||||
JigsawGenerate,
|
||||
KeepAlive,
|
||||
LockDifficulty,
|
||||
MovePlayerPosition,
|
||||
MovePlayerPositionAndRotation,
|
||||
MovePlayerRotation,
|
||||
MovePlayerStatusOnly,
|
||||
MoveVehicle,
|
||||
PaddleBoat,
|
||||
PickItem,
|
||||
PlaceRecipe,
|
||||
PlayerAbilities,
|
||||
PlayerAction,
|
||||
PlayerCommand,
|
||||
PlayerInput,
|
||||
Pong,
|
||||
RecipeBookChangeSettings,
|
||||
RecipeBookSeenRecipe,
|
||||
RenameItem,
|
||||
ResourcePack,
|
||||
SeenAdvancements,
|
||||
SelectTrade,
|
||||
SetBeacon,
|
||||
SetCarriedItem,
|
||||
SetCommandBlock,
|
||||
SetCommandBlockMinecart,
|
||||
SetCreativeModeSlot,
|
||||
SetJigsawBlock,
|
||||
SetStructureBlock,
|
||||
SignUpdate,
|
||||
Swing,
|
||||
TeleportToEntity,
|
||||
UseItemOn,
|
||||
UseItem,
|
||||
}
|
||||
|
||||
impl DecodePacket for C2sPlayPacket {
|
||||
fn decode_packet(r: &mut impl Read) -> anyhow::Result<C2sPlayPacket> {
|
||||
let packet_id = VarInt::decode(r).context("failed to read c2s play packet ID")?.0;
|
||||
match packet_id {
|
||||
$(
|
||||
$packet::PACKET_ID => {
|
||||
let pkt = $packet::decode(r)?;
|
||||
Ok(C2sPlayPacket::$packet(pkt))
|
||||
}
|
||||
)*
|
||||
id => bail!("unknown c2s play packet ID {:#04x}", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
fn c2s_play_packet_order() {
|
||||
let ids = [
|
||||
$(
|
||||
(stringify!($packet), $packet::PACKET_ID),
|
||||
)*
|
||||
];
|
||||
|
||||
if let Some(w) = ids.windows(2).find(|w| w[0].1 >= w[1].1) {
|
||||
panic!(
|
||||
"the {} (ID {:#x}) and {} (ID {:#x}) variants of the c2s play packet enum are not properly sorted by their packet ID",
|
||||
w[0].0,
|
||||
w[0].1,
|
||||
w[1].0,
|
||||
w[1].1
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def_c2s_play_packet_enum! {
|
||||
AcceptTeleportation,
|
||||
BlockEntityTagQuery,
|
||||
ChangeDifficulty,
|
||||
ChatCommand,
|
||||
Chat,
|
||||
ChatPreview,
|
||||
ClientCommand,
|
||||
ClientInformation,
|
||||
CommandSuggestion,
|
||||
ContainerButtonClick,
|
||||
ContainerClose,
|
||||
CustomPayload,
|
||||
EditBook,
|
||||
EntityTagQuery,
|
||||
Interact,
|
||||
JigsawGenerate,
|
||||
KeepAlive,
|
||||
LockDifficulty,
|
||||
MovePlayerPosition,
|
||||
MovePlayerPositionAndRotation,
|
||||
MovePlayerRotation,
|
||||
MovePlayerStatusOnly,
|
||||
MoveVehicle,
|
||||
PaddleBoat,
|
||||
PickItem,
|
||||
PlaceRecipe,
|
||||
PlayerAbilities,
|
||||
PlayerAction,
|
||||
PlayerCommand,
|
||||
PlayerInput,
|
||||
Pong,
|
||||
RecipeBookChangeSettings,
|
||||
RecipeBookSeenRecipe,
|
||||
RenameItem,
|
||||
ResourcePack,
|
||||
SeenAdvancements,
|
||||
SelectTrade,
|
||||
SetBeacon,
|
||||
SetCarriedItem,
|
||||
SetCommandBlock,
|
||||
SetCommandBlockMinecart,
|
||||
SetCreativeModeSlot,
|
||||
SetJigsawBlock,
|
||||
SetStructureBlock,
|
||||
SignUpdate,
|
||||
Swing,
|
||||
TeleportToEntity,
|
||||
UseItemOn,
|
||||
UseItem,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ use crate::protocol::packets::login::s2c::{EncryptionRequest, LoginSuccess, SetC
|
|||
use crate::protocol::packets::play::c2s::C2sPlayPacket;
|
||||
use crate::protocol::packets::play::s2c::S2cPlayPacket;
|
||||
use crate::protocol::packets::status::c2s::{PingRequest, StatusRequest};
|
||||
use crate::protocol::packets::status::s2c::{PongResponse, Response};
|
||||
use crate::protocol::packets::status::s2c::{PongResponse, StatusResponse};
|
||||
use crate::protocol::packets::{login, Property};
|
||||
use crate::protocol::{BoundedArray, BoundedString, VarInt};
|
||||
use crate::util::valid_username;
|
||||
|
@ -547,7 +547,7 @@ async fn handle_status(
|
|||
.insert("favicon".to_string(), Value::String(buf));
|
||||
}
|
||||
|
||||
c.0.write_packet(&Response {
|
||||
c.0.write_packet(&StatusResponse {
|
||||
json_response: json.to_string(),
|
||||
})
|
||||
.await?;
|
||||
|
|
20
src/text.rs
20
src/text.rs
|
@ -275,8 +275,13 @@ pub trait TextFormat: Into<Text> {
|
|||
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum TextContent {
|
||||
Text { text: Cow<'static, str> },
|
||||
// TODO: translate
|
||||
Text {
|
||||
text: Cow<'static, str>,
|
||||
},
|
||||
Translate {
|
||||
translate: Cow<'static, str>,
|
||||
// TODO: 'with' field
|
||||
},
|
||||
// TODO: score
|
||||
// TODO: entity names
|
||||
// TODO: keybind
|
||||
|
@ -320,15 +325,24 @@ enum HoverEvent {
|
|||
},
|
||||
}
|
||||
|
||||
#[allow(clippy::self_named_constructors)]
|
||||
impl Text {
|
||||
pub fn text(plain: impl Into<Cow<'static, str>>) -> Self {
|
||||
#![allow(clippy::self_named_constructors)]
|
||||
Self {
|
||||
content: TextContent::Text { text: plain.into() },
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn translate(key: impl Into<Cow<'static, str>>) -> Self {
|
||||
Self {
|
||||
content: TextContent::Translate {
|
||||
translate: key.into(),
|
||||
},
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_plain(&self) -> String {
|
||||
let mut res = String::new();
|
||||
self.write_plain(&mut res)
|
||||
|
|
Loading…
Reference in a new issue