1
2
Fork 0
mirror of https://github.com/mat-1/azalea.git synced 2025-08-02 14:26:04 +00:00

merge main

This commit is contained in:
mat 2023-02-21 19:06:10 +00:00
commit 7124e65857
26 changed files with 439 additions and 132 deletions

View file

@ -3,19 +3,31 @@
# Add the contents of this file to `config.toml` to enable "fast build" configuration. Please read the notes below.
# NOTE: For maximum performance, build using a nightly compiler
# If you are using rust stable, remove the "-Zshare-generics=y" below.
[target.x86_64-unknown-linux-gnu]
linker = "/usr/bin/clang"
rustflags = ["-Clink-arg=-fuse-ld=lld"]
linker = "clang"
rustflags = ["-Clink-arg=-fuse-ld=lld", "-Zshare-generics=y"]
# NOTE: you must manually install https://github.com/michaeleisel/zld on mac. you can easily do this with the "brew" package manager:
# `brew install michaeleisel/zld/zld`
# NOTE: you must install [Mach-O LLD Port](https://lld.llvm.org/MachO/index.html) on mac. you can easily do this by installing llvm which includes lld with the "brew" package manager:
# `brew install llvm`
[target.x86_64-apple-darwin]
rustflags = ["-C", "link-arg=-fuse-ld=/usr/local/bin/zld"]
rustflags = [
"-C",
"link-arg=-fuse-ld=/usr/local/opt/llvm/bin/ld64.lld",
"-Zshare-generics=y",
]
[target.aarch64-apple-darwin]
rustflags = [
"-C",
"link-arg=-fuse-ld=/opt/homebrew/opt/llvm/bin/ld64.lld",
"-Zshare-generics=y",
]
[target.x86_64-pc-windows-msvc]
linker = "rust-lld.exe"
rustflags = ["-Zshare-generics=y"]
rustflags = ["-Zshare-generics=n"]
# Optional: Uncommenting the following improves compile times, but reduces the amount of debug info to 'line number tables only'
# In most cases the gains are negligible, but if you are on macos and have slow compile times you should see significant gains.

View file

