mirror of
https://github.com/italicsjenga/valence.git
synced 2025-01-26 05:26:34 +11:00
Add the packet inspector proxy
This commit is contained in:
parent
9a87fda211
commit
a259bdf840
8 changed files with 425 additions and 196 deletions
|
@ -36,7 +36,7 @@ sha1 = "0.10"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
url = {version = "2.2.2", features = ["serde"] }
|
url = { version = "2.2.2", features = ["serde"] }
|
||||||
uuid = "1"
|
uuid = "1"
|
||||||
vek = "0.15"
|
vek = "0.15"
|
||||||
|
|
||||||
|
@ -55,9 +55,12 @@ anyhow = "1"
|
||||||
heck = "0.4"
|
heck = "0.4"
|
||||||
proc-macro2 = "1"
|
proc-macro2 = "1"
|
||||||
quote = "1"
|
quote = "1"
|
||||||
serde = {version = "1", features = ["derive"]}
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# Exposes the raw protocol API
|
# Exposes the raw protocol API
|
||||||
protocol = []
|
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;
|
mod player_list;
|
||||||
pub mod player_textures;
|
pub mod player_textures;
|
||||||
#[cfg(not(feature = "protocol"))]
|
#[cfg(not(feature = "protocol"))]
|
||||||
|
#[allow(unused)]
|
||||||
mod protocol;
|
mod protocol;
|
||||||
#[cfg(feature = "protocol")]
|
#[cfg(feature = "protocol")]
|
||||||
pub mod protocol;
|
pub mod protocol;
|
||||||
|
|
|
@ -99,6 +99,10 @@ impl<W: AsyncWrite + Unpin> Encoder<W> {
|
||||||
pub fn enable_compression(&mut self, threshold: u32) {
|
pub fn enable_compression(&mut self, threshold: u32) {
|
||||||
self.compression_threshold = Some(threshold);
|
self.compression_threshold = Some(threshold);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn into_inner(self) -> W {
|
||||||
|
self.write
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Decoder<R> {
|
pub struct Decoder<R> {
|
||||||
|
@ -222,6 +226,10 @@ impl<R: AsyncRead + Unpin> Decoder<R> {
|
||||||
pub fn enable_compression(&mut self, threshold: u32) {
|
pub fn enable_compression(&mut self, threshold: u32) {
|
||||||
self.compression_threshold = Some(threshold);
|
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
|
/// 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! {
|
def_struct! {
|
||||||
#[derive(PartialEq, Serialize, Deserialize)]
|
#[derive(PartialEq, Serialize, Deserialize)]
|
||||||
Property {
|
Property {
|
||||||
|
@ -366,7 +426,7 @@ pub mod status {
|
||||||
use super::super::*;
|
use super::super::*;
|
||||||
|
|
||||||
def_struct! {
|
def_struct! {
|
||||||
Response 0x00 {
|
StatusResponse 0x00 {
|
||||||
json_response: String
|
json_response: String
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -428,6 +488,24 @@ pub mod login {
|
||||||
threshold: VarInt
|
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 {
|
pub mod c2s {
|
||||||
|
@ -460,6 +538,21 @@ pub mod login {
|
||||||
sig: Vec<u8>, // TODO: bounds?
|
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 {
|
def_packet_group! {
|
||||||
{
|
S2cPlayPacket {
|
||||||
$($packet:ident),* $(,)?
|
AddEntity,
|
||||||
} => {
|
AddExperienceOrb,
|
||||||
/// An enum of all s2c play packets.
|
AddPlayer,
|
||||||
#[derive(Clone, Debug)]
|
Animate,
|
||||||
pub enum S2cPlayPacket {
|
BlockChangeAck,
|
||||||
$($packet($packet)),*
|
BlockDestruction,
|
||||||
}
|
BlockEntityData,
|
||||||
|
BlockEvent,
|
||||||
$(
|
BlockUpdate,
|
||||||
impl From<$packet> for S2cPlayPacket {
|
BossEvent,
|
||||||
fn from(p: $packet) -> S2cPlayPacket {
|
Disconnect,
|
||||||
S2cPlayPacket::$packet(p)
|
EntityEvent,
|
||||||
}
|
ForgetLevelChunk,
|
||||||
}
|
GameEvent,
|
||||||
)*
|
KeepAlive,
|
||||||
|
LevelChunkWithLight,
|
||||||
impl EncodePacket for S2cPlayPacket {
|
Login,
|
||||||
fn encode_packet(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
MoveEntityPosition,
|
||||||
match self {
|
MoveEntityPositionAndRotation,
|
||||||
$(
|
MoveEntityRotation,
|
||||||
Self::$packet(p) => {
|
PlayerChat,
|
||||||
VarInt($packet::PACKET_ID)
|
PlayerInfo,
|
||||||
.encode(w)
|
PlayerPosition,
|
||||||
.context(concat!("failed to write s2c play packet ID for `", stringify!($packet), "`"))?;
|
RemoveEntities,
|
||||||
p.encode(w)
|
RotateHead,
|
||||||
}
|
SectionBlocksUpdate,
|
||||||
)*
|
SetCarriedItem,
|
||||||
}
|
SetChunkCacheCenter,
|
||||||
}
|
SetChunkCacheRadius,
|
||||||
}
|
SpawnPosition,
|
||||||
|
SetEntityMetadata,
|
||||||
#[cfg(test)]
|
SetEntityMotion,
|
||||||
#[test]
|
SetTime,
|
||||||
fn s2c_play_packet_order() {
|
SystemChat,
|
||||||
let ids = [
|
TabList,
|
||||||
$(
|
TeleportEntity,
|
||||||
(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_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 {
|
pub mod c2s {
|
||||||
|
@ -1737,104 +1777,58 @@ pub mod play {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! def_c2s_play_packet_enum {
|
def_packet_group! {
|
||||||
{
|
C2sPlayPacket {
|
||||||
$($packet:ident),* $(,)?
|
AcceptTeleportation,
|
||||||
} => {
|
BlockEntityTagQuery,
|
||||||
/// An enum of all client-to-server play packets.
|
ChangeDifficulty,
|
||||||
#[derive(Clone, Debug)]
|
ChatCommand,
|
||||||
pub enum C2sPlayPacket {
|
Chat,
|
||||||
$($packet($packet)),*
|
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::c2s::C2sPlayPacket;
|
||||||
use crate::protocol::packets::play::s2c::S2cPlayPacket;
|
use crate::protocol::packets::play::s2c::S2cPlayPacket;
|
||||||
use crate::protocol::packets::status::c2s::{PingRequest, StatusRequest};
|
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::packets::{login, Property};
|
||||||
use crate::protocol::{BoundedArray, BoundedString, VarInt};
|
use crate::protocol::{BoundedArray, BoundedString, VarInt};
|
||||||
use crate::util::valid_username;
|
use crate::util::valid_username;
|
||||||
|
@ -547,7 +547,7 @@ async fn handle_status(
|
||||||
.insert("favicon".to_string(), Value::String(buf));
|
.insert("favicon".to_string(), Value::String(buf));
|
||||||
}
|
}
|
||||||
|
|
||||||
c.0.write_packet(&Response {
|
c.0.write_packet(&StatusResponse {
|
||||||
json_response: json.to_string(),
|
json_response: json.to_string(),
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
20
src/text.rs
20
src/text.rs
|
@ -275,8 +275,13 @@ pub trait TextFormat: Into<Text> {
|
||||||
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
|
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
enum TextContent {
|
enum TextContent {
|
||||||
Text { text: Cow<'static, str> },
|
Text {
|
||||||
// TODO: translate
|
text: Cow<'static, str>,
|
||||||
|
},
|
||||||
|
Translate {
|
||||||
|
translate: Cow<'static, str>,
|
||||||
|
// TODO: 'with' field
|
||||||
|
},
|
||||||
// TODO: score
|
// TODO: score
|
||||||
// TODO: entity names
|
// TODO: entity names
|
||||||
// TODO: keybind
|
// TODO: keybind
|
||||||
|
@ -320,15 +325,24 @@ enum HoverEvent {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::self_named_constructors)]
|
||||||
impl Text {
|
impl Text {
|
||||||
pub fn text(plain: impl Into<Cow<'static, str>>) -> Self {
|
pub fn text(plain: impl Into<Cow<'static, str>>) -> Self {
|
||||||
#![allow(clippy::self_named_constructors)]
|
|
||||||
Self {
|
Self {
|
||||||
content: TextContent::Text { text: plain.into() },
|
content: TextContent::Text { text: plain.into() },
|
||||||
..Self::default()
|
..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 {
|
pub fn to_plain(&self) -> String {
|
||||||
let mut res = String::new();
|
let mut res = String::new();
|
||||||
self.write_plain(&mut res)
|
self.write_plain(&mut res)
|
||||||
|
|
Loading…
Add table
Reference in a new issue