PatchBukkit/rust/tests/common/bot.rs
2026-09-18 20:19:58 +02:00

269 lines
12 KiB
Rust

use std::net::SocketAddr;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use pumpkin_data::packet::CURRENT_MC_VERSION;
use pumpkin_protocol::codec::var_int::VarInt;
use pumpkin_protocol::java::client::config::{
CConfigDisconnect, CConfigPing, CFinishConfig, CKnownPacks,
};
use pumpkin_protocol::java::client::login::{CLoginDisconnect, CLoginSuccess, CSetCompression};
use pumpkin_protocol::java::client::play::{CKeepAlive, CPlayerPosition};
use pumpkin_protocol::java::packet_decoder::TCPNetworkDecoder;
use pumpkin_protocol::java::packet_encoder::TCPNetworkEncoder;
use pumpkin_protocol::java::server::config::{SAcknowledgeFinishConfig, SConfigPong, SKnownPacks};
use pumpkin_protocol::java::server::handshake::SHandShake;
use pumpkin_protocol::java::server::login::{SLoginAcknowledged, SLoginStart};
use pumpkin_protocol::java::server::play::{
SChatCommand, SConfirmTeleport, SKeepAlive, SPlayerLoaded, SPlayerPosition, SSwingArm,
};
use pumpkin_protocol::ser::NetworkWriteExt;
use pumpkin_protocol::{ClientPacket, ConnectionState, MultiVersionJavaPacket, ServerPacket};
use pumpkin_util::math::vector3::Vector3;
use tokio::io::{BufReader, BufWriter};
use tokio::net::TcpStream;
use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
use tokio::sync::Mutex;
use tracing::{debug, trace};
use uuid::Uuid;
#[allow(dead_code)]
pub struct BotClient {
pub name: String,
pub uuid: Uuid,
network_writer: Arc<Mutex<TCPNetworkEncoder<BufWriter<OwnedWriteHalf>>>>,
network_reader: Arc<Mutex<TCPNetworkDecoder<BufReader<OwnedReadHalf>>>>,
pub in_play: Arc<AtomicBool>,
pub closed: Arc<AtomicBool>,
}
impl BotClient {
fn write_packet<P: ClientPacket>(packet: &P) -> Result<Vec<u8>, anyhow::Error> {
let mut buf = Vec::new();
buf.write_var_int(&VarInt(P::to_id(CURRENT_MC_VERSION)))?;
packet.write_packet_data(&mut buf, &CURRENT_MC_VERSION)?;
Ok(buf)
}
pub async fn send_packet<P: ClientPacket>(&self, packet: &P) -> anyhow::Result<()> {
let buf = Self::write_packet(packet)?;
let mut encoder = self.network_writer.lock().await;
encoder.write_packet(buf.into()).await?;
encoder.flush().await?;
Ok(())
}
pub async fn connect(address: SocketAddr, name: &str) -> anyhow::Result<Arc<Self>> {
let stream = TcpStream::connect(address).await?;
let (read_half, write_half) = stream.into_split();
let writer = Arc::new(Mutex::new(TCPNetworkEncoder::new(BufWriter::new(
write_half,
))));
let reader = Arc::new(Mutex::new(TCPNetworkDecoder::new(BufReader::new(
read_half,
))));
let bot = Arc::new(Self {
name: name.to_string(),
uuid: Uuid::new_v4(),
network_writer: writer,
network_reader: reader,
in_play: Arc::new(AtomicBool::new(false)),
closed: Arc::new(AtomicBool::new(false)),
});
debug!("[BOT] Connected TCP stream to {address}, sending SHandShake...");
// 1. Send Handshake
bot.send_packet(&SHandShake {
protocol_version: VarInt(CURRENT_MC_VERSION.protocol_version()),
server_address: address.ip().to_string().into_boxed_str(),
server_port: address.port(),
next_state: ConnectionState::Login,
})
.await?;
debug!("[BOT] Sent SHandShake, sending SLoginStart (name={name})...");
// 2. Send Login Start
bot.send_packet(&SLoginStart {
name: name.to_string().into_boxed_str(),
uuid: bot.uuid,
})
.await?;
// 3. Start packet processing loop in background
let bot_clone = bot.clone();
tokio::spawn(async move {
let mut connection_state = ConnectionState::Login;
trace!("[BOT] Background reader loop started in state: {connection_state:?}");
loop {
if bot_clone.closed.load(Ordering::Relaxed) {
trace!("[BOT] Reader loop exiting: closed flag set");
break;
}
let packet_opt = {
let mut reader_guard = bot_clone.network_reader.lock().await;
reader_guard.get_raw_packet().await
};
let raw = match packet_opt {
Ok(raw) => raw,
Err(e) => {
debug!("[BOT] Reader loop error reading packet: {e:?}");
break;
}
};
trace!(
"[BOT] Received raw packet id=0x{:02X} ({}) len={} in state {:?}",
raw.id,
raw.id,
raw.payload.len(),
connection_state
);
match connection_state {
ConnectionState::Login => {
let mut bytebuf = &raw.payload[..];
if raw.id == CSetCompression::to_id(CURRENT_MC_VERSION) {
trace!("[BOT] Login -> CSetCompression");
if let Ok(pkt) =
CSetCompression::read(&mut bytebuf, &CURRENT_MC_VERSION)
{
debug!("[BOT] Enabling compression threshold {}", pkt.threshold.0);
bot_clone
.network_reader
.lock()
.await
.set_compression(pkt.threshold.0 as usize);
bot_clone
.network_writer
.lock()
.await
.set_compression((pkt.threshold.0 as usize, 6));
}
} else if raw.id == CLoginSuccess::to_id(CURRENT_MC_VERSION) {
debug!(
"[BOT] Login -> CLoginSuccess! Sending SLoginAcknowledged and SKnownPacks"
);
let _ = bot_clone.send_packet(&SLoginAcknowledged).await;
connection_state = ConnectionState::Config;
let _ = bot_clone
.send_packet(&SKnownPacks {
known_packs: Vec::new(),
})
.await;
} else if raw.id == CLoginDisconnect::to_id(CURRENT_MC_VERSION) {
debug!("[BOT] Login -> CLoginDisconnect! Server kicked bot in login");
break;
} else {
trace!("[BOT] Unhandled packet in Login state: id=0x{:02X}", raw.id);
}
}
ConnectionState::Config => {
let mut bytebuf = &raw.payload[..];
if raw.id == CKnownPacks::to_id(CURRENT_MC_VERSION) {
trace!("[BOT] Config -> CKnownPacks, responding SKnownPacks");
let _ = bot_clone
.send_packet(&SKnownPacks {
known_packs: Vec::new(),
})
.await;
} else if raw.id == CConfigPing::to_id(CURRENT_MC_VERSION) {
trace!("[BOT] Config -> CConfigPing, responding SConfigPong");
if let Ok(pkt) = CConfigPing::read(&mut bytebuf, &CURRENT_MC_VERSION) {
let _ = bot_clone.send_packet(&SConfigPong { id: pkt.id }).await;
}
} else if raw.id == CFinishConfig::to_id(CURRENT_MC_VERSION) {
debug!("[BOT] Config -> CFinishConfig! Transitioning to Play");
let _ = bot_clone.send_packet(&SAcknowledgeFinishConfig).await;
connection_state = ConnectionState::Play;
} else if raw.id == CConfigDisconnect::to_id(CURRENT_MC_VERSION) {
debug!(
"[BOT] Config -> CConfigDisconnect! Server kicked bot in config"
);
break;
} else {
trace!(
"[BOT] Unhandled packet in Config state: id=0x{:02X}",
raw.id
);
}
}
ConnectionState::Play => {
let mut bytebuf = &raw.payload[..];
if raw.id == CKeepAlive::to_id(CURRENT_MC_VERSION) {
if let Ok(pkt) = CKeepAlive::read(&mut bytebuf, &CURRENT_MC_VERSION) {
let _ = bot_clone
.send_packet(&SKeepAlive {
keep_alive_id: pkt.keep_alive_id,
})
.await;
}
} else if raw.id == CPlayerPosition::to_id(CURRENT_MC_VERSION) {
debug!(
"[BOT] Play -> CPlayerPosition! Confirming teleport and setting in_play=true"
);
if let Ok(pkt) =
CPlayerPosition::read(&mut bytebuf, &CURRENT_MC_VERSION)
{
let _ = bot_clone
.send_packet(&SConfirmTeleport {
teleport_id: pkt.teleport_id,
})
.await;
let _ = bot_clone.send_packet(&SPlayerLoaded).await;
bot_clone.in_play.store(true, Ordering::Release);
}
}
}
_ => {}
}
}
trace!("[BOT] Reader loop ended, marking closed=true");
bot_clone.closed.store(true, Ordering::Release);
});
Ok(bot)
}
pub async fn wait_for_play(&self, timeout: Duration) -> anyhow::Result<()> {
let start = std::time::Instant::now();
while start.elapsed() < timeout {
if self.in_play.load(Ordering::Acquire) {
return Ok(());
}
if self.closed.load(Ordering::Acquire) {
anyhow::bail!("Bot connection closed before entering Play state");
}
tokio::time::sleep(Duration::from_millis(50)).await;
}
anyhow::bail!("Timed out waiting for bot to enter Play state");
}
pub async fn send_command(&self, command: &str) -> anyhow::Result<()> {
self.send_packet(&SChatCommand {
command: command.trim_start_matches('/'),
})
.await
}
pub async fn swing_arm(&self) -> anyhow::Result<()> {
self.send_packet(&SSwingArm { hand: VarInt(0) }).await
}
pub async fn move_to(&self, x: f64, y: f64, z: f64) -> anyhow::Result<()> {
self.send_packet(&SPlayerPosition {
position: Vector3::new(x, y, z),
collision: 1, // on ground
})
.await
}
pub async fn disconnect(&self) {
self.closed.store(true, Ordering::Release);
let mut writer = self.network_writer.lock().await;
let _ = writer.flush().await;
}
}