@ -1,6 +1,12 @@
//! Implementations of chat-related features.
use azalea_chat::FormattedText;
use azalea_ecs::{
app::{App, Plugin},
entity::Entity,
event::{EventReader, EventWriter},
schedule::IntoSystemDescriptor,
};
use azalea_protocol::packets::game::{
clientbound_player_chat_packet::ClientboundPlayerChatPacket,
clientbound_system_chat_packet::ClientboundSystemChatPacket,
@ -13,7 +19,7 @@ use std::{
};
use uuid::Uuid;
use crate::client::Client;
use crate::{client::Client, local_player::SendPacketEvent};
/// A chat packet, either a system message or a chat message.
#[derive(Debug, Clone, PartialEq)]
@ -107,42 +113,23 @@ impl Client {
/// whether the message is a command and using the proper packet for you,
/// so you should use that instead.
pub fn send_chat_packet(&self, message: &str) {
// TODO: chat signing
// let signature = sign_message();
let packet = ServerboundChatPacket {
message: message.to_string(),
timestamp: SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time shouldn't be before epoch")
.as_millis()
.try_into()
.expect("Instant should fit into a u64"),
salt: azalea_crypto::make_salt(),
signature: None,
last_seen_messages: LastSeenMessagesUpdate::default(),
}
.get();
self.write_packet(packet);
self.ecs.lock().send_event(SendChatKindEvent {
entity: self.entity,
content: message.to_string(),
kind: ChatPacketKind::Message,
});
self.run_schedule_sender.send(()).unwrap();
}
/// Send a command packet to the server. The `command` argument should not
/// include the slash at the front.
pub fn send_command_packet(&self, command: &str) {
// TODO: chat signing
let packet = ServerboundChatCommandPacket {
command: command.to_string(),
timestamp: SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time shouldn't be before epoch")
.as_millis()
.try_into()
.expect("Instant should fit into a u64"),
salt: azalea_crypto::make_salt(),
argument_signatures: vec![],
last_seen_messages: LastSeenMessagesUpdate::default(),
}
.get();
self.write_packet(packet);
self.ecs.lock().send_event(SendChatKindEvent {
entity: self.entity,
content: command.to_string(),
kind: ChatPacketKind::Command,
});
self.run_schedule_sender.send(()).unwrap();
}
/// Send a message in chat.
@ -154,15 +141,132 @@ impl Client {
/// # Ok(())
/// # }
/// ```
pub fn chat(&self, message: &str) {
if let Some(command) = message.strip_prefix('/') {
self.send_command_packet(command);
pub fn chat(&self, content: &str) {
self.ecs.lock().send_event(SendChatEvent {
entity: self.entity,
content: content.to_string(),
});
}
}
pub struct ChatPlugin;
impl Plugin for ChatPlugin {
fn build(&self, app: &mut App) {
app.add_event::<SendChatEvent>()
.add_event::<SendChatKindEvent>()
.add_event::<ChatReceivedEvent>()
.add_system(
handle_send_chat_event
.label("handle_send_chat_event")
.after("packet"),
)
.add_system(
handle_send_chat_kind_event
.label("handle_send_chat_kind_event")
.after("handle_send_chat_event"),
);
}
}
/// A client received a chat message packet.
#[derive(Debug, Clone)]
pub struct ChatReceivedEvent {
pub entity: Entity,
pub packet: ChatPacket,
}
/// Send a chat message (or command, if it starts with a slash) to the server.
pub struct SendChatEvent {
pub entity: Entity,
pub content: String,
}
fn handle_send_chat_event(
mut events: EventReader<SendChatEvent>,
mut send_chat_kind_events: EventWriter<SendChatKindEvent>,
) {
for event in events.iter() {
if event.content.starts_with('/') {
send_chat_kind_events.send(SendChatKindEvent {
entity: event.entity,
content: event.content[1..].to_string(),
kind: ChatPacketKind::Command,
});
} else {
self.send_chat_packet(message);
send_chat_kind_events.send(SendChatKindEvent {
entity: event.entity,
content: event.content.clone(),
kind: ChatPacketKind::Message,
});
}
}
}
/// Send a chat packet to the server of a specific kind (chat message or
/// command). Usually you just want [`SendChatEvent`] instead.
///
/// Usually setting the kind to `Message` will make it send a chat message even
/// if it starts with a slash, but some server implementations will always do a
/// command if it starts with a slash.
///
/// If you're wondering why this isn't two separate events, it's so ordering is
/// preserved if multiple chat messages and commands are sent at the same time.
pub struct SendChatKindEvent {
pub entity: Entity,
pub content: String,
pub kind: ChatPacketKind,
}
/// A kind of chat packet, either a chat message or a command.
pub enum ChatPacketKind {
Message,
Command,
}
fn handle_send_chat_kind_event(
mut events: EventReader<SendChatKindEvent>,
mut send_packet_events: EventWriter<SendPacketEvent>,
) {
for event in events.iter() {
let packet = match event.kind {
ChatPacketKind::Message => ServerboundChatPacket {
message: event.content.clone(),
timestamp: SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time shouldn't be before epoch")
.as_millis()
.try_into()
.expect("Instant should fit into a u64"),
salt: azalea_crypto::make_salt(),
signature: None,
last_seen_messages: LastSeenMessagesUpdate::default(),
}
.get(),
ChatPacketKind::Command => {
// TODO: chat signing
ServerboundChatCommandPacket {
command: event.content.clone(),
timestamp: SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time shouldn't be before epoch")
.as_millis()
.try_into()
.expect("Instant should fit into a u64"),
salt: azalea_crypto::make_salt(),
argument_signatures: vec![],
last_seen_messages: LastSeenMessagesUpdate::default(),
}
.get()
}
};
send_packet_events.send(SendPacketEvent {
entity: event.entity,
packet,
});
}
}
// TODO
// MessageSigner, ChatMessageContent, LastSeenMessages
// fn sign_message() -> MessageSignature {

View file

@ -1,12 +1,14 @@
pub use crate::chat::ChatPacket;
use crate::{
chat::ChatPlugin,
disconnect::{DisconnectEvent, DisconnectPlugin},
events::{Event, EventPlugin, LocalPlayerEvents},
inventory::{InventoryComponent, InventoryPlugin},
local_player::{
death_event, update_in_loaded_chunk, GameProfileComponent, LocalPlayer, PhysicsState,
death_event, handle_send_packet_event, update_in_loaded_chunk, GameProfileComponent,
LocalPlayer, PhysicsState, SendPacketEvent,
},
movement::{local_player_ai_step, send_position, sprint_listener, walk_listener},
packet_handling::{self, PacketHandlerPlugin},
packet_handling::{self, PacketHandlerPlugin, PacketReceiver},
player::retroactively_add_game_profile_component,
task_pool::TaskPoolPlugin,
Account, PlayerInfo, StartSprintEvent, StartWalkEvent,
@ -16,6 +18,7 @@ use azalea_auth::{game_profile::GameProfile, sessionserver::ClientSessionServerE
use azalea_chat::FormattedText;
use azalea_ecs::{
app::{App, Plugin, PluginGroup, PluginGroupBuilder},
bundle::Bundle,
component::Component,
entity::Entity,
schedule::{IntoSystemDescriptor, Schedule, Stage, SystemSet},
@ -81,6 +84,9 @@ pub struct Client {
/// directly. Note that if you're using a shared world (i.e. a swarm), this
/// will contain all entities in all worlds.
pub ecs: Arc<Mutex<Ecs>>,
/// Use this to force the client to run the schedule outside of a tick.
pub run_schedule_sender: mpsc::UnboundedSender<()>,
}
/// An error that happened while joining the server.
@ -108,7 +114,12 @@ impl Client {
/// Create a new client from the given GameProfile, Connection, and World.
/// You should only use this if you want to change these fields from the
/// defaults, otherwise use [`Client::join`].
pub fn new(profile: GameProfile, entity: Entity, ecs: Arc<Mutex<Ecs>>) -> Self {
pub fn new(
profile: GameProfile,
entity: Entity,
ecs: Arc<Mutex<Ecs>>,
run_schedule_sender: mpsc::UnboundedSender<()>,
) -> Self {
Self {
profile,
// default our id to 0, it'll be set later
@ -116,6 +127,8 @@ impl Client {
world: Arc::new(RwLock::new(PartialWorld::default())),
ecs,
run_schedule_sender,
}
}
@ -147,7 +160,7 @@ impl Client {
let resolved_address = resolver::resolve_address(&address).await?;
// An event that causes the schedule to run. This is only used internally.
let (run_schedule_sender, run_schedule_receiver) = mpsc::channel(1);
let (run_schedule_sender, run_schedule_receiver) = mpsc::unbounded_channel();
let app = init_ecs_app();
let ecs_lock = start_ecs(app, run_schedule_receiver, run_schedule_sender.clone());
@ -168,7 +181,7 @@ impl Client {
account: &Account,
address: &ServerAddress,
resolved_address: &SocketAddr,
run_schedule_sender: mpsc::Sender<()>,
run_schedule_sender: mpsc::UnboundedSender<()>,
) -> Result<(Self, mpsc::UnboundedReceiver<Event>), JoinError> {
let conn = Connection::new(resolved_address).await?;
let (conn, game_profile) = Self::handshake(conn, account, address).await?;
@ -183,7 +196,12 @@ impl Client {
let entity = entity_mut.id();
// we got the GameConnection, so the server is now connected :)
let client = Client::new(game_profile.clone(), entity, ecs_lock.clone());
let client = Client::new(
game_profile.clone(),
entity,
ecs_lock.clone(),
run_schedule_sender.clone(),
);
let (packet_writer_sender, packet_writer_receiver) = mpsc::unbounded_channel();
@ -210,15 +228,15 @@ impl Client {
local_player.tasks.push(read_packets_task);
local_player.tasks.push(write_packets_task);
ecs.entity_mut(entity).insert((
ecs.entity_mut(entity).insert(JoinedClientBundle {
local_player,
packet_receiver,
GameProfileComponent(game_profile),
PhysicsState::default(),
Local,
LocalPlayerEvents(tx),
InventoryComponent::default(),
));
game_profile: GameProfileComponent(game_profile),
physics_state: PhysicsState::default(),
local_player_events: LocalPlayerEvents(tx),
inventory: InventoryComponent::default(),
_local: Local,
});
Ok((client, rx))
}
@ -357,7 +375,9 @@ impl Client {
/// The OwnedReadHalf for the TCP connection is in one of the tasks, so it
/// automatically closes the connection when that's dropped.
pub fn disconnect(&self) {
self.local_player_mut(&mut self.ecs.lock()).disconnect();
self.ecs.lock().send_event(DisconnectEvent {
entity: self.entity,
});
}
pub fn local_player<'a>(&'a self, ecs: &'a mut Ecs) -> &'a LocalPlayer {
@ -454,14 +474,25 @@ impl Client {
}
}
/// A bundle for the components that are present on a local player that received
/// a login packet. If you want to filter for this, just use [`Local`].
#[derive(Bundle)]
pub struct JoinedClientBundle {
pub local_player: LocalPlayer,
pub packet_receiver: PacketReceiver,
pub game_profile: GameProfileComponent,
pub physics_state: PhysicsState,
pub local_player_events: LocalPlayerEvents,
pub inventory: InventoryComponent,
pub _local: Local,
}
pub struct AzaleaPlugin;
impl Plugin for AzaleaPlugin {
fn build(&self, app: &mut App) {
app.add_event::<StartWalkEvent>()
.add_event::<StartSprintEvent>();
app.add_plugins(DefaultPlugins);
app.add_tick_system_set(
SystemSet::new()
.with_system(send_position)
@ -492,6 +523,9 @@ impl Plugin for AzaleaPlugin {
.after("packet"),
);
app.add_event::<SendPacketEvent>()
.add_system(handle_send_packet_event.after("tick").after("packet"));
app.init_resource::<WorldContainer>();
}
}
@ -511,7 +545,7 @@ pub fn init_ecs_app() -> App {
// you might be able to just drop the lock or put it in its own scope to fix
let mut app = App::new();
app.add_plugin(AzaleaPlugin);
app.add_plugins(DefaultPlugins);
app
}
@ -520,8 +554,8 @@ pub fn init_ecs_app() -> App {
#[doc(hidden)]
pub fn start_ecs(
app: App,
run_schedule_receiver: mpsc::Receiver<()>,
run_schedule_sender: mpsc::Sender<()>,
run_schedule_receiver: mpsc::UnboundedReceiver<()>,
run_schedule_sender: mpsc::UnboundedSender<()>,
) -> Arc<Mutex<Ecs>> {
// all resources should have been added by now so we can take the ecs from the
// app
@ -540,7 +574,7 @@ pub fn start_ecs(
async fn run_schedule_loop(
ecs: Arc<Mutex<Ecs>>,
mut schedule: Schedule,
mut run_schedule_receiver: mpsc::Receiver<()>,
mut run_schedule_receiver: mpsc::UnboundedReceiver<()>,
) {
loop {
// whenever we get an event from run_schedule_receiver, run the schedule
@ -551,14 +585,14 @@ async fn run_schedule_loop(
/// Send an event to run the schedule every 50 milliseconds. It will stop when
/// the receiver is dropped.
pub async fn tick_run_schedule_loop(run_schedule_sender: mpsc::Sender<()>) {
pub async fn tick_run_schedule_loop(run_schedule_sender: mpsc::UnboundedSender<()>) {
let mut game_tick_interval = time::interval(time::Duration::from_millis(50));
// TODO: Minecraft bursts up to 10 ticks and then skips, we should too
game_tick_interval.set_missed_tick_behavior(time::MissedTickBehavior::Burst);
loop {
game_tick_interval.tick().await;
if let Err(e) = run_schedule_sender.send(()).await {
if let Err(e) = run_schedule_sender.send(()) {
println!("tick_run_schedule_loop error: {e}");
// the sender is closed so end the task
return;
@ -574,11 +608,14 @@ impl PluginGroup for DefaultPlugins {
fn build(self) -> PluginGroupBuilder {
PluginGroupBuilder::start::<Self>()
.add(TickPlugin::default())
.add(AzaleaPlugin)
.add(PacketHandlerPlugin)
.add(EntityPlugin)
.add(PhysicsPlugin)
.add(EventPlugin)
.add(TaskPoolPlugin::default())
.add(InventoryPlugin)
.add(ChatPlugin)
.add(DisconnectPlugin)
}
}

View file

@ -0,0 +1,31 @@
//! Disconnect a client from the server.
use azalea_ecs::{
app::{App, CoreStage, Plugin},
entity::Entity,
event::EventReader,
system::Commands,
};
use crate::client::JoinedClientBundle;
pub struct DisconnectPlugin;
impl Plugin for DisconnectPlugin {
fn build(&self, app: &mut App) {
app.add_event::<DisconnectEvent>()
.add_system_to_stage(CoreStage::PostUpdate, handle_disconnect);
}
}
/// An event sent when a client is getting disconnected.
pub struct DisconnectEvent {
pub entity: Entity,
}
/// System that removes the [`JoinedClientBundle`] from the entity when it
/// receives a [`DisconnectEvent`].
pub fn handle_disconnect(mut commands: Commands, mut events: EventReader<DisconnectEvent>) {
for DisconnectEvent { entity } in events.iter() {
commands.entity(*entity).remove::<JoinedClientBundle>();
}
}

View file

@ -19,11 +19,12 @@ use derive_more::{Deref, DerefMut};
use tokio::sync::mpsc;
use crate::{
chat::{ChatPacket, ChatReceivedEvent},
packet_handling::{
AddPlayerEvent, ChatReceivedEvent, DeathEvent, KeepAliveEvent, PacketReceiver,
RemovePlayerEvent, UpdatePlayerEvent,
AddPlayerEvent, DeathEvent, KeepAliveEvent, PacketReceiver, RemovePlayerEvent,
UpdatePlayerEvent,
},
ChatPacket, PlayerInfo,
PlayerInfo,
};
// (for contributors):

View file

@ -12,8 +12,9 @@
#![feature(type_alias_impl_trait)]
mod account;
mod chat;
pub mod chat;
mod client;
pub mod disconnect;
mod entity_query;
mod events;
mod get_mc_dir;
@ -27,7 +28,7 @@ pub mod task_pool;
pub use account::Account;
pub use azalea_ecs as ecs;
pub use client::{init_ecs_app, start_ecs, ChatPacket, Client, ClientInformation, JoinError};
pub use client::{init_ecs_app, start_ecs, Client, ClientInformation, JoinError};
pub use events::Event;
pub use local_player::{GameProfileComponent, LocalPlayer};
pub use movement::{SprintDirection, StartSprintEvent, StartWalkEvent, WalkDirection};

View file

@ -4,6 +4,7 @@ use azalea_auth::game_profile::GameProfile;
use azalea_core::ChunkPos;
use azalea_ecs::component::Component;
use azalea_ecs::entity::Entity;
use azalea_ecs::event::EventReader;
use azalea_ecs::{query::Added, system::Query};
use azalea_protocol::packets::game::ServerboundGamePacket;
use azalea_world::{
@ -111,12 +112,11 @@ impl LocalPlayer {
.send(packet)
.expect("write_packet shouldn't be able to be called if the connection is closed");
}
}
/// Disconnect this client from the server by ending all tasks.
///
/// The OwnedReadHalf for the TCP connection is in one of the tasks, so it
/// automatically closes the connection when that's dropped.
pub fn disconnect(&self) {
impl Drop for LocalPlayer {
/// Stop every active task when the `LocalPlayer` is dropped.
fn drop(&mut self) {
for task in &self.tasks {
task.abort();
}
@ -168,3 +168,20 @@ impl<T> From<std::sync::PoisonError<T>> for HandlePacketError {
HandlePacketError::Poison(e.to_string())
}
}
/// Event for sending a packet to the server.
pub struct SendPacketEvent {
pub entity: Entity,
pub packet: ServerboundGamePacket,
}
pub fn handle_send_packet_event(
mut send_packet_events: EventReader<SendPacketEvent>,
mut query: Query<&mut LocalPlayer>,
) {
for event in send_packet_events.iter() {
if let Ok(mut local_player) = query.get_mut(event.entity) {
local_player.write_packet(event.packet.clone());
}
}
}

View file

@ -37,9 +37,11 @@ use parking_lot::Mutex;
use tokio::sync::mpsc;
use crate::{
chat::{ChatPacket, ChatReceivedEvent},
disconnect::DisconnectEvent,
inventory::{ClientSideCloseContainerEvent, InventoryComponent},
local_player::{GameProfileComponent, LocalPlayer},
ChatPacket, ClientInformation, PlayerInfo,
ClientInformation, PlayerInfo,
};
pub struct PacketHandlerPlugin;
@ -83,13 +85,6 @@ pub struct UpdatePlayerEvent {
pub info: PlayerInfo,
}
/// A client received a chat message packet.
#[derive(Debug, Clone)]
pub struct ChatReceivedEvent {
pub entity: Entity,
pub packet: ChatPacket,
}
/// Event for when an entity dies. dies. If it's a local player and there's a
/// reason in the death screen, the [`ClientboundPlayerCombatKillPacket`] will
/// be included.
@ -113,7 +108,7 @@ pub struct KeepAliveEvent {
#[derive(Component, Clone)]
pub struct PacketReceiver {
pub packets: Arc<Mutex<Vec<ClientboundGamePacket>>>,
pub run_schedule_sender: mpsc::Sender<()>,
pub run_schedule_sender: mpsc::UnboundedSender<()>,
}
fn handle_packets(ecs: &mut Ecs) {
@ -293,10 +288,14 @@ fn handle_packets(ecs: &mut Ecs) {
}
ClientboundGamePacket::Disconnect(p) => {
debug!("Got disconnect packet {:?}", p);
let mut system_state: SystemState<Query<&LocalPlayer>> = SystemState::new(ecs);
let query = system_state.get(ecs);
let local_player = query.get(player_entity).unwrap();
local_player.disconnect();
let mut system_state: SystemState<EventWriter<DisconnectEvent>> =
SystemState::new(ecs);
let mut disconnect_events = system_state.get_mut(ecs);
disconnect_events.send(DisconnectEvent {
entity: player_entity,
});
// bye
return;
}
ClientboundGamePacket::UpdateRecipes(_p) => {
debug!("Got update recipes packet");
@ -938,7 +937,9 @@ fn handle_packets(ecs: &mut Ecs) {
ClientboundGamePacket::BlockChangedAck(_) => {}
ClientboundGamePacket::BlockDestruction(_) => {}
ClientboundGamePacket::BlockEntityData(_) => {}
ClientboundGamePacket::BlockEvent(_) => {}
ClientboundGamePacket::BlockEvent(p) => {
debug!("Got block event packet {:?}", p);
}
ClientboundGamePacket::BossEvent(_) => {}
ClientboundGamePacket::CommandSuggestions(_) => {}
ClientboundGamePacket::Cooldown(_) => {}
@ -1031,16 +1032,20 @@ impl PacketReceiver {
Ok(packet) => {
self.packets.lock().push(packet);
// tell the client to run all the systems
self.run_schedule_sender.send(()).await.unwrap();
self.run_schedule_sender.send(()).unwrap();
}
Err(error) => {
if !matches!(*error, ReadPacketError::ConnectionClosed) {
error!("Error reading packet from Client: {error:?}");
}
return;
break;
}
}
}
// TODO: it should send a DisconnectEvent here somehow
// maybe use a tokio::sync::oneshot that tells it to close and have the
// receiver in localplayer and have a system that watches that or
// something?
}
/// Consume the [`ServerboundGamePacket`] queue and actually write the
@ -1057,6 +1062,7 @@ impl PacketReceiver {
break;
};
}
println!("Write task finished");
// receiver is automatically closed when it's dropped
}
}

View file

@ -1,5 +1,6 @@
use crate::{BlockPos, Direction, Vec3};
#[derive(Debug, Clone, Copy)]
pub struct BlockHitResult {
pub location: Vec3,
pub direction: Direction,

View file

@ -5,7 +5,7 @@ use std::{
use azalea_buf::{BufReadError, McBufReadable, McBufWritable};
#[derive(Hash, Clone, Debug, PartialEq, Eq)]
#[derive(Hash, Clone, Copy, Debug, PartialEq, Eq)]
pub enum Difficulty {
PEACEFUL = 0,
EASY = 1,

View file

@ -97,7 +97,7 @@ impl McBufWritable for GameType {
/// Rust doesn't let us `impl McBufReadable for Option<GameType>` so we have to
/// make a new type :(
#[derive(Hash, Copy, Clone, Debug)]
pub struct OptionalGameType(Option<GameType>);
pub struct OptionalGameType(pub Option<GameType>);
impl From<Option<GameType>> for OptionalGameType {
fn from(game_type: Option<GameType>) -> Self {

View file

@ -20,13 +20,13 @@ pub struct BlockCollisions<'a> {
impl<'a> BlockCollisions<'a> {
pub fn new(world: &'a World, aabb: AABB) -> Self {
let origin_x = (aabb.min_x - EPSILON) as i32 - 1;
let origin_y = (aabb.min_y - EPSILON) as i32 - 1;
let origin_z = (aabb.min_z - EPSILON) as i32 - 1;
let origin_x = (aabb.min_x - EPSILON).floor() as i32 - 1;
let origin_y = (aabb.min_y - EPSILON).floor() as i32 - 1;
let origin_z = (aabb.min_z - EPSILON).floor() as i32 - 1;
let end_x = (aabb.max_x + EPSILON) as i32 + 1;
let end_y = (aabb.max_y + EPSILON) as i32 + 1;
let end_z = (aabb.max_z + EPSILON) as i32 + 1;
let end_x = (aabb.max_x + EPSILON).floor() as i32 + 1;
let end_y = (aabb.max_y + EPSILON).floor() as i32 + 1;
let end_z = (aabb.max_z + EPSILON).floor() as i32 + 1;
let cursor = Cursor3d::new(origin_x, origin_y, origin_z, end_x, end_y, end_z);

View file

@ -588,7 +588,68 @@ mod tests {
block_state.is_some(),
"Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed"
);
// do a few steps so we fall on the slab
// do a few steps so we fall on the wall
for _ in 0..20 {
app.update();
}
let entity_pos = app.world.get::<Position>(entity).unwrap();
assert_eq!(entity_pos.y, 70.5);
}
#[test]
fn test_negative_coordinates_weird_wall_collision() {
let mut app = make_test_app();
let world_lock = app.world.resource_mut::<WorldContainer>().insert(
ResourceLocation::new("minecraft:overworld").unwrap(),
384,
-64,
);
let mut partial_world = PartialWorld::default();
partial_world.chunks.set(
&ChunkPos { x: -1, z: -1 },
Some(Chunk::default()),
&mut world_lock.write().chunks,
);
let entity = app
.world
.spawn((
EntityBundle::new(
Uuid::nil(),
Vec3 {
x: -7.5,
y: 73.,
z: -7.5,
},
azalea_registry::EntityKind::Player,
ResourceLocation::new("minecraft:overworld").unwrap(),
),
MinecraftEntityId(0),
Local,
))
.id();
let block_state = world_lock.write().chunks.set_block_state(
&BlockPos {
x: -8,
y: 69,
z: -8,
},
azalea_block::CobblestoneWallBlock {
east: azalea_block::EastWall::Low,
north: azalea_block::NorthWall::Low,
south: azalea_block::SouthWall::Low,
west: azalea_block::WestWall::Low,
up: false,
waterlogged: false,
}
.into(),
);
assert!(
block_state.is_some(),
"Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed"
);
// do a few steps so we fall on the wall
for _ in 0..20 {
app.update();
}

View file

@ -79,14 +79,18 @@ impl Display for ServerAddress {
#[cfg(test)]
mod tests {
use std::io::Cursor;
use std::{
io::Cursor,
time::{SystemTime, UNIX_EPOCH},
};
use crate::{
packets::login::{
serverbound_hello_packet::ServerboundHelloPacket, ServerboundLoginPacket,
packets::{
game::serverbound_chat_packet::{LastSeenMessagesUpdate, ServerboundChatPacket},
login::{serverbound_hello_packet::ServerboundHelloPacket, ServerboundLoginPacket},
},
read::read_packet,
write::write_packet,
read::{compression_decoder, read_packet},
write::{compression_encoder, packet_encoder, write_packet},
};
use bytes::BytesMut;
use uuid::Uuid;
@ -140,4 +144,29 @@ mod tests {
.await
.unwrap();
}
#[tokio::test]
async fn test_read_long_compressed_chat() {
let compression_threshold = 256;
let buf = packet_encoder(
&ServerboundChatPacket {
message: "a".repeat(256),
timestamp: 0,
salt: 0,
signature: None,
last_seen_messages: LastSeenMessagesUpdate::default(),
}
.get(),
)
.unwrap();
let buf = compression_encoder(&buf, compression_threshold)
.await
.unwrap();
println!("{:?}", buf);
compression_decoder(&mut Cursor::new(&buf), compression_threshold).unwrap();
}
}

View file

@ -1,12 +1,12 @@
use azalea_block::BlockState;
use azalea_buf::McBuf;
use azalea_core::BlockPos;
use azalea_protocol_macros::ClientboundGamePacket;
use azalea_registry::Block;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
pub struct ClientboundBlockEventPacket {
pub pos: BlockPos,
pub b0: u8,
pub b1: u8,
pub block: BlockState,
pub action_id: u8,
pub action_parameter: u8,
pub block: Block,
}

View file

@ -18,7 +18,7 @@ pub struct Tags {
}
#[derive(Clone, Debug)]
pub struct TagMap(HashMap<ResourceLocation, Vec<Tags>>);
pub struct TagMap(pub HashMap<ResourceLocation, Vec<Tags>>);
impl McBufReadable for TagMap {
fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {

View file

@ -4,5 +4,5 @@ use azalea_protocol_macros::ServerboundGamePacket;
#[derive(Clone, Debug, McBuf, ServerboundGamePacket)]
pub struct ServerboundChatAckPacket {
#[var]
pub offset: u32,
pub messages: u32,
}

View file

@ -15,6 +15,6 @@ pub struct ServerboundChatPacket {
#[derive(Clone, Debug, McBuf, Default)]
pub struct LastSeenMessagesUpdate {
#[var]
pub offset: u32,
pub messages: u32,
pub acknowledged: FixedBitSet<20>,
}

View file

@ -163,7 +163,7 @@ pub enum DecompressionError {
/// Get the decompressed bytes from a packet. It must have been decrypted
/// first.
fn compression_decoder(
pub fn compression_decoder(
stream: &mut Cursor<&[u8]>,
compression_threshold: u32,
) -> Result<Vec<u8>, DecompressionError> {
@ -264,8 +264,7 @@ mod tests {
use crate::packets::game::ClientboundGamePacket;
use std::io::Cursor;
#[tokio::test]
async fn test_read_packet() {
fn test_read_packet() {
let mut buf: Cursor<&[u8]> = Cursor::new(&[
56, 64, 85, 58, 141, 138, 71, 146, 193, 64, 88, 0, 0, 0, 0, 0, 0, 64, 60, 224, 105, 34,
119, 8, 228, 67, 50, 51, 68, 194, 177, 230, 101, 0, 17, 0,

View file

@ -29,7 +29,7 @@ pub enum PacketEncodeError {
},
}
fn packet_encoder<P: ProtocolPacket + std::fmt::Debug>(
pub fn packet_encoder<P: ProtocolPacket + std::fmt::Debug>(
packet: &P,
) -> Result<Vec<u8>, PacketEncodeError> {
let mut buf = Vec::new();
@ -51,7 +51,7 @@ pub enum PacketCompressError {
Io(#[from] std::io::Error),
}
async fn compression_encoder(
pub async fn compression_encoder(
data: &[u8],
compression_threshold: u32,
) -> Result<Vec<u8>, PacketCompressError> {
@ -60,15 +60,21 @@ async fn compression_encoder(
if n < compression_threshold as usize {
let mut buf = Vec::new();
0.var_write_into(&mut buf)?;
buf.write_all(data).await?;
std::io::Write::write_all(&mut buf, data)?;
Ok(buf)
} else {
// otherwise, compress
let mut deflater = ZlibEncoder::new(data);
// write deflated data to buf
let mut buf = Vec::new();
deflater.read_to_end(&mut buf).await?;
Ok(buf)
let mut compressed_data = Vec::new();
deflater.read_to_end(&mut compressed_data).await?;
// prepend the length
let mut len_prepended_compressed_data = Vec::new();
(data.len() as u32).var_write_into(&mut len_prepended_compressed_data)?;
len_prepended_compressed_data.append(&mut compressed_data);
Ok(len_prepended_compressed_data)
}
}
@ -82,7 +88,7 @@ where
P: ProtocolPacket + Debug,
W: AsyncWrite + Unpin + Send,
{
trace!("Sending packet: {:?}", packet);
trace!("Sending packet: {:?}", packet,);
let mut buf = packet_encoder(packet).unwrap();
if let Some(threshold) = compression_threshold {
buf = compression_encoder(&buf, threshold).await.unwrap();

View file

@ -7,13 +7,13 @@ mod bit_storage;
mod chunk_storage;
mod container;
pub mod entity;
mod palette;
pub mod palette;
mod world;
use std::backtrace::Backtrace;
pub use bit_storage::BitStorage;
pub use chunk_storage::{Chunk, ChunkStorage, PartialChunkStorage};
pub use chunk_storage::{Chunk, ChunkStorage, PartialChunkStorage, Section};
pub use container::*;
use thiserror::Error;
pub use world::*;

View file

@ -77,7 +77,7 @@ Azalea lets you create "swarms", which are a group of bots in the same world tha
# Plugins
Azalea uses [Bevy ECS](https://docs.rs/bevy_ecs) internally to store information about the world and clients. Bevy plugins are more powerful than async handler functions, but more difficult to use. See [pathfinder](azalea/src/pathfinder/mod.rs) as an example of how to make a plugin. You can then enable a plugin by adding `.add_plugin(ExamplePlugin)` in your client/swarm builder.
Azalea uses [Bevy ECS](https://docs.rs/bevy_ecs) internally to store information about the world and clients. Bevy plugins are more powerful than async handler functions, but more difficult to use. See [pathfinder](https://github.com/mat-1/azalea/blob/main/azalea/src/pathfinder/mod.rs) as an example of how to make a plugin. You can then enable a plugin by adding `.add_plugin(ExamplePlugin)` in your client/swarm builder.
Also note that just because something is an entity in the ECS doesn't mean that it's a Minecraft entity. You can filter for that by having `With<MinecraftEntityId>` as a filter.

View file

@ -4,6 +4,8 @@ use azalea::prelude::*;
#[tokio::main]
async fn main() {
env_logger::init();
let account = Account::offline("bot");
// or let account = Account::microsoft("email").await;

View file

@ -135,7 +135,7 @@ where
let resolved_address = resolver::resolve_address(&address).await?;
// An event that causes the schedule to run. This is only used internally.
let (run_schedule_sender, run_schedule_receiver) = mpsc::channel(1);
let (run_schedule_sender, run_schedule_receiver) = mpsc::unbounded_channel();
let ecs_lock = start_ecs(self.app, run_schedule_receiver, run_schedule_sender.clone());
let (bot, mut rx) = Client::start_client(

View file

@ -13,7 +13,7 @@
// in Swarm that's set to the smallest index of all the bots, and we remove all
// messages from the queue that are before that index.
use azalea_client::{packet_handling::ChatReceivedEvent, ChatPacket};
use azalea_client::chat::{ChatPacket, ChatReceivedEvent};
use azalea_ecs::{
app::{App, Plugin},
component::Component,

View file

@ -5,7 +5,7 @@ mod events;
pub mod prelude;
use crate::{bot::DefaultBotPlugins, HandleFn};
use azalea_client::{init_ecs_app, start_ecs, Account, ChatPacket, Client, Event, JoinError};
use azalea_client::{chat::ChatPacket, init_ecs_app, start_ecs, Account, Client, Event, JoinError};
use azalea_ecs::{
app::{App, Plugin, PluginGroup, PluginGroupBuilder},
component::Component,
@ -47,7 +47,7 @@ pub struct Swarm {
bots_tx: mpsc::UnboundedSender<(Option<Event>, Client)>,
swarm_tx: mpsc::UnboundedSender<SwarmEvent>,
run_schedule_sender: mpsc::Sender<()>,
run_schedule_sender: mpsc::UnboundedSender<()>,
}
/// Create a new [`Swarm`].
@ -253,7 +253,7 @@ where
let (bots_tx, mut bots_rx) = mpsc::unbounded_channel();
let (swarm_tx, mut swarm_rx) = mpsc::unbounded_channel();
let (run_schedule_sender, run_schedule_receiver) = mpsc::channel(1);
let (run_schedule_sender, run_schedule_receiver) = mpsc::unbounded_channel();
let ecs_lock = start_ecs(self.app, run_schedule_receiver, run_schedule_sender.clone());
let swarm = Swarm {
@ -509,8 +509,8 @@ impl Swarm {
Ok(bot) => return bot,
Err(e) => {
disconnects += 1;
let delay = (Duration::from_secs(5) * 2u32.pow(disconnects))
.min(Duration::from_secs(120));
let delay = (Duration::from_secs(5) * 2u32.pow(disconnects.min(16)))
.min(Duration::from_secs(15));
let username = account.username.clone();
error!("Error joining as {username}: {e}. Waiting {delay:?} and trying again.");
tokio::time::sleep(delay).await;