* make azalea-pathfinder dir

* start writing d* lite impl

* more work on d* lite

* work more on implementing d* lite

* full d* lite impl

* updated edges

* add next() function

* add NoPathError

* why does dstar lite not work

* fix d* lite implementation

* make the test actually check the coords

* replace while loop with if statement

* fix clippy complaints

* make W only have to be PartialOrd

* fix PartialOrd issues

* implement mtd* lite

* add a test to mtd* lite

* remove normal d* lite

* make heuristic only take in one arg

* add `success` function

* Update README.md

* evil black magic to make .entity not need dimension

* start adding moves

* slightly improve the vec3/position situation

new macro that implements all the useful functions

* moves stuff

* make it compile

* update deps in az-pathfinder

* make it compile again

* more pathfinding stuff

* add Bot::look_at

* replace EntityMut and EntityRef with just Entity

* block pos pathfinding stuff

* rename movedirection to walkdirection

* execute path every tick

* advance path

* change az-pf version

* make azalea_client keep plugin state

* fix Plugins::get

* why does it think there is air

* start debugging incorrect air

* update some From methods to use rem_euclid

* start adding swarm

* fix deadlock

i still don't understand why it was happening but the solution was to keep the Client::player lock for shorter so it didn't overlap with the Client::dimension lock

* make lookat actually work probably

* fix going too fast

* Update main.rs

* make a thing immutable

* direction_looking_at

* fix rotations

* import swarm in an example

* fix stuff from merge

* remove azalea_pathfinder import

* delete azalea-pathfinder crate

already in azalea::pathfinder module

* swarms

* start working on shared dimensions

* Shared worlds work

* start adding Swarm::add_account

* add_account works

* change "client" to "bot" in some places

* Fix issues from merge

* Update world.rs

* add SwarmEvent::Disconnect(Account)

* almost add SwarmEvent::Chat and new plugin system

it panics rn

* make plugins have to provide the State associated type

* improve comments

* make fn build slightly cleaner

* fix SwarmEvent::Chat

* change a println in bot/main.rs

* Client::shutdown -> disconnect

* polish

fix clippy warnings + improve some docs a bit

* fix shared worlds*

*there's a bug that entities and bots will have their positions exaggerated because the relative movement packet is applied for every entity once per bot

* i am being trolled by rust

for some reason some stuff is really slow for literally no reason and it makes no sense i am going insane

* make world an RwLock again

* remove debug messages

* fix skipping event ticks

unfortunately now sending events is `.send().await?` instead of just `.send()`

* fix deadlock + warnings

* turns out my floor_mod impl was wrong

and i32::rem_euclid has the correct behavior LOL

* still errors with lots of bots

* make swarm iter & fix new chunks not loading

* improve docs

* start fixing tests

* fix all the tests

except the examples i don't know how to exclude them from the tests

* improve docs some more
This commit is contained in:
mat 2022-11-27 16:25:07 -06:00 committed by GitHub
parent 962b9fcaae
commit 631ed63dbd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 2262 additions and 830 deletions

14
Cargo.lock generated
View file

@ -114,18 +114,22 @@ dependencies = [
"anyhow",
"async-trait",
"azalea-block",
"azalea-chat",
"azalea-client",
"azalea-core",
"azalea-physics",
"azalea-protocol",
"azalea-world",
"env_logger",
"futures",
"log",
"nohash-hasher",
"num-traits",
"parking_lot",
"priority-queue",
"thiserror",
"tokio",
"uuid",
]
[[package]]
@ -402,8 +406,10 @@ version = "0.2.0"
dependencies = [
"anyhow",
"azalea",
"azalea-protocol",
"env_logger",
"parking_lot",
"rand",
"tokio",
"uuid",
]
@ -467,9 +473,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.22"
version = "0.4.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1"
checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
dependencies = [
"num-integer",
"num-traits",
@ -2085,9 +2091,9 @@ dependencies = [
[[package]]
name = "uuid"
version = "1.2.1"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "feb41e78f93363bb2df8b0e86a2ca30eed7806ea16ea0c790d757cf93f79be83"
checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c"
[[package]]
name = "vcpkg"

View file

@ -209,6 +209,7 @@ pub struct GameOwnershipItem {
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ProfileResponse {
// todo: make the id a uuid
pub id: String,
pub name: String,
pub skins: Vec<serde_json::Value>,
@ -463,7 +464,7 @@ pub enum GetProfileError {
Http(#[from] reqwest::Error),
}
async fn get_profile(
pub async fn get_profile(
client: &reqwest::Client,
minecraft_access_token: &str,
) -> Result<ProfileResponse, GetProfileError> {

View file

@ -1,6 +1,6 @@
use crate::{style::Style, Component};
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq)]
pub struct BaseComponent {
// implements mutablecomponent
pub siblings: Vec<Component>,

View file

@ -13,7 +13,7 @@ use std::{
};
/// A chat component, basically anything you can see in chat.
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq)]
pub enum Component {
Text(TextComponent),
Translatable(TranslatableComponent),
@ -63,13 +63,14 @@ impl Component {
///
/// ```rust
/// use azalea_chat::Component;
/// use serde::de::Deserialize;
///
/// let component = Component::deserialize(&serde_json::json!({
/// "text": "Hello, world!",
/// "color": "red",
/// })).unwrap();
///
/// println!("{}", component.to_ansi());
/// println!("{}", component.to_ansi(None));
/// ```
pub fn to_ansi(&self, default_style: Option<&Style>) -> String {
// default the default_style to white if it's not set

View file

@ -274,7 +274,7 @@ impl TryFrom<ChatFormatting> for TextColor {
}
}
#[derive(Clone, Debug, Default)]
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Style {
// these are options instead of just bools because None is different than false in this case
pub color: Option<TextColor>,

View file

@ -3,7 +3,7 @@ use std::fmt::Display;
use crate::{base_component::BaseComponent, style::ChatFormatting, Component};
/// A component that contains text that's the same in all locales.
#[derive(Clone, Debug, Default)]
#[derive(Clone, Debug, Default, PartialEq)]
pub struct TextComponent {
pub base: BaseComponent,
pub text: String,

View file

@ -4,14 +4,14 @@ use crate::{
base_component::BaseComponent, style::Style, text_component::TextComponent, Component,
};
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq)]
pub enum StringOrComponent {
String(String),
Component(Component),
}
/// A message whose content depends on the client's language.
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq)]
pub struct TranslatableComponent {
pub base: BaseComponent,
pub key: String,

View file

@ -11,14 +11,14 @@ version = "0.4.0"
[dependencies]
anyhow = "1.0.59"
async-trait = "0.1.58"
azalea-auth = {path = "../azalea-auth", version = "0.4.0" }
azalea-block = {path = "../azalea-block", version = "0.4.0" }
azalea-chat = {path = "../azalea-chat", version = "0.4.0" }
azalea-core = {path = "../azalea-core", version = "0.4.0" }
azalea-crypto = {path = "../azalea-crypto", version = "0.4.0" }
azalea-physics = {path = "../azalea-physics", version = "0.4.0" }
azalea-protocol = {path = "../azalea-protocol", version = "0.4.0" }
azalea-world = {path = "../azalea-world", version = "0.4.0" }
azalea-auth = {path = "../azalea-auth", version = "0.4.0"}
azalea-block = {path = "../azalea-block", version = "0.4.0"}
azalea-chat = {path = "../azalea-chat", version = "0.4.0"}
azalea-core = {path = "../azalea-core", version = "0.4.0"}
azalea-crypto = {path = "../azalea-crypto", version = "0.4.0"}
azalea-physics = {path = "../azalea-physics", version = "0.4.0"}
azalea-protocol = {path = "../azalea-protocol", version = "0.4.0"}
azalea-world = {path = "../azalea-world", version = "0.4.0"}
log = "0.4.17"
nohash-hasher = "0.2.0"
once_cell = "1.16.0"

View file

@ -12,7 +12,7 @@ use azalea_protocol::packets::game::{
use std::time::{SystemTime, UNIX_EPOCH};
/// A chat packet, either a system message or a chat message.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub enum ChatPacket {
System(ClientboundSystemChatPacket),
Player(Box<ClientboundPlayerChatPacket>),
@ -126,28 +126,9 @@ impl Client {
/// Send a message in chat.
///
/// # Examples
///
/// ```rust,no_run
/// # use azalea::prelude::*;
/// # use parking_lot::Mutex;
/// # use std::sync::Arc;
/// # #[tokio::main]
/// # async fn main() {
/// # let account = Account::offline("bot");
/// # azalea::start(azalea::Options {
/// # account,
/// # address: "localhost",
/// # state: State::default(),
/// # plugins: plugins![],
/// # handle,
/// # })
/// # .await
/// # .unwrap();
/// # }
/// # #[derive(Default, Clone)]
/// # pub struct State {}
/// # async fn handle(bot: Client, event: Event, state: State) -> anyhow::Result<()> {
/// # use azalea_client::{Client, Event};
/// # async fn handle(bot: Client, event: Event) -> anyhow::Result<()> {
/// bot.chat("Hello, world!").await.unwrap();
/// # Ok(())
/// # }

View file

@ -1,5 +1,5 @@
pub use crate::chat::ChatPacket;
use crate::{movement::WalkDirection, plugins::Plugins, Account, PlayerInfo};
use crate::{movement::WalkDirection, plugins::PluginStates, Account, PlayerInfo};
use azalea_auth::game_profile::GameProfile;
use azalea_chat::Component;
use azalea_core::{ChunkPos, GameType, ResourceLocation, Vec3};
@ -15,7 +15,10 @@ use azalea_protocol::{
serverbound_move_player_pos_rot_packet::ServerboundMovePlayerPosRotPacket,
ClientboundGamePacket, ServerboundGamePacket,
},
handshake::client_intention_packet::ClientIntentionPacket,
handshake::{
client_intention_packet::ClientIntentionPacket, ClientboundHandshakePacket,
ServerboundHandshakePacket,
},
login::{
serverbound_custom_query_packet::ServerboundCustomQueryPacket,
serverbound_hello_packet::ServerboundHelloPacket,
@ -29,9 +32,9 @@ use azalea_protocol::{
};
use azalea_world::{
entity::{metadata, Entity, EntityData, EntityMetadata},
World,
WeakWorld, WeakWorldContainer, World,
};
use log::{debug, error, info, warn};
use log::{debug, error, info, trace, warn};
use parking_lot::{Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard};
use std::{
collections::HashMap,
@ -41,7 +44,7 @@ use std::{
};
use thiserror::Error;
use tokio::{
sync::mpsc::{self, UnboundedReceiver, UnboundedSender},
sync::mpsc::{self, Receiver, Sender},
task::JoinHandle,
time::{self},
};
@ -57,7 +60,7 @@ pub enum Event {
/// it's actually spawned. This can be useful for setting the client
/// information with `Client::set_client_information`, so the packet
/// doesn't have to be sent twice.
Initialize,
Init,
Login,
Chat(ChatPacket),
/// Happens 20 times per second, but only when the world is loaded.
@ -102,14 +105,20 @@ pub struct Client {
pub read_conn: Arc<tokio::sync::Mutex<ReadConnection<ClientboundGamePacket>>>,
pub write_conn: Arc<tokio::sync::Mutex<WriteConnection<ServerboundGamePacket>>>,
pub entity_id: Arc<RwLock<u32>>,
/// The world that this client has access to. This supports shared worlds.
pub world: Arc<RwLock<World>>,
/// A container of world names to worlds. If we're not using a shared world
/// (i.e. not a swarm), then this will only contain data about the world
/// we're currently in.
world_container: Arc<RwLock<WeakWorldContainer>>,
pub world_name: Arc<RwLock<Option<ResourceLocation>>>,
pub physics_state: Arc<Mutex<PhysicsState>>,
pub client_information: Arc<RwLock<ClientInformation>>,
pub dead: Arc<Mutex<bool>>,
/// Plugins are a way for other crates to add custom functionality to the
/// client and keep state. If you're not making a plugin and you're using
/// the `azalea` crate. you can ignore this field.
pub plugins: Arc<Plugins>,
pub plugins: Arc<PluginStates>,
/// A map of player uuids to their information in the tab list
pub players: Arc<RwLock<HashMap<Uuid, PlayerInfo>>>,
tasks: Arc<Mutex<Vec<JoinHandle<()>>>>,
@ -152,13 +161,50 @@ pub enum JoinError {
pub enum HandleError {
#[error("{0}")]
Poison(String),
#[error("{0}")]
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
Other(#[from] anyhow::Error),
#[error("{0}")]
Send(#[from] mpsc::error::SendError<Event>),
}
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,
conn: Connection<ClientboundGamePacket, ServerboundGamePacket>,
world_container: Option<Arc<RwLock<WeakWorldContainer>>>,
) -> Self {
let (read_conn, write_conn) = conn.into_split();
let (read_conn, write_conn) = (
Arc::new(tokio::sync::Mutex::new(read_conn)),
Arc::new(tokio::sync::Mutex::new(write_conn)),
);
Self {
profile,
read_conn,
write_conn,
// default our id to 0, it'll be set later
entity_id: Arc::new(RwLock::new(0)),
world: Arc::new(RwLock::new(World::default())),
world_container: world_container
.unwrap_or_else(|| Arc::new(RwLock::new(WeakWorldContainer::new()))),
world_name: Arc::new(RwLock::new(None)),
physics_state: Arc::new(Mutex::new(PhysicsState::default())),
client_information: Arc::new(RwLock::new(ClientInformation::default())),
dead: Arc::new(Mutex::new(false)),
// The plugins can be modified by the user by replacing the plugins
// field right after this. No Mutex so the user doesn't need to .lock().
plugins: Arc::new(PluginStates::default()),
players: Arc::new(RwLock::new(HashMap::new())),
tasks: Arc::new(Mutex::new(Vec::new())),
}
}
/// Connect to a Minecraft server.
///
/// To change the render distance and other settings, use
@ -168,26 +214,56 @@ impl Client {
/// # Examples
///
/// ```rust,no_run
/// use azalea_client::Client;
/// use azalea_client::{Client, Account};
///
/// #[tokio::main]
/// async fn main() -> Box<dyn std::error::Error> {
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let account = Account::offline("bot");
/// let (client, rx) = Client::join(&account, "localhost").await?;
/// client.chat("Hello, world!").await?;
/// client.shutdown().await?;
/// client.disconnect().await?;
/// Ok(())
/// }
/// ```
pub async fn join(
account: &Account,
address: impl TryInto<ServerAddress>,
) -> Result<(Self, UnboundedReceiver<Event>), JoinError> {
) -> Result<(Self, Receiver<Event>), JoinError> {
let address: ServerAddress = address.try_into().map_err(|_| JoinError::InvalidAddress)?;
let resolved_address = resolver::resolve_address(&address).await?;
let mut conn = Connection::new(&resolved_address).await?;
let conn = Connection::new(&resolved_address).await?;
let (conn, game_profile) = Self::handshake(conn, account, &address).await?;
// The buffer has to be 1 to avoid a bug where if it lags events are
// received a bit later instead of the instant they were fired.
// That bug especially causes issues with the pathfinder.
let (tx, rx) = mpsc::channel(1);
// we got the GameConnection, so the server is now connected :)
let client = Client::new(game_profile, conn, None);
tx.send(Event::Init).await.expect("Failed to send event");
// just start up the game loop and we're ready!
client.start_tasks(tx);
Ok((client, rx))
}
/// Do a handshake with the server and get to the game state from the initial handshake state.
pub async fn handshake(
mut conn: Connection<ClientboundHandshakePacket, ServerboundHandshakePacket>,
account: &Account,
address: &ServerAddress,
) -> Result<
(
Connection<ClientboundGamePacket, ServerboundGamePacket>,
GameProfile,
),
JoinError,
> {
// handshake
conn.write(
ClientIntentionPacket {
@ -267,48 +343,7 @@ impl Client {
}
};
let (read_conn, write_conn) = conn.into_split();
let read_conn = Arc::new(tokio::sync::Mutex::new(read_conn));
let write_conn = Arc::new(tokio::sync::Mutex::new(write_conn));
let (tx, rx) = mpsc::unbounded_channel();
// we got the GameConnection, so the server is now connected :)
let client = Client {
profile,
read_conn,
write_conn,
// default our id to 0, it'll be set later
entity_id: Arc::new(RwLock::new(0)),
world: Arc::new(RwLock::new(World::default())),
physics_state: Arc::new(Mutex::new(PhysicsState::default())),
client_information: Arc::new(RwLock::new(ClientInformation::default())),
dead: Arc::new(Mutex::new(false)),
// The plugins can be modified by the user by replacing the plugins
// field right after this. No Mutex so the user doesn't need to .lock().
plugins: Arc::new(Plugins::new()),
players: Arc::new(RwLock::new(HashMap::new())),
tasks: Arc::new(Mutex::new(Vec::new())),
};
tx.send(Event::Initialize).unwrap();
// just start up the game loop and we're ready!
// if you get an error right here that means you're doing something with locks wrong
// read the error to see where the issue is
// you might be able to just drop the lock or put it in its own scope to fix
{
let mut tasks = client.tasks.lock();
tasks.push(tokio::spawn(Self::protocol_loop(
client.clone(),
tx.clone(),
)));
tasks.push(tokio::spawn(Self::game_tick_loop(client.clone(), tx)));
}
Ok((client, rx))
Ok((conn, profile))
}
/// Write a packet directly to the server.
@ -317,8 +352,8 @@ impl Client {
Ok(())
}
/// Disconnect from the server, ending all tasks.
pub async fn shutdown(&self) -> Result<(), std::io::Error> {
/// Disconnect this client from the server, ending all tasks.
pub async fn disconnect(&self) -> Result<(), std::io::Error> {
if let Err(e) = self.write_conn.lock().await.shutdown().await {
warn!(
"Error shutting down connection, but it might be fine: {}",
@ -332,7 +367,22 @@ impl Client {
Ok(())
}
async fn protocol_loop(client: Client, tx: UnboundedSender<Event>) {
/// Start the protocol and game tick loop.
#[doc(hidden)]
pub fn start_tasks(&self, tx: Sender<Event>) {
// if you get an error right here that means you're doing something with locks wrong
// read the error to see where the issue is
// you might be able to just drop the lock or put it in its own scope to fix
let mut tasks = self.tasks.lock();
tasks.push(tokio::spawn(Client::protocol_loop(
self.clone(),
tx.clone(),
)));
tasks.push(tokio::spawn(Client::game_tick_loop(self.clone(), tx)));
}
async fn protocol_loop(client: Client, tx: Sender<Event>) {
loop {
let r = client.read_conn.lock().await.read().await;
match r {
@ -340,9 +390,7 @@ impl Client {
Ok(_) => {}
Err(e) => {
error!("Error handling packet: {}", e);
if IGNORE_ERRORS {
continue;
} else {
if !IGNORE_ERRORS {
panic!("Error handling packet: {e}");
}
}
@ -350,16 +398,15 @@ impl Client {
Err(e) => {
if let ReadPacketError::ConnectionClosed = e {
info!("Connection closed");
if let Err(e) = client.shutdown().await {
if let Err(e) = client.disconnect().await {
error!("Error shutting down connection: {:?}", e);
}
return;
break;
}
if IGNORE_ERRORS {
warn!("{}", e);
match e {
ReadPacketError::FrameSplitter { .. } => panic!("Error: {e:?}"),
_ => continue,
if let ReadPacketError::FrameSplitter { .. } = e {
panic!("Error: {e:?}");
}
} else {
panic!("{}", e);
@ -372,12 +419,12 @@ impl Client {
async fn handle(
packet: &ClientboundGamePacket,
client: &Client,
tx: &UnboundedSender<Event>,
tx: &Sender<Event>,
) -> Result<(), HandleError> {
tx.send(Event::Packet(Box::new(packet.clone()))).unwrap();
tx.send(Event::Packet(Box::new(packet.clone()))).await?;
match packet {
ClientboundGamePacket::Login(p) => {
debug!("Got login packet {:?}", p);
debug!("Got login packet");
{
// // write p into login.txt
@ -440,16 +487,27 @@ impl Client {
.as_int()
.expect("min_y tag is not an int");
// add this world to the world_container (or don't if it's already there)
let weak_world =
client
.world_container
.write()
.insert(p.dimension.clone(), height, min_y);
// set the loaded_world to an empty world
// (when we add chunks or entities those will be in the world_container)
let mut world_lock = client.world.write();
// the 16 here is our render distance
// i'll make this an actual setting later
*world_lock = World::new(16, height, min_y);
*world_lock = World::new(
client.client_information.read().view_distance.into(),
weak_world,
p.player_id,
);
let entity = EntityData::new(
client.profile.uuid,
Vec3::default(),
EntityMetadata::Player(metadata::Player::default()),
);
// make it so other entities don't update this entity in a shared world
world_lock.add_entity(p.player_id, entity);
*client.entity_id.write() = p.player_id;
@ -476,7 +534,7 @@ impl Client {
)
.await?;
tx.send(Event::Login).unwrap();
tx.send(Event::Login).await?;
}
ClientboundGamePacket::SetChunkCacheRadius(p) => {
debug!("Got set chunk cache radius packet {:?}", p);
@ -501,7 +559,7 @@ impl Client {
}
ClientboundGamePacket::Disconnect(p) => {
debug!("Got disconnect packet {:?}", p);
client.shutdown().await?;
client.disconnect().await?;
}
ClientboundGamePacket::UpdateRecipes(_p) => {
debug!("Got update recipes packet");
@ -521,9 +579,7 @@ impl Client {
let mut world_lock = client.world.write();
let mut player_entity = world_lock
.entity_mut(player_entity_id)
.expect("Player entity doesn't exist");
let mut player_entity = world_lock.entity_mut(player_entity_id).unwrap();
let delta_movement = player_entity.delta;
@ -604,94 +660,102 @@ impl Client {
use azalea_protocol::packets::game::clientbound_player_info_packet::Action;
debug!("Got player info packet {:?}", p);
let mut players_lock = client.players.write();
match &p.action {
Action::AddPlayer(players) => {
for player in players {
let player_info = PlayerInfo {
profile: GameProfile {
let mut events = Vec::new();
{
let mut players_lock = client.players.write();
match &p.action {
Action::AddPlayer(players) => {
for player in players {
let player_info = PlayerInfo {
profile: GameProfile {
uuid: player.uuid,
name: player.name.clone(),
properties: player.properties.clone(),
},
uuid: player.uuid,
name: player.name.clone(),
properties: player.properties.clone(),
},
uuid: player.uuid,
gamemode: player.gamemode,
latency: player.latency,
display_name: player.display_name.clone(),
};
players_lock.insert(player.uuid, player_info.clone());
tx.send(Event::UpdatePlayers(UpdatePlayersEvent::Add(player_info)))
.unwrap();
gamemode: player.gamemode,
latency: player.latency,
display_name: player.display_name.clone(),
};
players_lock.insert(player.uuid, player_info.clone());
events.push(Event::UpdatePlayers(UpdatePlayersEvent::Add(
player_info,
)));
}
}
}
Action::UpdateGameMode(players) => {
for player in players {
if let Some(p) = players_lock.get_mut(&player.uuid) {
p.gamemode = player.gamemode;
tx.send(Event::UpdatePlayers(UpdatePlayersEvent::GameMode {
uuid: player.uuid,
game_mode: player.gamemode,
}))
.unwrap();
} else {
warn!(
Action::UpdateGameMode(players) => {
for player in players {
if let Some(p) = players_lock.get_mut(&player.uuid) {
p.gamemode = player.gamemode;
events.push(Event::UpdatePlayers(
UpdatePlayersEvent::GameMode {
uuid: player.uuid,
game_mode: player.gamemode,
},
));
} else {
warn!(
"Ignoring PlayerInfo (UpdateGameMode) for unknown player {}",
player.uuid
);
}
}
}
}
Action::UpdateLatency(players) => {
for player in players {
if let Some(p) = players_lock.get_mut(&player.uuid) {
p.latency = player.latency;
tx.send(Event::UpdatePlayers(UpdatePlayersEvent::Latency {
uuid: player.uuid,
latency: player.latency,
}))
.unwrap();
} else {
warn!(
"Ignoring PlayerInfo (UpdateLatency) for unknown player {}",
player.uuid
);
Action::UpdateLatency(players) => {
for player in players {
if let Some(p) = players_lock.get_mut(&player.uuid) {
p.latency = player.latency;
events.push(Event::UpdatePlayers(
UpdatePlayersEvent::Latency {
uuid: player.uuid,
latency: player.latency,
},
));
} else {
warn!(
"Ignoring PlayerInfo (UpdateLatency) for unknown player {}",
player.uuid
);
}
}
}
}
Action::UpdateDisplayName(players) => {
for player in players {
if let Some(p) = players_lock.get_mut(&player.uuid) {
p.display_name = player.display_name.clone();
tx.send(Event::UpdatePlayers(UpdatePlayersEvent::DisplayName {
uuid: player.uuid,
display_name: player.display_name.clone(),
}))
.unwrap();
} else {
warn!(
Action::UpdateDisplayName(players) => {
for player in players {
if let Some(p) = players_lock.get_mut(&player.uuid) {
p.display_name = player.display_name.clone();
events.push(Event::UpdatePlayers(
UpdatePlayersEvent::DisplayName {
uuid: player.uuid,
display_name: player.display_name.clone(),
},
));
} else {
warn!(
"Ignoring PlayerInfo (UpdateDisplayName) for unknown player {}",
player.uuid
);
}
}
}
}
Action::RemovePlayer(players) => {
for player in players {
if players_lock.remove(&player.uuid).is_some() {
tx.send(Event::UpdatePlayers(UpdatePlayersEvent::Remove {
uuid: player.uuid,
}))
.unwrap();
} else {
warn!(
"Ignoring PlayerInfo (RemovePlayer) for unknown player {}",
player.uuid
);
Action::RemovePlayer(players) => {
for player in players {
if players_lock.remove(&player.uuid).is_some() {
events.push(Event::UpdatePlayers(UpdatePlayersEvent::Remove {
uuid: player.uuid,
}));
} else {
warn!(
"Ignoring PlayerInfo (RemovePlayer) for unknown player {}",
player.uuid
);
}
}
}
}
}
// TODO
for event in events {
tx.send(event).await?;
}
}
ClientboundGamePacket::SetChunkCacheCenter(p) => {
debug!("Got chunk cache center packet {:?}", p);
@ -701,8 +765,29 @@ impl Client {
.update_view_center(&ChunkPos::new(p.x, p.z));
}
ClientboundGamePacket::LevelChunkWithLight(p) => {
debug!("Got chunk with light packet {} {}", p.x, p.z);
// debug!("Got chunk with light packet {} {}", p.x, p.z);
let pos = ChunkPos::new(p.x, p.z);
// OPTIMIZATION: if we already know about the chunk from the
// shared world (and not ourselves), then we don't need to
// parse it again. This is only used when we have a shared
// world, since we check that the chunk isn't currently owned
// by this client.
let shared_has_chunk = client.world.read().get_chunk(&pos).is_some();
let this_client_has_chunk = client
.world
.read()
.chunk_storage
.limited_get(&pos)
.is_some();
if shared_has_chunk && !this_client_has_chunk {
trace!(
"Skipping parsing chunk {:?} because we already know about it",
pos
);
return Ok(());
}
// let chunk = Chunk::read_with_world_height(&mut p.chunk_data);
// debug("chunk {:?}")
if let Err(e) = client
@ -727,7 +812,7 @@ impl Client {
if let Some(mut entity) = world.entity_mut(p.id) {
entity.apply_metadata(&p.packed_items.0);
} else {
warn!("Server sent an entity data packet for an entity id ({}) that we don't know about", p.id);
// warn!("Server sent an entity data packet for an entity id ({}) that we don't know about", p.id);
}
}
ClientboundGamePacket::UpdateAttributes(_p) => {
@ -759,10 +844,11 @@ impl Client {
ClientboundGamePacket::SetHealth(p) => {
debug!("Got set health packet {:?}", p);
if p.health == 0.0 {
let mut dead_lock = client.dead.lock();
if !*dead_lock {
*dead_lock = true;
tx.send(Event::Death(None)).unwrap();
// we can't define a variable here with client.dead.lock()
// because of https://github.com/rust-lang/rust/issues/57478
if !*client.dead.lock() {
*client.dead.lock() = true;
tx.send(Event::Death(None)).await?;
}
}
}
@ -771,17 +857,14 @@ impl Client {
}
ClientboundGamePacket::TeleportEntity(p) => {
let mut world_lock = client.world.write();
world_lock
.set_entity_pos(
p.id,
Vec3 {
x: p.x,
y: p.y,
z: p.z,
},
)
.map_err(|e| HandleError::Other(e.into()))?;
let _ = world_lock.set_entity_pos(
p.id,
Vec3 {
x: p.x,
y: p.y,
z: p.z,
},
);
}
ClientboundGamePacket::UpdateAdvancements(p) => {
debug!("Got update advancements packet {:?}", p);
@ -792,16 +875,12 @@ impl Client {
ClientboundGamePacket::MoveEntityPos(p) => {
let mut world_lock = client.world.write();
world_lock
.move_entity_with_delta(p.entity_id, &p.delta)
.map_err(|e| HandleError::Other(e.into()))?;
let _ = world_lock.move_entity_with_delta(p.entity_id, &p.delta);
}
ClientboundGamePacket::MoveEntityPosRot(p) => {
let mut world_lock = client.world.write();
world_lock
.move_entity_with_delta(p.entity_id, &p.delta)
.map_err(|e| HandleError::Other(e.into()))?;
let _ = world_lock.move_entity_with_delta(p.entity_id, &p.delta);
}
ClientboundGamePacket::MoveEntityRot(_p) => {
// debug!("Got move entity rot packet {:?}", p);
@ -816,16 +895,16 @@ impl Client {
debug!("Got remove entities packet {:?}", p);
}
ClientboundGamePacket::PlayerChat(p) => {
// debug!("Got player chat packet {:?}", p);
debug!("Got player chat packet {:?}", p);
tx.send(Event::Chat(ChatPacket::Player(Box::new(p.clone()))))
.unwrap();
.await?;
}
ClientboundGamePacket::SystemChat(p) => {
debug!("Got system chat packet {:?}", p);
tx.send(Event::Chat(ChatPacket::System(p.clone()))).unwrap();
tx.send(Event::Chat(ChatPacket::System(p.clone()))).await?;
}
ClientboundGamePacket::Sound(p) => {
debug!("Got sound packet {:?}", p);
ClientboundGamePacket::Sound(_p) => {
// debug!("Got sound packet {:?}", p);
}
ClientboundGamePacket::LevelEvent(p) => {
debug!("Got level event packet {:?}", p);
@ -892,10 +971,11 @@ impl Client {
ClientboundGamePacket::PlayerCombatKill(p) => {
debug!("Got player kill packet {:?}", p);
if *client.entity_id.read() == p.player_id {
let mut dead_lock = client.dead.lock();
if !*dead_lock {
*dead_lock = true;
tx.send(Event::Death(Some(Box::new(p.clone())))).unwrap();
// we can't define a variable here with client.dead.lock()
// because of https://github.com/rust-lang/rust/issues/57478
if !*client.dead.lock() {
*client.dead.lock() = true;
tx.send(Event::Death(Some(Box::new(p.clone())))).await?;
}
}
}
@ -938,7 +1018,7 @@ impl Client {
}
/// Runs game_tick every 50 milliseconds.
async fn game_tick_loop(mut client: Client, tx: UnboundedSender<Event>) {
async fn game_tick_loop(mut client: Client, tx: Sender<Event>) {
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);
@ -949,24 +1029,25 @@ impl Client {
}
/// Runs every 50 milliseconds.
async fn game_tick(client: &mut Client, tx: &UnboundedSender<Event>) {
async fn game_tick(client: &mut Client, tx: &Sender<Event>) {
// return if there's no chunk at the player's position
{
let world_lock = client.world.write();
let world_lock = client.world.read();
let player_entity_id = *client.entity_id.read();
let player_entity = world_lock.entity(player_entity_id);
let player_entity = if let Some(player_entity) = player_entity {
player_entity
} else {
let Some(player_entity) = player_entity else {
return;
};
let player_chunk_pos: ChunkPos = player_entity.pos().into();
if world_lock[&player_chunk_pos].is_none() {
if world_lock.get_chunk(&player_chunk_pos).is_none() {
return;
}
}
tx.send(Event::Tick).unwrap();
tx.send(Event::Tick)
.await
.expect("Sending tick event should never fail");
// TODO: if we're a passenger, send the required packets
@ -978,15 +1059,34 @@ impl Client {
// TODO: minecraft does ambient sounds here
}
/// Get a [`WeakWorld`] from our world container. If it's a normal client,
/// then it'll be the same as the world the client has loaded. If the
/// client using a shared world, then the shared world will be a superset
/// of the client's world.
///
/// # Panics
/// Panics if the client has not received the login packet yet. You can check this with [`Client::logged_in`].
pub fn world(&self) -> Arc<WeakWorld> {
let world_name = self.world_name.read();
let world_name = world_name
.as_ref()
.expect("Client has not received login packet yet");
if let Some(world) = self.world_container.read().get(world_name) {
world
} else {
unreachable!("The world name must be in the world container");
}
}
/// Returns the entity associated to the player.
pub fn entity_mut(&self) -> Entity<RwLockWriteGuard<World>> {
let entity_id = *self.entity_id.read();
let mut world = self.world.write();
let world = self.world.write();
let entity_data = world
.entity_storage
.get_mut_by_id(entity_id)
.get_by_id(entity_id)
.expect("Player entity should exist");
let entity_ptr = unsafe { entity_data.as_ptr() };
Entity::new(world, entity_id, entity_ptr)
@ -994,26 +1094,36 @@ impl Client {
/// Returns the entity associated to the player.
pub fn entity(&self) -> Entity<RwLockReadGuard<World>> {
let entity_id = *self.entity_id.read();
let world = self.world.read();
let entity_data = world
.entity_storage
.get_by_id(entity_id)
.expect("Player entity should be in the given world");
let entity_ptr = unsafe { entity_data.as_const_ptr() };
let entity_ptr = unsafe { entity_data.as_ptr() };
Entity::new(world, entity_id, entity_ptr)
}
/// Returns whether we have a received the login packet yet.
pub fn logged_in(&self) -> bool {
let world = self.world.read();
let entity_id = *self.entity_id.read();
world.entity(entity_id).is_some()
// the login packet tells us the world name
self.world_name.read().is_some()
}
/// Tell the server we changed our game options (i.e. render distance, main hand).
/// If this is not set before the login packet, the default will be sent.
///
/// ```rust,no_run
/// # use azalea_client::{Client, ClientInformation};
/// # async fn example(bot: Client) -> Result<(), Box<dyn std::error::Error>> {
/// bot.set_client_information(ClientInformation {
/// view_distance: 2,
/// ..Default::default()
/// })
/// .await?;
/// # Ok(())
/// # }
/// ```
pub async fn set_client_information(
&self,
client_information: ServerboundClientInformationPacket,

6
azalea-client/src/lib.rs Executable file → Normal file
View file

@ -7,6 +7,8 @@
#![allow(incomplete_features)]
#![feature(trait_upcasting)]
#![feature(error_generic_member_access)]
#![feature(provide_any)]
mod account;
mod chat;
@ -18,10 +20,10 @@ mod player;
mod plugins;
pub use account::Account;
pub use client::{ChatPacket, Client, ClientInformation, Event, JoinError};
pub use client::{ChatPacket, Client, ClientInformation, Event, JoinError, PhysicsState};
pub use movement::{SprintDirection, WalkDirection};
pub use player::PlayerInfo;
pub use plugins::{Plugin, Plugins};
pub use plugins::{Plugin, PluginState, PluginStates, Plugins};
#[cfg(test)]
mod tests {

39
azalea-client/src/movement.rs Executable file → Normal file
View file

@ -1,3 +1,5 @@
use std::backtrace::Backtrace;
use crate::Client;
use azalea_core::Vec3;
use azalea_physics::collision::{MovableEntity, MoverType};
@ -15,7 +17,7 @@ use thiserror::Error;
#[derive(Error, Debug)]
pub enum MovePlayerError {
#[error("Player is not in world")]
PlayerNotInWorld,
PlayerNotInWorld(Backtrace),
#[error("{0}")]
Io(#[from] std::io::Error),
}
@ -23,7 +25,9 @@ pub enum MovePlayerError {
impl From<MoveEntityError> for MovePlayerError {
fn from(err: MoveEntityError) -> Self {
match err {
MoveEntityError::EntityDoesNotExist => MovePlayerError::PlayerNotInWorld,
MoveEntityError::EntityDoesNotExist(backtrace) => {
MovePlayerError::PlayerNotInWorld(backtrace)
}
}
}
}
@ -152,7 +156,7 @@ impl Client {
}
// Set our current position to the provided Vec3, potentially clipping through blocks.
pub async fn set_pos(&mut self, new_pos: Vec3) -> Result<(), MovePlayerError> {
pub async fn set_position(&mut self, new_pos: Vec3) -> Result<(), MovePlayerError> {
let player_entity_id = *self.entity_id.read();
let mut world_lock = self.world.write();
@ -167,7 +171,7 @@ impl Client {
let mut entity = world_lock
.entity_mut(player_entity_id)
.ok_or(MovePlayerError::PlayerNotInWorld)?;
.ok_or(MovePlayerError::PlayerNotInWorld(Backtrace::capture()))?;
log::trace!(
"move entity bounding box: {} {:?}",
entity.id,
@ -258,6 +262,19 @@ impl Client {
/// Start walking in the given direction. To sprint, use
/// [`Client::sprint`]. To stop walking, call walk with
/// `WalkDirection::None`.
///
/// # Examples
///
/// Walk for 1 second
/// ```rust,no_run
/// # use azalea_client::{Client, WalkDirection};
/// # use std::time::Duration;
/// # async fn example(mut bot: Client) {
/// bot.walk(WalkDirection::Forward);
/// tokio::time::sleep(Duration::from_secs(1)).await;
/// bot.walk(WalkDirection::None);
/// # }
/// ```
pub fn walk(&mut self, direction: WalkDirection) {
{
let mut physics_state = self.physics_state.lock();
@ -269,6 +286,19 @@ impl Client {
/// Start sprinting in the given direction. To stop moving, call
/// [`Client::walk(WalkDirection::None)`]
///
/// # Examples
///
/// Sprint for 1 second
/// ```rust,no_run
/// # use azalea_client::{Client, WalkDirection, SprintDirection};
/// # use std::time::Duration;
/// # async fn example(mut bot: Client) {
/// bot.sprint(SprintDirection::Forward);
/// tokio::time::sleep(Duration::from_secs(1)).await;
/// bot.walk(WalkDirection::None);
/// # }
/// ```
pub fn sprint(&mut self, direction: SprintDirection) {
let mut physics_state = self.physics_state.lock();
physics_state.move_direction = WalkDirection::from(direction);
@ -321,6 +351,7 @@ impl Client {
/// Sets your rotation. `y_rot` is yaw (looking to the side), `x_rot` is
/// pitch (looking up and down). You can get these numbers from the vanilla
/// f3 screen.
/// `y_rot` goes from -180 to 180, and `x_rot` goes from -90 to 90.
pub fn set_rotation(&mut self, y_rot: f32, x_rot: f32) {
let mut player_entity = self.entity_mut();
player_entity.set_rotation(y_rot, x_rot);

View file

@ -1,7 +1,6 @@
use azalea_auth::game_profile::GameProfile;
use azalea_chat::Component;
use azalea_core::GameType;
use azalea_world::entity::EntityData;
use azalea_world::World;
use uuid::Uuid;
@ -21,19 +20,3 @@ pub struct PlayerInfo {
/// The player's display name in the tab list.
pub display_name: Option<Component>,
}
impl PlayerInfo {
/// Get a reference to the entity of the player in the world.
pub fn entity<'d>(&'d self, world: &'d World) -> Option<&EntityData> {
world.entity_by_uuid(&self.uuid)
}
/// Get a mutable reference to the entity of the player in the world.
pub fn entity_mut<'d>(&'d mut self, world: &'d mut World) -> Option<&'d mut EntityData> {
world.entity_mut_by_uuid(&self.uuid)
}
pub fn set_uuid(&mut self, uuid: Uuid) {
self.uuid = uuid;
}
}

View file

@ -10,31 +10,24 @@ use std::{
type U64Hasher = BuildHasherDefault<NoHashHasher<u64>>;
// kind of based on https://docs.rs/http/latest/src/http/extensions.rs.html
/// A map of plugin ids to Plugin trait objects. The client stores this so we
/// can keep the state for our plugins.
///
/// If you're using azalea, you should generate this from the `plugins!` macro.
#[derive(Clone, Default)]
pub struct Plugins {
map: Option<HashMap<TypeId, Box<dyn Plugin>, U64Hasher>>,
pub struct PluginStates {
map: Option<HashMap<TypeId, Box<dyn PluginState>, U64Hasher>>,
}
impl Plugins {
pub fn new() -> Self {
Self::default()
}
/// A map of PluginState TypeIds to AnyPlugin objects. This can then be built
/// into a [`PluginStates`] object to get a fresh new state based on this
/// plugin.
///
/// If you're using the azalea crate, you should generate this from the
/// `plugins!` macro.
#[derive(Clone, Default)]
pub struct Plugins {
map: Option<HashMap<TypeId, Box<dyn AnyPlugin>, U64Hasher>>,
}
pub fn add<T: Plugin>(&mut self, plugin: T) {
if self.map.is_none() {
self.map = Some(HashMap::with_hasher(BuildHasherDefault::default()));
}
self.map
.as_mut()
.unwrap()
.insert(TypeId::of::<T>(), Box::new(plugin));
}
pub fn get<T: Plugin>(&self) -> Option<&T> {
impl PluginStates {
pub fn get<T: PluginState>(&self) -> Option<&T> {
self.map
.as_ref()
.and_then(|map| map.get(&TypeId::of::<T>()))
@ -42,10 +35,40 @@ impl Plugins {
}
}
impl IntoIterator for Plugins {
type Item = Box<dyn Plugin>;
impl Plugins {
/// Create a new empty set of plugins.
pub fn new() -> Self {
Self::default()
}
/// Add a new plugin to this set.
pub fn add<T: Plugin + Clone>(&mut self, plugin: T) {
if self.map.is_none() {
self.map = Some(HashMap::with_hasher(BuildHasherDefault::default()));
}
self.map
.as_mut()
.unwrap()
.insert(TypeId::of::<T::State>(), Box::new(plugin));
}
/// Build our plugin states from this set of plugins. Note that if you're
/// using `azalea` you'll probably never need to use this as it's called
/// for you.
pub fn build(self) -> PluginStates {
let mut map = HashMap::with_hasher(BuildHasherDefault::default());
for (id, plugin) in self.map.unwrap().into_iter() {
map.insert(id, plugin.build());
}
PluginStates { map: Some(map) }
}
}
impl IntoIterator for PluginStates {
type Item = Box<dyn PluginState>;
type IntoIter = std::vec::IntoIter<Self::Item>;
/// Iterate over the plugin states.
fn into_iter(self) -> Self::IntoIter {
self.map
.map(|map| map.into_values().collect::<Vec<_>>())
@ -54,26 +77,67 @@ impl IntoIterator for Plugins {
}
}
/// Plugins can keep their own personal state, listen to events, and add new functions to Client.
/// A `PluginState` keeps the current state of a plugin for a client. All the
/// fields must be atomic. Unique `PluginState`s are built from [`Plugin`]s.
#[async_trait]
pub trait Plugin: Send + Sync + PluginClone + Any + 'static {
pub trait PluginState: Send + Sync + PluginStateClone + Any + 'static {
async fn handle(self: Box<Self>, event: Event, bot: Client);
}
/// An internal trait that allows Plugin to be cloned.
#[doc(hidden)]
pub trait PluginClone {
fn clone_box(&self) -> Box<dyn Plugin>;
/// Plugins can keep their own personal state, listen to [`Event`]s, and add
/// new functions to [`Client`].
pub trait Plugin: Send + Sync + Any + 'static {
type State: PluginState;
fn build(&self) -> Self::State;
}
impl<T> PluginClone for T
/// AnyPlugin is basically a Plugin but without the State associated type
/// it has to exist so we can do a hashmap with Box<dyn AnyPlugin>
#[doc(hidden)]
pub trait AnyPlugin: Send + Sync + Any + AnyPluginClone + 'static {
fn build(&self) -> Box<dyn PluginState>;
}
impl<S: PluginState, B: Plugin<State = S> + Clone> AnyPlugin for B {
fn build(&self) -> Box<dyn PluginState> {
Box::new(self.build())
}
}
/// An internal trait that allows PluginState to be cloned.
#[doc(hidden)]
pub trait PluginStateClone {
fn clone_box(&self) -> Box<dyn PluginState>;
}
impl<T> PluginStateClone for T
where
T: 'static + Plugin + Clone,
T: 'static + PluginState + Clone,
{
fn clone_box(&self) -> Box<dyn Plugin> {
fn clone_box(&self) -> Box<dyn PluginState> {
Box::new(self.clone())
}
}
impl Clone for Box<dyn Plugin> {
impl Clone for Box<dyn PluginState> {
fn clone(&self) -> Self {
self.clone_box()
}
}
/// An internal trait that allows AnyPlugin to be cloned.
#[doc(hidden)]
pub trait AnyPluginClone {
fn clone_box(&self) -> Box<dyn AnyPlugin>;
}
impl<T> AnyPluginClone for T
where
T: 'static + Plugin + Clone,
{
fn clone_box(&self) -> Box<dyn AnyPlugin> {
Box::new(self.clone())
}
}
impl Clone for Box<dyn AnyPlugin> {
fn clone(&self) -> Self {
self.clone_box()
}

View file

@ -1,7 +1,5 @@
use azalea_buf::McBuf;
use crate::floor_mod;
#[derive(Clone, Copy, Debug, McBuf, Default)]
pub enum Direction {
#[default]
@ -116,7 +114,7 @@ impl AxisCycle {
}
}
pub fn between(axis0: Axis, axis1: Axis) -> Self {
Self::from_ordinal(floor_mod(axis1 as i32 - axis0 as i32, 3))
Self::from_ordinal(i32::rem_euclid(axis1 as i32 - axis0 as i32, 3) as u32)
}
pub fn inverse(self) -> Self {
match self {
@ -128,8 +126,8 @@ impl AxisCycle {
pub fn cycle(self, axis: Axis) -> Axis {
match self {
Self::None => axis,
Self::Forward => Axis::from_ordinal(floor_mod(axis as i32 + 1, 3)),
Self::Backward => Axis::from_ordinal(floor_mod(axis as i32 - 1, 3)),
Self::Forward => Axis::from_ordinal(i32::rem_euclid(axis as i32 + 1, 3) as u32),
Self::Backward => Axis::from_ordinal(i32::rem_euclid(axis as i32 - 1, 3) as u32),
}
}
pub fn cycle_xyz(self, x: i32, y: i32, z: i32, axis: Axis) -> i32 {

View file

@ -38,16 +38,6 @@ pub use aabb::*;
mod block_hit_result;
pub use block_hit_result::*;
// java moment
// TODO: add tests and optimize/simplify this
pub fn floor_mod(x: i32, y: u32) -> u32 {
if x < 0 {
y - ((-x) as u32 % y)
} else {
x as u32 % y
}
}
// TODO: make this generic
pub fn binary_search(mut min: i32, max: i32, predicate: &dyn Fn(i32) -> bool) -> i32 {
let mut diff = max - min;

View file

@ -7,12 +7,12 @@ pub struct SaltSignaturePair {
pub signature: Vec<u8>,
}
#[derive(Clone, Debug, Default, McBuf)]
#[derive(Clone, Debug, Default, McBuf, PartialEq)]
pub struct MessageSignature {
pub bytes: Vec<u8>,
}
#[derive(Clone, Debug, McBuf)]
#[derive(Clone, Debug, McBuf, PartialEq)]
pub struct SignedMessageHeader {
pub previous_signature: Option<MessageSignature>,
pub sender: Uuid,

0
azalea-physics/Cargo.toml Executable file → Normal file
View file

0
azalea-physics/src/collision/mod.rs Executable file → Normal file
View file

View file

@ -57,7 +57,7 @@ impl<'a> BlockCollisions<'a> {
}
}
fn get_chunk(&self, block_x: i32, block_z: i32) -> Option<&Arc<Mutex<Chunk>>> {
fn get_chunk(&self, block_x: i32, block_z: i32) -> Option<Arc<Mutex<Chunk>>> {
let chunk_x = ChunkSectionPos::block_to_section_coord(block_x);
let chunk_z = ChunkSectionPos::block_to_section_coord(block_z);
let chunk_pos = ChunkPos::new(chunk_x, chunk_z);
@ -75,7 +75,7 @@ impl<'a> BlockCollisions<'a> {
// return var7;
// }
self.world[&chunk_pos].as_ref()
self.world.get_chunk(&chunk_pos)
}
}

10
azalea-physics/src/lib.rs Executable file → Normal file
View file

@ -231,7 +231,10 @@ fn jump_boost_power<D: DerefMut<Target = World>>(_entity: &Entity<D>) -> f64 {
mod tests {
use super::*;
use azalea_core::ChunkPos;
use azalea_world::{Chunk, World};
use azalea_world::{
entity::{metadata, EntityMetadata},
Chunk, World,
};
use uuid::Uuid;
#[test]
@ -247,6 +250,7 @@ mod tests {
y: 70.,
z: 0.,
},
EntityMetadata::Player(metadata::Player::default()),
),
);
let mut entity = world.entity_mut(0).unwrap();
@ -279,6 +283,7 @@ mod tests {
y: 70.,
z: 0.5,
},
EntityMetadata::Player(metadata::Player::default()),
),
);
let block_state = world.set_block_state(&BlockPos { x: 0, y: 69, z: 0 }, BlockState::Stone);
@ -311,6 +316,7 @@ mod tests {
y: 71.,
z: 0.5,
},
EntityMetadata::Player(metadata::Player::default()),
),
);
let block_state = world.set_block_state(
@ -344,6 +350,7 @@ mod tests {
y: 71.,
z: 0.5,
},
EntityMetadata::Player(metadata::Player::default()),
),
);
let block_state = world.set_block_state(
@ -377,6 +384,7 @@ mod tests {
y: 73.,
z: 0.5,
},
EntityMetadata::Player(metadata::Player::default()),
),
);
let block_state = world.set_block_state(

View file

@ -44,8 +44,22 @@ pub struct WriteConnection<W: ProtocolPacket> {
///
/// Join an offline-mode server and go through the handshake.
/// ```rust,no_run
/// use azalea_protocol::{
/// resolver,
/// connect::Connection,
/// packets::{
/// ConnectionProtocol, PROTOCOL_VERSION,
/// login::{
/// ClientboundLoginPacket,
/// serverbound_hello_packet::ServerboundHelloPacket,
/// serverbound_key_packet::{ServerboundKeyPacket, NonceOrSaltSignature}
/// },
/// handshake::client_intention_packet::ClientIntentionPacket
/// }
/// };
///
/// #[tokio::main]
/// async fn main() -> anyhow::Result<()> {
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let resolved_address = resolver::resolve_address(&"localhost".try_into().unwrap()).await?;
/// let mut conn = Connection::new(&resolved_address).await?;
///
@ -97,8 +111,8 @@ pub struct WriteConnection<W: ProtocolPacket> {
/// break (conn.game(), p.game_profile);
/// }
/// ClientboundLoginPacket::LoginDisconnect(p) => {
/// println!("login disconnect: {}", p.reason);
/// bail!("{}", p.reason);
/// eprintln!("login disconnect: {}", p.reason);
/// return Err("login disconnect".into());
/// }
/// ClientboundLoginPacket::CustomQuery(p) => {}
/// }
@ -258,24 +272,51 @@ impl Connection<ClientboundLoginPacket, ServerboundLoginPacket> {
/// # Examples
///
/// ```rust,no_run
/// let token = azalea_auth::auth(azalea_auth::AuthOpts {
/// ..Default::default()
/// })
/// .await;
/// let player_data = azalea_auth::get_profile(token).await;
/// use azalea_auth::AuthResult;
/// use azalea_protocol::connect::Connection;
/// use azalea_protocol::packets::login::{
/// ClientboundLoginPacket,
/// serverbound_key_packet::{ServerboundKeyPacket, NonceOrSaltSignature}
/// };
/// use uuid::Uuid;
/// # use azalea_protocol::ServerAddress;
///
/// let mut connection = azalea::Connection::new(&server_address).await?;
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
/// let AuthResult { access_token, profile } = azalea_auth::auth(
/// "example@example.com",
/// azalea_auth::AuthOpts::default()
/// ).await.expect("Couldn't authenticate");
/// #
/// # let address = ServerAddress::try_from("example@example.com").unwrap();
/// # let resolved_address = azalea_protocol::resolver::resolve_address(&address).await?;
///
/// let mut conn = Connection::new(&resolved_address).await?;
///
/// // transition to the login state, in a real program we would have done a handshake first
/// connection.login();
/// let mut conn = conn.login();
///
/// match connection.read().await? {
/// ClientboundLoginPacket::Hello(p) => {
/// // tell Mojang we're joining the server
/// connection.authenticate(&token, player_data.uuid, p).await?;
/// }
/// _ => {}
/// match conn.read().await? {
/// ClientboundLoginPacket::Hello(p) => {
/// // tell Mojang we're joining the server & enable encryption
/// let e = azalea_crypto::encrypt(&p.public_key, &p.nonce).unwrap();
/// conn.authenticate(
/// &access_token,
/// &Uuid::parse_str(&profile.id).expect("Invalid UUID"),
/// e.secret_key,
/// p
/// ).await?;
/// conn.write(
/// ServerboundKeyPacket {
/// nonce_or_salt_signature: NonceOrSaltSignature::Nonce(e.encrypted_nonce),
/// key_bytes: e.encrypted_public_key,
/// }.get()
/// ).await?;
/// conn.set_encryption_key(e.secret_key);
/// }
/// _ => {}
/// }
/// # Ok(())
/// # }
/// ```
pub async fn authenticate(
&self,

17
azalea-protocol/src/lib.rs Executable file → Normal file
View file

@ -13,7 +13,7 @@
#![feature(error_generic_member_access)]
#![feature(provide_any)]
use std::str::FromStr;
use std::{net::SocketAddr, str::FromStr};
#[cfg(feature = "connecting")]
pub mod connect;
@ -35,13 +35,12 @@ pub mod write;
/// assert_eq!(addr.host, "localhost");
/// assert_eq!(addr.port, 25565);
/// ```
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct ServerAddress {
pub host: String,
pub port: u16,
}
// impl try_from for ServerAddress
impl<'a> TryFrom<&'a str> for ServerAddress {
type Error = String;
@ -59,6 +58,18 @@ impl<'a> TryFrom<&'a str> for ServerAddress {
}
}
impl From<SocketAddr> for ServerAddress {
/// Convert an existing SocketAddr into a ServerAddress. This just converts
/// the ip to a string and passes along the port. The resolver will realize
/// it's already an IP address and not do any DNS requests.
fn from(addr: SocketAddr) -> Self {
ServerAddress {
host: addr.ip().to_string(),
port: addr.port(),
}
}
}
#[cfg(test)]
mod tests {
use std::io::Cursor;

View file

@ -8,7 +8,7 @@ use azalea_crypto::{MessageSignature, SignedMessageHeader};
use azalea_protocol_macros::ClientboundGamePacket;
use uuid::Uuid;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
#[derive(Clone, Debug, McBuf, ClientboundGamePacket, PartialEq)]
pub struct ClientboundPlayerChatPacket {
pub message: PlayerChatMessage,
pub chat_type: ChatTypeBound,
@ -25,14 +25,14 @@ pub enum ChatType {
EmoteCommand = 6,
}
#[derive(Clone, Debug, McBuf)]
#[derive(Clone, Debug, McBuf, PartialEq)]
pub struct ChatTypeBound {
pub chat_type: ChatType,
pub name: Component,
pub target_name: Option<Component>,
}
#[derive(Clone, Debug, McBuf)]
#[derive(Clone, Debug, McBuf, PartialEq)]
pub struct PlayerChatMessage {
pub signed_header: SignedMessageHeader,
pub header_signature: MessageSignature,
@ -41,7 +41,7 @@ pub struct PlayerChatMessage {
pub filter_mask: FilterMask,
}
#[derive(Clone, Debug, McBuf)]
#[derive(Clone, Debug, PartialEq, McBuf)]
pub struct SignedMessageBody {
pub content: ChatMessageContent,
pub timestamp: u64,
@ -117,7 +117,7 @@ impl ChatType {
}
}
#[derive(Clone, Debug, McBuf)]
#[derive(Clone, Debug, McBuf, PartialEq)]
pub struct LastSeenMessagesEntry {
pub profile_id: Uuid,
pub last_signature: MessageSignature,
@ -129,14 +129,14 @@ pub struct LastSeenMessagesUpdate {
pub last_received: Option<LastSeenMessagesEntry>,
}
#[derive(Clone, Debug, McBuf)]
#[derive(Clone, Debug, McBuf, PartialEq)]
pub struct ChatMessageContent {
pub plain: String,
/// Only sent if the decorated message is different than the plain.
pub decorated: Option<Component>,
}
#[derive(Clone, Debug, McBuf)]
#[derive(Clone, Debug, McBuf, PartialEq)]
pub enum FilterMask {
PassThrough,
FullyFiltered,

View file

@ -2,7 +2,7 @@ use azalea_buf::McBuf;
use azalea_chat::Component;
use azalea_protocol_macros::ClientboundGamePacket;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
#[derive(Clone, Debug, McBuf, ClientboundGamePacket, PartialEq)]
pub struct ClientboundSystemChatPacket {
pub content: Component,
pub overlay: bool,

0
azalea-world/Cargo.toml Executable file → Normal file
View file

View file

@ -4,36 +4,61 @@ use crate::World;
use azalea_block::BlockState;
use azalea_buf::BufReadError;
use azalea_buf::{McBufReadable, McBufWritable};
use azalea_core::floor_mod;
use azalea_core::{BlockPos, ChunkBlockPos, ChunkPos, ChunkSectionBlockPos};
use log::debug;
use log::trace;
use log::warn;
use parking_lot::Mutex;
use parking_lot::RwLock;
use std::collections::HashMap;
use std::fmt::Debug;
use std::io::Cursor;
use std::{
io::Write,
ops::{Index, IndexMut},
sync::Arc,
};
use std::sync::Weak;
use std::{io::Write, sync::Arc};
const SECTION_HEIGHT: u32 = 16;
pub struct ChunkStorage {
/// An efficient storage of chunks for a client that has a limited render
/// distance. This has support for using a shared [`WeakChunkStorage`]. If you
/// have an infinite render distance (like a server), you should use
/// [`ChunkStorage`] instead.
pub struct PartialChunkStorage {
/// Chunk storage that can be shared by clients.
shared: Arc<RwLock<WeakChunkStorage>>,
pub view_center: ChunkPos,
chunk_radius: u32,
view_range: u32,
pub height: u32,
pub min_y: i32,
// chunks is a list of size chunk_radius * chunk_radius
chunks: Vec<Option<Arc<Mutex<Chunk>>>>,
}
/// A storage for chunks where they're only stored weakly, so if they're not
/// actively being used somewhere else they'll be forgotten. This is used for
/// shared worlds.
pub struct WeakChunkStorage {
pub height: u32,
pub min_y: i32,
pub chunks: HashMap<ChunkPos, Weak<Mutex<Chunk>>>,
}
/// A storage of potentially infinite chunks in a world. Chunks are stored as
/// an `Arc<Mutex>` so they can be shared across threads.
pub struct ChunkStorage {
pub height: u32,
pub min_y: i32,
pub chunks: HashMap<ChunkPos, Arc<Mutex<Chunk>>>,
}
/// A single chunk in a world (16*?*16 blocks). This only contains the blocks and biomes. You
/// can derive the height of the chunk from the number of sections, but you
/// need a [`ChunkStorage`] to get the minimum Y coordinate.
#[derive(Debug)]
pub struct Chunk {
pub sections: Vec<Section>,
}
/// A section of a chunk, i.e. a 16*16*16 block area.
#[derive(Clone, Debug)]
pub struct Section {
pub block_count: u16,
@ -59,22 +84,28 @@ impl Default for Chunk {
}
}
impl ChunkStorage {
pub fn new(chunk_radius: u32, height: u32, min_y: i32) -> Self {
impl PartialChunkStorage {
pub fn new(chunk_radius: u32, shared: Arc<RwLock<WeakChunkStorage>>) -> Self {
let view_range = chunk_radius * 2 + 1;
ChunkStorage {
PartialChunkStorage {
shared,
view_center: ChunkPos::new(0, 0),
chunk_radius,
view_range,
height,
min_y,
chunks: vec![None; (view_range * view_range) as usize],
}
}
pub fn min_y(&self) -> i32 {
self.shared.read().min_y
}
pub fn height(&self) -> u32 {
self.shared.read().height
}
fn get_index(&self, chunk_pos: &ChunkPos) -> usize {
(floor_mod(chunk_pos.x, self.view_range) * self.view_range
+ floor_mod(chunk_pos.z, self.view_range)) as usize
(i32::rem_euclid(chunk_pos.x, self.view_range as i32) * (self.view_range as i32)
+ i32::rem_euclid(chunk_pos.z, self.view_range as i32)) as usize
}
pub fn in_range(&self, chunk_pos: &ChunkPos) -> bool {
@ -84,19 +115,19 @@ impl ChunkStorage {
pub fn get_block_state(&self, pos: &BlockPos) -> Option<BlockState> {
let chunk_pos = ChunkPos::from(pos);
let chunk = self[&chunk_pos].as_ref()?;
let chunk = self.get(&chunk_pos)?;
let chunk = chunk.lock();
chunk.get(&ChunkBlockPos::from(pos), self.min_y)
chunk.get(&ChunkBlockPos::from(pos), self.min_y())
}
pub fn set_block_state(&self, pos: &BlockPos, state: BlockState) -> Option<BlockState> {
if pos.y < self.min_y || pos.y >= (self.min_y + self.height as i32) {
if pos.y < self.min_y() || pos.y >= (self.min_y() + self.height() as i32) {
return None;
}
let chunk_pos = ChunkPos::from(pos);
let chunk = self[&chunk_pos].as_ref()?;
let chunk = self.get(&chunk_pos)?;
let mut chunk = chunk.lock();
Some(chunk.get_and_set(&ChunkBlockPos::from(pos), state, self.min_y))
Some(chunk.get_and_set(&ChunkBlockPos::from(pos), state, self.min_y()))
}
pub fn replace_with_packet_data(
@ -116,27 +147,77 @@ impl ChunkStorage {
let chunk = Arc::new(Mutex::new(Chunk::read_with_dimension_height(
data,
self.height,
self.height(),
)?));
trace!("Loaded chunk {:?}", pos);
self[pos] = Some(chunk);
self.set(pos, Some(chunk));
Ok(())
}
}
impl Index<&ChunkPos> for ChunkStorage {
type Output = Option<Arc<Mutex<Chunk>>>;
/// Get a [`Chunk`] within render distance, or `None` if it's not loaded.
/// Use [`PartialChunkStorage::get`] to get a chunk from the shared storage.
pub fn limited_get(&self, pos: &ChunkPos) -> Option<&Arc<Mutex<Chunk>>> {
if !self.in_range(pos) {
warn!(
"Chunk at {:?} is not in the render distance (center: {:?}, {} chunks)",
pos, self.view_center, self.chunk_radius,
);
return None;
}
fn index(&self, pos: &ChunkPos) -> &Self::Output {
&self.chunks[self.get_index(pos)]
let index = self.get_index(pos);
self.chunks[index].as_ref()
}
/// Get a mutable reference to a [`Chunk`] within render distance, or
/// `None` if it's not loaded. Use [`PartialChunkStorage::get`] to get
/// a chunk from the shared storage.
pub fn limited_get_mut(&mut self, pos: &ChunkPos) -> Option<&mut Option<Arc<Mutex<Chunk>>>> {
if !self.in_range(pos) {
return None;
}
let index = self.get_index(pos);
Some(&mut self.chunks[index])
}
/// Get a chunk,
pub fn get(&self, pos: &ChunkPos) -> Option<Arc<Mutex<Chunk>>> {
self.shared
.read()
.chunks
.get(pos)
.and_then(|chunk| chunk.upgrade())
}
/// Set a chunk in the shared storage and reference it from the limited
/// storage.
///
/// # Panics
/// If the chunk is not in the render distance.
pub fn set(&mut self, pos: &ChunkPos, chunk: Option<Arc<Mutex<Chunk>>>) {
if let Some(chunk) = &chunk {
self.shared
.write()
.chunks
.insert(*pos, Arc::downgrade(chunk));
} else {
// don't remove it from the shared storage, since it'll be removed
// automatically if this was the last reference
}
if let Some(chunk_mut) = self.limited_get_mut(pos) {
*chunk_mut = chunk;
}
}
}
impl IndexMut<&ChunkPos> for ChunkStorage {
fn index_mut<'a>(&'a mut self, pos: &ChunkPos) -> &'a mut Self::Output {
let index = self.get_index(pos);
&mut self.chunks[index]
impl WeakChunkStorage {
pub fn new(height: u32, min_y: i32) -> Self {
WeakChunkStorage {
height,
min_y,
chunks: HashMap::new(),
}
}
}
@ -214,14 +295,14 @@ impl McBufWritable for Chunk {
}
}
impl Debug for ChunkStorage {
impl Debug for PartialChunkStorage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ChunkStorage")
.field("view_center", &self.view_center)
.field("chunk_radius", &self.chunk_radius)
.field("view_range", &self.view_range)
.field("height", &self.height)
.field("min_y", &self.min_y)
.field("height", &self.height())
.field("min_y", &self.min_y())
// .field("chunks", &self.chunks)
.field("chunks", &format_args!("{} items", self.chunks.len()))
.finish()
@ -292,9 +373,14 @@ impl Section {
}
}
impl Default for ChunkStorage {
impl Default for PartialChunkStorage {
fn default() -> Self {
Self::new(8, 384, -64)
Self::new(8, Arc::new(RwLock::new(WeakChunkStorage::default())))
}
}
impl Default for WeakChunkStorage {
fn default() -> Self {
Self::new(384, -64)
}
}
@ -317,8 +403,11 @@ mod tests {
#[test]
fn test_out_of_bounds_y() {
let mut chunk_storage = ChunkStorage::default();
chunk_storage[&ChunkPos { x: 0, z: 0 }] = Some(Arc::new(Mutex::new(Chunk::default())));
let mut chunk_storage = PartialChunkStorage::default();
chunk_storage.set(
&ChunkPos { x: 0, z: 0 },
Some(Arc::new(Mutex::new(Chunk::default()))),
);
assert!(chunk_storage
.get_block_state(&BlockPos { x: 0, y: 319, z: 0 })
.is_some());

View file

@ -0,0 +1,54 @@
use crate::WeakWorld;
use azalea_core::ResourceLocation;
use log::error;
use std::{
collections::HashMap,
sync::{Arc, Weak},
};
/// A container of [`WeakWorld`]s. Worlds are stored as a Weak pointer here, so
/// if no clients are using a world it will be forgotten.
#[derive(Default)]
pub struct WeakWorldContainer {
pub worlds: HashMap<ResourceLocation, Weak<WeakWorld>>,
}
impl WeakWorldContainer {
pub fn new() -> Self {
WeakWorldContainer {
worlds: HashMap::new(),
}
}
/// Get a world from the container.
pub fn get(&self, name: &ResourceLocation) -> Option<Arc<WeakWorld>> {
self.worlds.get(name).and_then(|world| world.upgrade())
}
/// Add an empty world to the container (or not if it already exists) and
/// returns a strong reference to the world.
#[must_use = "the world will be immediately forgotten if unused"]
pub fn insert(&mut self, name: ResourceLocation, height: u32, min_y: i32) -> Arc<WeakWorld> {
if let Some(existing) = self.worlds.get(&name).and_then(|world| world.upgrade()) {
if existing.height() != height {
error!(
"Shared dimension height mismatch: {} != {}",
existing.height(),
height,
);
}
if existing.min_y() != min_y {
error!(
"Shared world min_y mismatch: {} != {}",
existing.min_y(),
min_y,
);
}
existing
} else {
let world = Arc::new(WeakWorld::new(height, min_y));
self.worlds.insert(name, Arc::downgrade(&world));
world
}
}
}

View file

@ -1,4 +1,4 @@
//! https://minecraft.fandom.com/wiki/Attribute
//! <https://minecraft.fandom.com/wiki/Attribute>
use std::{
collections::HashMap,

View file

@ -270,20 +270,11 @@ impl EntityData {
&self.pos
}
/// Convert this &mut self into a (mutable) pointer.
///
/// # Safety
/// The entity MUST exist while this pointer exists.
pub unsafe fn as_ptr(&mut self) -> NonNull<EntityData> {
NonNull::new_unchecked(self as *mut EntityData)
}
/// Convert this &self into a (mutable) pointer.
///
/// # Safety
/// The entity MUST exist while this pointer exists. You also must not
/// modify the data inside the pointer.
pub unsafe fn as_const_ptr(&self) -> NonNull<EntityData> {
/// The entity MUST exist for at least as long as this pointer exists.
pub unsafe fn as_ptr(&self) -> NonNull<EntityData> {
// this is cursed
NonNull::new_unchecked(self as *const EntityData as *mut EntityData)
}

View file

@ -2,101 +2,229 @@ use crate::entity::EntityData;
use azalea_core::ChunkPos;
use log::warn;
use nohash_hasher::{IntMap, IntSet};
use std::collections::HashMap;
use parking_lot::RwLock;
use std::{
collections::HashMap,
sync::{Arc, Weak},
};
use uuid::Uuid;
#[derive(Debug)]
pub struct EntityStorage {
data_by_id: IntMap<u32, EntityData>,
id_by_chunk: HashMap<ChunkPos, IntSet<u32>>,
id_by_uuid: HashMap<Uuid, u32>,
// How entity updates are processed (to avoid issues with shared worlds)
// - each bot contains a map of { entity id: updates received }
// - the shared world also contains a canonical "true" updates received for each entity
// - when a client loads an entity, its "updates received" is set to the same as the global "updates received"
// - when the shared world sees an entity for the first time, the "updates received" is set to 1.
// - clients can force the shared "updates received" to 0 to make it so certain entities (i.e. other bots in our swarm) don't get confused and updated by other bots
// - when a client gets an update to an entity, we check if our "updates received" is the same as the shared world's "updates received":
// if it is, then process the update and increment the client's and shared world's "updates received"
// if not, then we simply increment our local "updates received" and do nothing else
/// Store a map of entities by ID. To get an iterator over all entities, use
/// `storage.shared.read().entities` [`WeakEntityStorage::entities`].
///
/// This is meant to be used with shared worlds.
#[derive(Debug, Default)]
pub struct PartialEntityStorage {
pub shared: Arc<RwLock<WeakEntityStorage>>,
/// The entity id of the player that owns this struct.
pub owner_entity_id: u32,
pub updates_received: IntMap<u32, u32>,
/// Strong references to the entities we have loaded.
data_by_id: IntMap<u32, Arc<EntityData>>,
}
impl EntityStorage {
pub fn new() -> Self {
/// Weakly store entities in a world. If the entities aren't being referenced
/// by anything else (like an [`PartialEntityStorage`]), they'll be forgotten.
#[derive(Debug, Default)]
pub struct WeakEntityStorage {
data_by_id: IntMap<u32, Weak<EntityData>>,
/// An index of all the entity ids we know are in a chunk
ids_by_chunk: HashMap<ChunkPos, IntSet<u32>>,
/// An index of entity ids by their UUIDs
id_by_uuid: HashMap<Uuid, u32>,
pub updates_received: IntMap<u32, u32>,
}
impl PartialEntityStorage {
pub fn new(shared: Arc<RwLock<WeakEntityStorage>>, owner_entity_id: u32) -> Self {
shared.write().updates_received.insert(owner_entity_id, 0);
Self {
shared,
owner_entity_id,
updates_received: IntMap::default(),
data_by_id: IntMap::default(),
id_by_chunk: HashMap::default(),
id_by_uuid: HashMap::default(),
}
}
/// Add an entity to the storage.
#[inline]
pub fn insert(&mut self, id: u32, entity: EntityData) {
self.id_by_chunk
// if the entity is already in the shared world, we don't need to do anything
if self.shared.read().data_by_id.contains_key(&id) {
return;
}
// add the entity to the "indexes"
let mut shared = self.shared.write();
shared
.ids_by_chunk
.entry(ChunkPos::from(entity.pos()))
.or_default()
.insert(id);
self.id_by_uuid.insert(entity.uuid, id);
shared.id_by_uuid.insert(entity.uuid, id);
// now store the actual entity data
let entity = Arc::new(entity);
shared.data_by_id.insert(id, Arc::downgrade(&entity));
self.data_by_id.insert(id, entity);
// set our updates_received to the shared updates_received, unless it's
// not there in which case set both to 1
if let Some(&shared_updates_received) = shared.updates_received.get(&id) {
// 0 means we're never tracking updates for this entity
if shared_updates_received != 0 || id == self.owner_entity_id {
self.updates_received.insert(id, 1);
}
} else {
shared.updates_received.insert(id, 1);
self.updates_received.insert(id, 1);
}
}
/// Remove an entity from the storage by its id.
/// Remove an entity from this storage by its id. It will only be removed
/// from the shared storage if there are no other references to it.
#[inline]
pub fn remove_by_id(&mut self, id: u32) {
if let Some(entity) = self.data_by_id.remove(&id) {
let entity_chunk = ChunkPos::from(entity.pos());
let entity_uuid = entity.uuid;
if self.id_by_chunk.remove(&entity_chunk).is_none() {
warn!("Tried to remove entity with id {id} from chunk {entity_chunk:?} but it was not found.");
}
if self.id_by_uuid.remove(&entity_uuid).is_none() {
warn!("Tried to remove entity with id {id} from uuid {entity_uuid:?} but it was not found.");
}
let chunk = ChunkPos::from(entity.pos());
let uuid = entity.uuid;
self.updates_received.remove(&id);
drop(entity);
// maybe remove it from the storage
self.shared.write().remove_entity_if_unused(id, uuid, chunk);
} else {
warn!("Tried to remove entity with id {id} but it was not found.")
}
}
/// Check if there is an entity that exists with the given id.
/// Whether the entity with the given id is being loaded by this storage.
/// If you want to check whether the entity is in the shared storage, use
/// [`WeakEntityStorage::contains_id`].
#[inline]
pub fn contains_id(&self, id: &u32) -> bool {
pub fn limited_contains_id(&self, id: &u32) -> bool {
self.data_by_id.contains_key(id)
}
/// Get a reference to an entity by its id.
/// Whether the entity with the given id is in the shared storage (i.e.
/// it's possible we don't see the entity but something else in the shared
/// storage does). To check whether the entity is being loaded by this
/// storage, use [`PartialEntityStorage::limited_contains_id`].
#[inline]
pub fn get_by_id(&self, id: u32) -> Option<&EntityData> {
pub fn contains_id(&self, id: &u32) -> bool {
self.shared.read().data_by_id.contains_key(id)
}
/// Get a reference to an entity by its id, if it's being loaded by this storage.
#[inline]
pub fn limited_get_by_id(&self, id: u32) -> Option<&Arc<EntityData>> {
self.data_by_id.get(&id)
}
/// Get a mutable reference to an entity by its id.
/// Get a mutable reference to an entity by its id, if it's being loaded by
/// this storage.
#[inline]
pub fn get_mut_by_id(&mut self, id: u32) -> Option<&mut EntityData> {
pub fn limited_get_mut_by_id(&mut self, id: u32) -> Option<&mut Arc<EntityData>> {
self.data_by_id.get_mut(&id)
}
/// Get a reference to an entity by its uuid.
/// Returns whether we're allowed to update this entity (to prevent two clients in
/// a shared world updating it twice), and acknowleges that we WILL update
/// it if it's true. Don't call this unless you actually got an entity
/// update that all other clients within render distance will get too.
pub fn maybe_update(&mut self, id: u32) -> bool {
let this_client_updates_received = self.updates_received.get(&id).copied();
let shared_updates_received = self.shared.read().updates_received.get(&id).copied();
let can_update = this_client_updates_received == shared_updates_received;
if can_update {
let new_updates_received = this_client_updates_received.unwrap_or(0) + 1;
self.updates_received.insert(id, new_updates_received);
self.shared
.write()
.updates_received
.insert(id, new_updates_received);
true
} else {
false
}
}
/// Get an entity in the shared storage by its id, if it exists.
#[inline]
pub fn get_by_uuid(&self, uuid: &Uuid) -> Option<&EntityData> {
self.id_by_uuid
pub fn get_by_id(&self, id: u32) -> Option<Arc<EntityData>> {
self.shared
.read()
.data_by_id
.get(&id)
.and_then(|e| e.upgrade())
}
/// Get a reference to an entity by its UUID, if it's being loaded by this
/// storage.
#[inline]
pub fn limited_get_by_uuid(&self, uuid: &Uuid) -> Option<&Arc<EntityData>> {
self.shared
.read()
.id_by_uuid
.get(uuid)
.and_then(|id| self.data_by_id.get(id))
}
/// Get a mutable reference to an entity by its uuid.
/// Get a mutable reference to an entity by its UUID, if it's being loaded
/// by this storage.
#[inline]
pub fn get_mut_by_uuid(&mut self, uuid: &Uuid) -> Option<&mut EntityData> {
self.id_by_uuid
pub fn limited_get_mut_by_uuid(&mut self, uuid: &Uuid) -> Option<&mut Arc<EntityData>> {
self.shared
.read()
.id_by_uuid
.get(uuid)
.and_then(|id| self.data_by_id.get_mut(id))
}
/// Clear all entities in a chunk.
/// Get an entity in the shared storage by its UUID, if it exists.
#[inline]
pub fn get_by_uuid(&self, uuid: &Uuid) -> Option<Arc<EntityData>> {
self.shared.read().id_by_uuid.get(uuid).and_then(|id| {
self.shared
.read()
.data_by_id
.get(id)
.and_then(|e| e.upgrade())
})
}
/// Clear all entities in a chunk. This will not clear them from the
/// shared storage, unless there are no other references to them.
pub fn clear_chunk(&mut self, chunk: &ChunkPos) {
if let Some(entities) = self.id_by_chunk.remove(chunk) {
for entity_id in entities {
if let Some(entity) = self.data_by_id.remove(&entity_id) {
self.id_by_uuid.remove(&entity.uuid);
} else {
warn!("While clearing chunk {chunk:?}, found an entity that isn't in by_id {entity_id}.");
if let Some(entities) = self.shared.read().ids_by_chunk.get(chunk) {
for id in entities.iter() {
if let Some(entity) = self.data_by_id.remove(id) {
let uuid = entity.uuid;
drop(entity);
// maybe remove it from the storage
self.shared
.write()
.remove_entity_if_unused(*id, uuid, *chunk);
}
}
// for entity_id in entities {
// self.remove_by_id(entity_id);
// }
}
}
/// Updates an entity from its old chunk.
/// Move an entity from its old chunk to a new chunk.
#[inline]
pub fn update_entity_chunk(
&mut self,
@ -104,36 +232,40 @@ impl EntityStorage {
old_chunk: &ChunkPos,
new_chunk: &ChunkPos,
) {
if let Some(entities) = self.id_by_chunk.get_mut(old_chunk) {
if let Some(entities) = self.shared.write().ids_by_chunk.get_mut(old_chunk) {
entities.remove(&entity_id);
}
self.id_by_chunk
self.shared
.write()
.ids_by_chunk
.entry(*new_chunk)
.or_default()
.insert(entity_id);
}
/// Get an iterator over all entities.
#[inline]
pub fn entities(&self) -> std::collections::hash_map::Values<'_, u32, EntityData> {
self.data_by_id.values()
pub fn find_one_entity<F>(&self, mut f: F) -> Option<Arc<EntityData>>
where
F: FnMut(&Arc<EntityData>) -> bool,
{
for entity in self.shared.read().entities() {
if let Some(entity) = entity.upgrade() {
if f(&entity) {
return Some(entity);
}
}
}
None
}
pub fn find_one_entity<F>(&self, mut f: F) -> Option<&EntityData>
pub fn find_one_entity_in_chunk<F>(&self, chunk: &ChunkPos, mut f: F) -> Option<Arc<EntityData>>
where
F: FnMut(&EntityData) -> bool,
{
self.entities().find(|&entity| f(entity))
}
pub fn find_one_entity_in_chunk<F>(&self, chunk: &ChunkPos, mut f: F) -> Option<&EntityData>
where
F: FnMut(&EntityData) -> bool,
{
if let Some(entities) = self.id_by_chunk.get(chunk) {
let shared = self.shared.read();
if let Some(entities) = shared.ids_by_chunk.get(chunk) {
for entity_id in entities {
if let Some(entity) = self.data_by_id.get(entity_id) {
if f(entity) {
if let Some(entity) = shared.data_by_id.get(entity_id).and_then(|e| e.upgrade()) {
if f(&entity) {
return Some(entity);
}
}
@ -143,9 +275,81 @@ impl EntityStorage {
}
}
impl Default for EntityStorage {
fn default() -> Self {
Self::new()
impl WeakEntityStorage {
pub fn new() -> Self {
Self {
data_by_id: IntMap::default(),
ids_by_chunk: HashMap::default(),
id_by_uuid: HashMap::default(),
updates_received: IntMap::default(),
}
}
/// Remove an entity from the storage if it has no strong references left.
/// Returns whether the entity was removed.
pub fn remove_entity_if_unused(&mut self, id: u32, uuid: Uuid, chunk: ChunkPos) -> bool {
if self.data_by_id.get(&id).and_then(|e| e.upgrade()).is_some() {
// if we could get the entity, that means there are still strong
// references to it
false
} else {
if self.ids_by_chunk.remove(&chunk).is_none() {
warn!("Tried to remove entity with id {id} from chunk {chunk:?} but it was not found.");
}
if self.id_by_uuid.remove(&uuid).is_none() {
warn!(
"Tried to remove entity with id {id} from uuid {uuid:?} but it was not found."
);
}
if self.updates_received.remove(&id).is_none() {
// if this happens it means we weren't tracking the updates_received for the client (bad)
warn!(
"Tried to remove entity with id {id} from updates_received but it was not found."
);
}
true
}
}
/// Remove a chunk from the storage if the entities in it have no strong
/// references left.
pub fn remove_chunk_if_unused(&mut self, chunk: &ChunkPos) {
if let Some(entities) = self.ids_by_chunk.get(chunk) {
if entities.is_empty() {
self.ids_by_chunk.remove(chunk);
}
}
}
/// Get an iterator over all entities in the shared storage. The iterator
/// is over `Weak<EntityData>`s, so you'll have to manually try to upgrade.
///
/// # Examples
///
/// ```rust
/// let mut storage = EntityStorage::new();
/// storage.insert(
/// 0,
/// Arc::new(EntityData::new(
/// uuid,
/// Vec3::default(),
/// EntityMetadata::Player(metadata::Player::default()),
/// )),
/// );
/// for entity in storage.shared.read().entities() {
/// if let Some(entity) = entity.upgrade() {
/// println!("Entity: {:?}", entity);
/// }
/// }
/// ```
pub fn entities(&self) -> std::collections::hash_map::Values<'_, u32, Weak<EntityData>> {
self.data_by_id.values()
}
/// Whether the entity with the given id is in the shared storage.
#[inline]
pub fn contains_id(&self, id: &u32) -> bool {
self.data_by_id.contains_key(id)
}
}
@ -158,7 +362,7 @@ mod tests {
#[test]
fn test_store_entity() {
let mut storage = EntityStorage::new();
let mut storage = PartialEntityStorage::default();
assert!(storage.get_by_id(0).is_none());
let uuid = Uuid::from_u128(100);

170
azalea-world/src/lib.rs Executable file → Normal file
View file

@ -1,174 +1,26 @@
#![feature(int_roundings)]
#![feature(error_generic_member_access)]
#![feature(provide_any)]
mod bit_storage;
mod chunk_storage;
mod container;
pub mod entity;
mod entity_storage;
mod palette;
mod world;
use std::backtrace::Backtrace;
use azalea_block::BlockState;
use azalea_buf::BufReadError;
use azalea_core::{BlockPos, ChunkPos, PositionDelta8, Vec3};
pub use bit_storage::BitStorage;
pub use chunk_storage::{Chunk, ChunkStorage};
use entity::{Entity, EntityData};
pub use entity_storage::EntityStorage;
use parking_lot::Mutex;
use std::{
io::Cursor,
ops::{Index, IndexMut},
sync::Arc,
};
pub use chunk_storage::{Chunk, ChunkStorage, PartialChunkStorage, WeakChunkStorage};
pub use container::*;
pub use entity_storage::{PartialEntityStorage, WeakEntityStorage};
use thiserror::Error;
use uuid::Uuid;
/// A world is a collection of chunks and entities. They're called "levels" in Minecraft's source code.
#[derive(Debug, Default)]
pub struct World {
pub chunk_storage: ChunkStorage,
pub entity_storage: EntityStorage,
}
pub use world::*;
#[derive(Error, Debug)]
pub enum MoveEntityError {
#[error("Entity doesn't exist")]
EntityDoesNotExist,
}
impl World {
pub fn new(chunk_radius: u32, height: u32, min_y: i32) -> Self {
World {
chunk_storage: ChunkStorage::new(chunk_radius, height, min_y),
entity_storage: EntityStorage::new(),
}
}
pub fn replace_with_packet_data(
&mut self,
pos: &ChunkPos,
data: &mut Cursor<&[u8]>,
) -> Result<(), BufReadError> {
self.chunk_storage.replace_with_packet_data(pos, data)
}
pub fn set_chunk(&mut self, pos: &ChunkPos, chunk: Option<Chunk>) -> Result<(), BufReadError> {
self[pos] = chunk.map(|c| Arc::new(Mutex::new(c)));
Ok(())
}
pub fn update_view_center(&mut self, pos: &ChunkPos) {
self.chunk_storage.view_center = *pos;
}
pub fn get_block_state(&self, pos: &BlockPos) -> Option<BlockState> {
self.chunk_storage.get_block_state(pos)
}
pub fn set_block_state(&mut self, pos: &BlockPos, state: BlockState) -> Option<BlockState> {
self.chunk_storage.set_block_state(pos, state)
}
pub fn set_entity_pos(&mut self, entity_id: u32, new_pos: Vec3) -> Result<(), MoveEntityError> {
let mut entity = self
.entity_mut(entity_id)
.ok_or(MoveEntityError::EntityDoesNotExist)?;
let old_chunk = ChunkPos::from(entity.pos());
let new_chunk = ChunkPos::from(&new_pos);
// this is fine because we update the chunk below
unsafe { entity.move_unchecked(new_pos) };
if old_chunk != new_chunk {
self.entity_storage
.update_entity_chunk(entity_id, &old_chunk, &new_chunk);
}
Ok(())
}
pub fn move_entity_with_delta(
&mut self,
entity_id: u32,
delta: &PositionDelta8,
) -> Result<(), MoveEntityError> {
let mut entity = self
.entity_mut(entity_id)
.ok_or(MoveEntityError::EntityDoesNotExist)?;
let new_pos = entity.pos().with_delta(delta);
let old_chunk = ChunkPos::from(entity.pos());
let new_chunk = ChunkPos::from(&new_pos);
// this is fine because we update the chunk below
unsafe { entity.move_unchecked(new_pos) };
if old_chunk != new_chunk {
self.entity_storage
.update_entity_chunk(entity_id, &old_chunk, &new_chunk);
}
Ok(())
}
pub fn add_entity(&mut self, id: u32, entity: EntityData) {
self.entity_storage.insert(id, entity);
}
pub fn height(&self) -> u32 {
self.chunk_storage.height
}
pub fn min_y(&self) -> i32 {
self.chunk_storage.min_y
}
pub fn entity_data_by_id(&self, id: u32) -> Option<&EntityData> {
self.entity_storage.get_by_id(id)
}
pub fn entity_data_mut_by_id(&mut self, id: u32) -> Option<&mut EntityData> {
self.entity_storage.get_mut_by_id(id)
}
pub fn entity(&self, id: u32) -> Option<Entity<&World>> {
let entity_data = self.entity_storage.get_by_id(id)?;
let entity_ptr = unsafe { entity_data.as_const_ptr() };
Some(Entity::new(self, id, entity_ptr))
}
pub fn entity_mut(&mut self, id: u32) -> Option<Entity<'_, &mut World>> {
let entity_data = self.entity_storage.get_mut_by_id(id)?;
let entity_ptr = unsafe { entity_data.as_ptr() };
Some(Entity::new(self, id, entity_ptr))
}
pub fn entity_by_uuid(&self, uuid: &Uuid) -> Option<&EntityData> {
self.entity_storage.get_by_uuid(uuid)
}
pub fn entity_mut_by_uuid(&mut self, uuid: &Uuid) -> Option<&mut EntityData> {
self.entity_storage.get_mut_by_uuid(uuid)
}
/// Get an iterator over all entities.
#[inline]
pub fn entities(&self) -> std::collections::hash_map::Values<'_, u32, EntityData> {
self.entity_storage.entities()
}
pub fn find_one_entity<F>(&self, mut f: F) -> Option<&EntityData>
where
F: FnMut(&EntityData) -> bool,
{
self.entity_storage.find_one_entity(|entity| f(entity))
}
}
impl Index<&ChunkPos> for World {
type Output = Option<Arc<Mutex<Chunk>>>;
fn index(&self, pos: &ChunkPos) -> &Self::Output {
&self.chunk_storage[pos]
}
}
impl IndexMut<&ChunkPos> for World {
fn index_mut<'a>(&'a mut self, pos: &ChunkPos) -> &'a mut Self::Output {
&mut self.chunk_storage[pos]
}
EntityDoesNotExist(Backtrace),
}

181
azalea-world/src/world.rs Normal file
View file

@ -0,0 +1,181 @@
use crate::{
entity::{Entity, EntityData},
Chunk, MoveEntityError, PartialChunkStorage, PartialEntityStorage, WeakChunkStorage,
WeakEntityStorage,
};
use azalea_block::BlockState;
use azalea_buf::BufReadError;
use azalea_core::{BlockPos, ChunkPos, PositionDelta8, Vec3};
use parking_lot::{Mutex, RwLock};
use std::{backtrace::Backtrace, fmt::Debug};
use std::{fmt::Formatter, io::Cursor, sync::Arc};
use uuid::Uuid;
/// A world is a collection of chunks and entities. They're called "levels" in Minecraft's source code.
#[derive(Default)]
pub struct World {
// we just need to keep a strong reference to `shared` so it doesn't get
// dropped, we don't need to do anything with it
_shared: Arc<WeakWorld>,
pub chunk_storage: PartialChunkStorage,
pub entity_storage: PartialEntityStorage,
}
/// A world where the chunks are stored as weak pointers. This is used for shared worlds.
#[derive(Default)]
pub struct WeakWorld {
pub chunk_storage: Arc<RwLock<WeakChunkStorage>>,
pub entity_storage: Arc<RwLock<WeakEntityStorage>>,
}
impl World {
pub fn new(chunk_radius: u32, shared: Arc<WeakWorld>, owner_entity_id: u32) -> Self {
World {
_shared: shared.clone(),
chunk_storage: PartialChunkStorage::new(chunk_radius, shared.chunk_storage.clone()),
entity_storage: PartialEntityStorage::new(
shared.entity_storage.clone(),
owner_entity_id,
),
}
}
pub fn replace_with_packet_data(
&mut self,
pos: &ChunkPos,
data: &mut Cursor<&[u8]>,
) -> Result<(), BufReadError> {
self.chunk_storage.replace_with_packet_data(pos, data)
}
pub fn get_chunk(&self, pos: &ChunkPos) -> Option<Arc<Mutex<Chunk>>> {
self.chunk_storage.get(pos)
}
pub fn set_chunk(&mut self, pos: &ChunkPos, chunk: Option<Chunk>) -> Result<(), BufReadError> {
self.chunk_storage
.set(pos, chunk.map(|c| Arc::new(Mutex::new(c))));
Ok(())
}
pub fn update_view_center(&mut self, pos: &ChunkPos) {
self.chunk_storage.view_center = *pos;
}
pub fn get_block_state(&self, pos: &BlockPos) -> Option<BlockState> {
self.chunk_storage.get_block_state(pos)
}
pub fn set_block_state(&mut self, pos: &BlockPos, state: BlockState) -> Option<BlockState> {
self.chunk_storage.set_block_state(pos, state)
}
pub fn set_entity_pos(&mut self, entity_id: u32, new_pos: Vec3) -> Result<(), MoveEntityError> {
let mut entity = self
.entity_mut(entity_id)
.ok_or_else(|| MoveEntityError::EntityDoesNotExist(Backtrace::capture()))?;
let old_chunk = ChunkPos::from(entity.pos());
let new_chunk = ChunkPos::from(&new_pos);
// this is fine because we update the chunk below
unsafe { entity.move_unchecked(new_pos) };
if old_chunk != new_chunk {
self.entity_storage
.update_entity_chunk(entity_id, &old_chunk, &new_chunk);
}
Ok(())
}
pub fn move_entity_with_delta(
&mut self,
entity_id: u32,
delta: &PositionDelta8,
) -> Result<(), MoveEntityError> {
let mut entity = self
.entity_mut(entity_id)
.ok_or_else(|| MoveEntityError::EntityDoesNotExist(Backtrace::capture()))?;
let new_pos = entity.pos().with_delta(delta);
let old_chunk = ChunkPos::from(entity.pos());
let new_chunk = ChunkPos::from(&new_pos);
// this is fine because we update the chunk below
unsafe { entity.move_unchecked(new_pos) };
if old_chunk != new_chunk {
self.entity_storage
.update_entity_chunk(entity_id, &old_chunk, &new_chunk);
}
Ok(())
}
pub fn add_entity(&mut self, id: u32, entity: EntityData) {
self.entity_storage.insert(id, entity);
}
pub fn height(&self) -> u32 {
self.chunk_storage.height()
}
pub fn min_y(&self) -> i32 {
self.chunk_storage.min_y()
}
pub fn entity_data_by_id(&self, id: u32) -> Option<Arc<EntityData>> {
self.entity_storage.get_by_id(id)
}
pub fn entity(&self, id: u32) -> Option<Entity<&World>> {
let entity_data = self.entity_storage.get_by_id(id)?;
let entity_ptr = unsafe { entity_data.as_ptr() };
Some(Entity::new(self, id, entity_ptr))
}
/// Returns a mutable reference to the entity with the given ID.
pub fn entity_mut(&mut self, id: u32) -> Option<Entity<'_, &mut World>> {
// no entity for you (we're processing this entity somewhere else)
if id != self.entity_storage.owner_entity_id && !self.entity_storage.maybe_update(id) {
return None;
}
let entity_data = self.entity_storage.get_by_id(id)?;
let entity_ptr = unsafe { entity_data.as_ptr() };
Some(Entity::new(self, id, entity_ptr))
}
pub fn entity_by_uuid(&self, uuid: &Uuid) -> Option<Arc<EntityData>> {
self.entity_storage.get_by_uuid(uuid)
}
pub fn find_one_entity<F>(&self, mut f: F) -> Option<Arc<EntityData>>
where
F: FnMut(&EntityData) -> bool,
{
self.entity_storage.find_one_entity(|entity| f(entity))
}
}
impl WeakWorld {
pub fn new(height: u32, min_y: i32) -> Self {
WeakWorld {
chunk_storage: Arc::new(RwLock::new(WeakChunkStorage::new(height, min_y))),
entity_storage: Arc::new(RwLock::new(WeakEntityStorage::new())),
}
}
pub fn height(&self) -> u32 {
self.chunk_storage.read().height
}
pub fn min_y(&self) -> i32 {
self.chunk_storage.read().min_y
}
}
impl Debug for World {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("World")
.field("chunk_storage", &self.chunk_storage)
.field("entity_storage", &self.entity_storage)
.finish()
}
}

12
azalea/Cargo.toml Executable file → Normal file
View file

@ -10,19 +10,23 @@ version = "0.4.0"
[dependencies]
anyhow = "^1.0.65"
async-trait = "^0.1.57"
azalea-block = { version = "0.4.0", path = "../azalea-block" }
async-trait = "0.1.58"
azalea-block = {version = "0.4.0", path = "../azalea-block"}
azalea-chat = { version = "0.4.0", path = "../azalea-chat" }
azalea-client = {version = "0.4.0", path = "../azalea-client"}
azalea-core = {version = "0.4.0", path = "../azalea-core"}
azalea-physics = { version = "0.4.0", path = "../azalea-physics" }
azalea-physics = {version = "0.4.0", path = "../azalea-physics"}
azalea-protocol = {version = "0.4.0", path = "../azalea-protocol"}
azalea-world = { version = "0.4.0", path = "../azalea-world" }
azalea-world = {version = "0.4.0", path = "../azalea-world"}
futures = "0.3.25"
log = "0.4.17"
nohash-hasher = "0.2.0"
num-traits = "0.2.15"
parking_lot = {version = "^0.12.1", features = ["deadlock_detection"]}
priority-queue = "1.3.0"
thiserror = "^1.0.37"
tokio = "^1.21.2"
uuid = "1.2.2"
[dev-dependencies]
anyhow = "^1.0.65"

View file

@ -1,3 +1,4 @@
Azalea is a framework for creating Minecraft bots.
Internally, it's just a wrapper over azalea-client, adding useful functions for making bots.

29
azalea/examples/mine_a_chunk.rs Executable file → Normal file
View file

@ -1,13 +1,16 @@
use azalea::{Account, Accounts, Client, Event, Swarm};
use azalea::{prelude::*, SwarmEvent};
use azalea::{Account, Client, Event, Swarm};
use parking_lot::Mutex;
use std::sync::Arc;
#[tokio::main]
async fn main() {
let accounts = Accounts::new();
let mut accounts = Vec::new();
let mut states = Vec::new();
for i in 0..10 {
accounts.add(Account::offline(&format!("bot{}", i)));
accounts.push(Account::offline(&format!("bot{}", i)));
states.push(Arc::new(Mutex::new(State::default())));
}
azalea::start_swarm(azalea::SwarmOptions {
@ -15,13 +18,15 @@ async fn main() {
address: "localhost",
swarm_state: State::default(),
state: State::default(),
states,
swarm_plugins: plugins![azalea_pathfinder::Plugin::default()],
swarm_plugins: plugins![],
plugins: plugins![],
handle: Box::new(handle),
swarm_handle: Box::new(swarm_handle),
handle,
swarm_handle,
join_delay: None,
})
.await
.unwrap();
@ -37,9 +42,13 @@ async fn handle(bot: Client, event: Event, state: State) -> anyhow::Result<()> {
Ok(())
}
async fn swarm_handle(swarm: Swarm, event: Event, state: SwarmState) -> anyhow::Result<()> {
match event {
Event::Login => {
async fn swarm_handle(
swarm: Swarm<State>,
event: SwarmEvent,
state: SwarmState,
) -> anyhow::Result<()> {
match &event {
SwarmEvent::Login => {
swarm.goto(azalea::BlockPos::new(0, 70, 0)).await;
// or bots.goto_goal(pathfinder::Goals::Goto(azalea::BlockPos(0, 70, 0))).await;

View file

@ -14,7 +14,7 @@ pub struct Plugin {
pub struct State {}
#[async_trait]
impl azalea::Plugin for Plugin {
impl azalea::PluginState for Plugin {
async fn handle(self: Box<Self>, event: Event, bot: Client) {
match event {
Event::UpdateHunger => {

View file

@ -15,7 +15,7 @@ async fn main() {
account,
address: "localhost",
state: State::default(),
plugins: plugins![autoeat::Plugin::default(), pathfinder::Plugin::default(),],
plugins: plugins![autoeat::Plugin, pathfinder::Plugin],
handle,
})
.await

View file

@ -15,7 +15,7 @@ async fn main() {
swarm_state: State::default(),
state: State::default(),
swarm_plugins: plugins![pathfinder::Plugin::default()],
swarm_plugins: plugins![pathfinder::Plugin],
plugins: plugins![],
handle: Box::new(handle),
@ -32,7 +32,7 @@ struct State {}
struct SwarmState {}
async fn handle(bot: Client, event: Event, state: State) {}
async fn swarm_handle(swarm: Swarm, event: Event, state: State) {
async fn swarm_handle<S>(swarm: Swarm<S>, event: Event, state: State) {
match event {
Event::Tick => {
// choose an arbitrary player within render distance to target

37
azalea/src/bot.rs Executable file → Normal file
View file

@ -4,9 +4,14 @@ use azalea_core::Vec3;
use parking_lot::Mutex;
use std::{f64::consts::PI, sync::Arc};
#[derive(Default, Clone)]
pub struct Plugin {
pub state: State,
#[derive(Clone, Default)]
pub struct Plugin;
impl crate::Plugin for Plugin {
type State = State;
fn build(&self) -> State {
State::default()
}
}
#[derive(Default, Clone)]
@ -14,6 +19,18 @@ pub struct State {
jumping_once: Arc<Mutex<bool>>,
}
#[async_trait]
impl crate::PluginState for State {
async fn handle(self: Box<Self>, event: Event, mut bot: Client) {
if let Event::Tick = event {
if *self.jumping_once.lock() && bot.jumping() {
*self.jumping_once.lock() = false;
bot.set_jumping(false);
}
}
}
}
pub trait BotTrait {
fn jump(&mut self);
fn look_at(&mut self, pos: &Vec3);
@ -23,7 +40,7 @@ impl BotTrait for azalea_client::Client {
/// Queue a jump for the next tick.
fn jump(&mut self) {
self.set_jumping(true);
let state = self.plugins.get::<Plugin>().unwrap().state.clone();
let state = self.plugins.get::<State>().unwrap().clone();
*state.jumping_once.lock() = true;
}
@ -34,18 +51,6 @@ impl BotTrait for azalea_client::Client {
}
}
#[async_trait]
impl crate::Plugin for Plugin {
async fn handle(self: Box<Self>, event: Event, mut bot: Client) {
if let Event::Tick = event {
if *self.state.jumping_once.lock() && bot.jumping() {
*self.state.jumping_once.lock() = false;
bot.set_jumping(false);
}
}
}
}
fn direction_looking_at(current: &Vec3, target: &Vec3) -> (f32, f32) {
// borrowed from mineflayer's Bot.lookAt because i didn't want to do math
let delta = target - current;

149
azalea/src/lib.rs Executable file → Normal file
View file

@ -75,152 +75,19 @@
//!
//! [`azalea_client`]: https://crates.io/crates/azalea-client
#![feature(trait_upcasting)]
#![feature(async_closure)]
#![allow(incomplete_features)]
mod bot;
pub mod pathfinder;
pub mod prelude;
mod start;
mod swarm;
pub use azalea_client::*;
pub use azalea_core::{BlockPos, Vec3};
use azalea_protocol::ServerAddress;
use std::{future::Future, sync::Arc};
use thiserror::Error;
pub use start::{start, Options};
pub use swarm::*;
pub type HandleFn<Fut, S> = fn(Client, Event, S) -> Fut;
/// The options that are passed to [`azalea::start`].
///
/// [`azalea::start`]: fn.start.html
pub struct Options<S, A, Fut>
where
A: TryInto<ServerAddress>,
Fut: Future<Output = Result<(), anyhow::Error>>,
{
/// The address of the server that we're connecting to. This can be a
/// `&str`, [`ServerAddress`], or anything that implements
/// `TryInto<ServerAddress>`.
///
/// [`ServerAddress`]: azalea_protocol::ServerAddress
pub address: A,
/// The account that's going to join the server.
pub account: Account,
/// The plugins that are going to be used. Plugins are external crates that
/// add extra functionality to Azalea. You should use the [`plugins`] macro
/// for this field.
///
/// ```rust,no_run
/// plugins![azalea_pathfinder::Plugin::default()]
/// ```
pub plugins: Plugins,
/// A struct that contains the data that you want your bot to remember
/// across events.
///
/// # Examples
///
/// ```rust
/// use parking_lot::Mutex;
/// use std::sync::Arc;
///
/// #[derive(Default, Clone)]
/// struct State {
/// farming: Arc<Mutex<bool>>,
/// }
/// ```
pub state: S,
/// The function that's called whenever we get an event.
///
/// # Examples
///
/// ```rust
/// use azalea::prelude::*;
///
/// async fn handle(bot: Client, event: Event, state: State) -> anyhow::Result<()> {
/// Ok(())
/// }
/// ```
pub handle: HandleFn<Fut, S>,
}
#[derive(Error, Debug)]
pub enum Error {
#[error("Invalid address")]
InvalidAddress,
#[error("Join error: {0}")]
Join(#[from] azalea_client::JoinError),
}
/// Join a server and start handling events. This function will run forever until
/// it gets disconnected from the server.
///
/// # Examples
///
/// ```rust,no_run
/// let error = azalea::start(azalea::Options {
/// account,
/// address: "localhost",
/// state: State::default(),
/// plugins: plugins![azalea_pathfinder::Plugin::default()],
/// handle,
/// }).await;
/// ```
pub async fn start<
S: Send + Sync + Clone + 'static,
A: Send + TryInto<ServerAddress>,
Fut: Future<Output = Result<(), anyhow::Error>> + Send + 'static,
>(
options: Options<S, A, Fut>,
) -> Result<(), Error> {
let address = match options.address.try_into() {
Ok(address) => address,
Err(_) => return Err(Error::InvalidAddress),
};
let (mut bot, mut rx) = Client::join(&options.account, address).await?;
let mut plugins = options.plugins;
plugins.add(bot::Plugin::default());
plugins.add(pathfinder::Plugin::default());
bot.plugins = Arc::new(plugins);
let state = options.state;
while let Some(event) = rx.recv().await {
let cloned_plugins = (*bot.plugins).clone();
for plugin in cloned_plugins.into_iter() {
tokio::spawn(plugin.handle(event.clone(), bot.clone()));
}
tokio::spawn(bot::Plugin::handle(
Box::new(bot.plugins.get::<bot::Plugin>().unwrap().clone()),
event.clone(),
bot.clone(),
));
tokio::spawn(pathfinder::Plugin::handle(
Box::new(bot.plugins.get::<pathfinder::Plugin>().unwrap().clone()),
event.clone(),
bot.clone(),
));
tokio::spawn((options.handle)(bot.clone(), event.clone(), state.clone()));
}
Ok(())
}
/// A helper macro that generates a [`Plugins`] struct from a list of objects
/// that implement [`Plugin`].
///
/// ```rust,no_run
/// plugins![azalea_pathfinder::Plugin::default()];
/// ```
#[macro_export]
macro_rules! plugins {
($($plugin:expr),*) => {
{
let mut plugins = azalea::Plugins::new();
$(
plugins.add($plugin);
)*
plugins
}
};
}

View file

@ -13,9 +13,14 @@ use parking_lot::Mutex;
use std::collections::VecDeque;
use std::sync::Arc;
#[derive(Default, Clone)]
pub struct Plugin {
pub state: State,
#[derive(Clone, Default)]
pub struct Plugin;
impl crate::Plugin for Plugin {
type State = State;
fn build(&self) -> State {
State::default()
}
}
#[derive(Default, Clone)]
@ -25,10 +30,10 @@ pub struct State {
}
#[async_trait]
impl crate::Plugin for Plugin {
impl crate::PluginState for State {
async fn handle(self: Box<Self>, event: Event, mut bot: Client) {
if let Event::Tick = event {
let mut path = self.state.path.lock();
let mut path = self.path.lock();
if !path.is_empty() {
tick_execute_path(&mut bot, &mut path);
@ -102,9 +107,8 @@ impl Trait for azalea_client::Client {
let state = self
.plugins
.get::<Plugin>()
.get::<State>()
.expect("Pathfinder plugin not installed!")
.state
.clone();
// convert the Option<Vec<Node>> to a VecDeque<Node>
*state.path.lock() = p.expect("no path").into_iter().collect();
@ -127,7 +131,7 @@ fn tick_execute_path(bot: &mut Client, path: &mut VecDeque<Node>) {
}
if target.is_reached(&bot.entity()) {
println!("ok target {target:?} reached");
// println!("ok target {target:?} reached");
path.pop_front();
if path.is_empty() {
bot.walk(WalkDirection::None);
@ -165,13 +169,13 @@ impl Node {
/// Returns whether the entity is at the node and should start going to the
/// next node.
pub fn is_reached(&self, entity: &EntityData) -> bool {
println!(
"entity.delta.y: {} {:?}=={:?}, self.vertical_vel={:?}",
entity.delta.y,
BlockPos::from(entity.pos()),
self.pos,
self.vertical_vel
);
// println!(
// "entity.delta.y: {} {:?}=={:?}, self.vertical_vel={:?}",
// entity.delta.y,
// BlockPos::from(entity.pos()),
// self.pos,
// self.vertical_vel
// );
BlockPos::from(entity.pos()) == self.pos
&& match self.vertical_vel {
VerticalVel::NoneMidair => entity.delta.y > -0.1 && entity.delta.y < 0.1,

2
azalea/src/prelude.rs Executable file → Normal file
View file

@ -2,5 +2,5 @@
pub use crate::bot::BotTrait;
pub use crate::pathfinder::Trait;
pub use crate::plugins;
pub use crate::{plugins, swarm_plugins, Plugin};
pub use azalea_client::{Account, Client, Event};

136
azalea/src/start.rs Normal file
View file

@ -0,0 +1,136 @@
use crate::{bot, pathfinder, HandleFn};
use azalea_client::{Account, Client, Plugins};
use azalea_protocol::ServerAddress;
use std::{future::Future, sync::Arc};
use thiserror::Error;
/// A helper macro that generates a [`Plugins`] struct from a list of objects
/// that implement [`Plugin`].
///
/// ```rust,no_run
/// plugins![azalea_pathfinder::Plugin];
/// ```
///
/// [`Plugin`]: crate::Plugin
#[macro_export]
macro_rules! plugins {
($($plugin:expr),*) => {
{
let mut plugins = azalea::Plugins::new();
$(
plugins.add($plugin);
)*
plugins
}
};
}
/// The options that are passed to [`azalea::start`].
///
/// [`azalea::start`]: crate::start()
pub struct Options<S, A, Fut>
where
A: TryInto<ServerAddress>,
Fut: Future<Output = Result<(), anyhow::Error>>,
{
/// The address of the server that we're connecting to. This can be a
/// `&str`, [`ServerAddress`], or anything that implements
/// `TryInto<ServerAddress>`.
///
/// [`ServerAddress`]: azalea_protocol::ServerAddress
pub address: A,
/// The account that's going to join the server.
pub account: Account,
/// The plugins that are going to be used. Plugins are external crates that
/// add extra functionality to Azalea. You should use the [`plugins`] macro
/// for this field.
///
/// ```rust,no_run
/// plugins![azalea_pathfinder::Plugin]
/// ```
pub plugins: Plugins,
/// A struct that contains the data that you want your bot to remember
/// across events.
///
/// # Examples
///
/// ```rust
/// use parking_lot::Mutex;
/// use std::sync::Arc;
///
/// #[derive(Default, Clone)]
/// struct State {
/// farming: Arc<Mutex<bool>>,
/// }
/// ```
pub state: S,
/// The function that's called whenever we get an event.
///
/// # Examples
///
/// ```rust
/// use azalea::prelude::*;
///
/// async fn handle(bot: Client, event: Event, state: State) -> anyhow::Result<()> {
/// Ok(())
/// }
/// ```
pub handle: HandleFn<Fut, S>,
}
#[derive(Error, Debug)]
pub enum StartError {
#[error("Invalid address")]
InvalidAddress,
#[error("Join error: {0}")]
Join(#[from] azalea_client::JoinError),
}
/// Join a server and start handling events. This function will run forever until
/// it gets disconnected from the server.
///
/// # Examples
///
/// ```rust,no_run
/// let error = azalea::start(azalea::Options {
/// account,
/// address: "localhost",
/// state: State::default(),
/// plugins: plugins![azalea_pathfinder::Plugin],
/// handle,
/// }).await;
/// ```
pub async fn start<
S: Send + Sync + Clone + 'static,
A: Send + TryInto<ServerAddress>,
Fut: Future<Output = Result<(), anyhow::Error>> + Send + 'static,
>(
options: Options<S, A, Fut>,
) -> Result<(), StartError> {
let address = match options.address.try_into() {
Ok(address) => address,
Err(_) => return Err(StartError::InvalidAddress),
};
let (mut bot, mut rx) = Client::join(&options.account, address).await?;
let mut plugins = options.plugins;
// DEFAULT PLUGINS
plugins.add(bot::Plugin);
plugins.add(pathfinder::Plugin);
bot.plugins = Arc::new(plugins.build());
let state = options.state;
while let Some(event) = rx.recv().await {
let cloned_plugins = (*bot.plugins).clone();
for plugin in cloned_plugins.into_iter() {
tokio::spawn(plugin.handle(event.clone(), bot.clone()));
}
tokio::spawn((options.handle)(bot.clone(), event.clone(), state.clone()));
}
Ok(())
}

147
azalea/src/swarm/chat.rs Normal file
View file

@ -0,0 +1,147 @@
//! Implements SwarmEvent::Chat
// How the chat event works (to avoid firing the event multiple times):
// ---
// There's a shared queue of all the chat messages
// Each bot contains an index of the farthest message we've seen
// When a bot receives a chat messages, it looks into the queue to find the
// earliest instance of the message content that's after the bot's chat index.
// If it finds it, then its personal index is simply updated. Otherwise, fire
// the event and add to the queue.
//
// To make sure the queue doesn't grow too large, we keep a `chat_min_index`
// 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 crate::{Swarm, SwarmEvent};
use async_trait::async_trait;
use azalea_client::{ChatPacket, Client, Event};
use parking_lot::Mutex;
use std::{collections::VecDeque, sync::Arc};
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
#[derive(Clone)]
pub struct Plugin {
pub swarm_state: SwarmState,
pub tx: UnboundedSender<ChatPacket>,
}
impl crate::Plugin for Plugin {
type State = State;
fn build(&self) -> State {
State {
farthest_chat_index: Arc::new(Mutex::new(0)),
swarm_state: self.swarm_state.clone(),
tx: self.tx.clone(),
}
}
}
#[derive(Clone)]
pub struct State {
pub farthest_chat_index: Arc<Mutex<usize>>,
pub tx: UnboundedSender<ChatPacket>,
pub swarm_state: SwarmState,
}
#[derive(Clone)]
pub struct SwarmState {
pub chat_queue: Arc<Mutex<VecDeque<ChatPacket>>>,
pub chat_min_index: Arc<Mutex<usize>>,
pub rx: Arc<tokio::sync::Mutex<UnboundedReceiver<ChatPacket>>>,
}
#[async_trait]
impl crate::PluginState for State {
async fn handle(self: Box<Self>, event: Event, _bot: Client) {
// we're allowed to access Plugin::swarm_state since it's shared for every bot
if let Event::Chat(m) = event {
// When a bot receives a chat messages, it looks into the queue to find the
// earliest instance of the message content that's after the bot's chat index.
// If it finds it, then its personal index is simply updated. Otherwise, fire
// the event and add to the queue.
let mut chat_queue = self.swarm_state.chat_queue.lock();
let chat_min_index = self.swarm_state.chat_min_index.lock();
let mut farthest_chat_index = self.farthest_chat_index.lock();
let actual_vec_index = *farthest_chat_index - *chat_min_index;
// go through the queue and find the first message that's after the bot's index
let mut found = false;
for (i, msg) in chat_queue.iter().enumerate().skip(actual_vec_index) {
if msg == &m {
// found the message, update the index
*farthest_chat_index = i + *chat_min_index + 1;
found = true;
break;
}
}
if !found {
// didn't find the message, so fire the swarm event and add to the queue
self.tx
.send(m.clone())
.expect("failed to send chat message to swarm");
chat_queue.push_back(m);
*farthest_chat_index = chat_queue.len() - 1 + *chat_min_index;
}
}
}
}
impl SwarmState {
pub fn new<S>(swarm: Swarm<S>) -> (Self, UnboundedSender<ChatPacket>)
where
S: Send + Sync + Clone + 'static,
{
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
let swarm_state = SwarmState {
chat_queue: Arc::new(Mutex::new(VecDeque::new())),
chat_min_index: Arc::new(Mutex::new(0)),
rx: Arc::new(tokio::sync::Mutex::new(rx)),
};
tokio::spawn(swarm_state.clone().start(swarm));
(swarm_state, tx)
}
async fn start<S>(self, swarm: Swarm<S>)
where
S: Send + Sync + Clone + 'static,
{
// it should never be locked unless we reused the same plugin for two swarms (bad)
let mut rx = self.rx.lock().await;
while let Some(m) = rx.recv().await {
swarm.swarm_tx.send(SwarmEvent::Chat(m)).unwrap();
// To make sure the queue doesn't grow too large, we keep a `chat_min_index`
// 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.
let chat_min_index = *self.chat_min_index.lock();
let mut new_chat_min_index = usize::MAX;
for (bot, _) in swarm.bot_datas.lock().iter() {
let this_farthest_chat_index = *bot
.plugins
.get::<State>()
.expect("Chat plugin not installed")
.farthest_chat_index
.lock();
if this_farthest_chat_index < new_chat_min_index {
new_chat_min_index = this_farthest_chat_index;
}
}
let mut chat_queue = self.chat_queue.lock();
// remove all messages from the queue that are before the min index
for _ in 0..(new_chat_min_index - chat_min_index) {
chat_queue.pop_front();
}
// update the min index
*self.chat_min_index.lock() = new_chat_min_index;
}
}
}

447
azalea/src/swarm/mod.rs Normal file
View file

@ -0,0 +1,447 @@
/// Swarms are a way to conveniently control many bots.
mod chat;
mod plugins;
pub use self::plugins::*;
use crate::{bot, HandleFn};
use azalea_client::{Account, ChatPacket, Client, Event, JoinError, Plugins};
use azalea_protocol::{
connect::{Connection, ConnectionError},
resolver::{self, ResolverError},
ServerAddress,
};
use azalea_world::WeakWorldContainer;
use futures::future::join_all;
use log::error;
use parking_lot::{Mutex, RwLock};
use std::{future::Future, net::SocketAddr, sync::Arc, time::Duration};
use thiserror::Error;
use tokio::sync::mpsc::{self, UnboundedSender};
/// A helper macro that generates a [`SwarmPlugins`] struct from a list of objects
/// that implement [`SwarmPlugin`].
///
/// ```rust,no_run
/// swarm_plugins![azalea_pathfinder::Plugin];
/// ```
#[macro_export]
macro_rules! swarm_plugins {
($($plugin:expr),*) => {
{
let mut plugins = azalea::SwarmPlugins::new();
$(
plugins.add($plugin);
)*
plugins
}
};
}
/// A swarm is a way to conveniently control many bots at once, while also
/// being able to control bots at an individual level when desired.
///
/// Swarms are created from the [`azalea::start_swarm`] function.
///
/// The `S` type parameter is the type of the state for individual bots.
/// It's used to make the [`Swarm::add`] function work.
///
/// [`azalea::start_swarm`]: fn.start_swarm.html
#[derive(Clone)]
pub struct Swarm<S> {
bot_datas: Arc<Mutex<Vec<(Client, S)>>>,
resolved_address: SocketAddr,
address: ServerAddress,
pub worlds: Arc<RwLock<WeakWorldContainer>>,
/// Plugins that are set for new bots
plugins: Plugins,
bots_tx: UnboundedSender<(Option<Event>, (Client, S))>,
swarm_tx: UnboundedSender<SwarmEvent>,
}
/// An event about something that doesn't have to do with a single bot.
#[derive(Clone, Debug)]
pub enum SwarmEvent {
/// All the bots in the swarm have successfully joined the server.
Login,
/// The swarm was created. This is only fired once, and it's guaranteed to
/// be the first event to fire.
Init,
/// A bot got disconnected from the server.
///
/// You can implement an auto-reconnect by calling [`Swarm::add`]
/// with the account from this event.
Disconnect(Account),
/// At least one bot received a chat message.
Chat(ChatPacket),
}
pub type SwarmHandleFn<Fut, S, SS> = fn(Swarm<S>, SwarmEvent, SS) -> Fut;
/// The options that are passed to [`azalea::start_swarm`].
///
/// [`azalea::start_swarm`]: crate::start_swarm()
pub struct SwarmOptions<S, SS, A, Fut, SwarmFut>
where
A: TryInto<ServerAddress>,
Fut: Future<Output = Result<(), anyhow::Error>>,
SwarmFut: Future<Output = Result<(), anyhow::Error>>,
{
/// The address of the server that we're connecting to. This can be a
/// `&str`, [`ServerAddress`], or anything that implements
/// `TryInto<ServerAddress>`.
///
/// [`ServerAddress`]: azalea_protocol::ServerAddress
pub address: A,
/// The accounts that are going to join the server.
pub accounts: Vec<Account>,
/// The plugins that are going to be used for all the bots.
///
/// You can usually leave this as `plugins![]`.
pub plugins: Plugins,
/// The plugins that are going to be used for the swarm.
///
/// You can usually leave this as `swarm_plugins![]`.
pub swarm_plugins: SwarmPlugins<S>,
/// The individual bot states. This must be the same length as `accounts`,
/// since each bot gets one state.
pub states: Vec<S>,
/// The state for the overall swarm.
pub swarm_state: SS,
/// The function that's called every time a bot receives an [`Event`].
pub handle: HandleFn<Fut, S>,
/// The function that's called every time the swarm receives a [`SwarmEvent`].
pub swarm_handle: SwarmHandleFn<SwarmFut, S, SS>,
/// How long we should wait between each bot joining the server. Set to
/// None to have every bot connect at the same time. None is different than
/// a duration of 0, since if a duration is present the bots will wait for
/// the previous one to be ready.
pub join_delay: Option<std::time::Duration>,
}
#[derive(Error, Debug)]
pub enum SwarmStartError {
#[error("Invalid address")]
InvalidAddress,
#[error(transparent)]
ResolveAddress(#[from] ResolverError),
#[error("Join error: {0}")]
Join(#[from] azalea_client::JoinError),
}
/// Make a bot [`Swarm`].
///
/// [`Swarm`]: struct.Swarm.html
///
/// # Examples
/// ```rust,no_run
/// use azalea::{prelude::*, Swarm, SwarmEvent};
/// use azalea::{Account, Client, Event};
/// use std::time::Duration;
///
/// #[derive(Default, Clone)]
/// struct State {}
///
/// #[derive(Default, Clone)]
/// struct SwarmState {}
///
/// #[tokio::main]
/// async fn main() -> anyhow::Result<()> {
/// let mut accounts = Vec::new();
/// let mut states = Vec::new();
///
/// for i in 0..10 {
/// accounts.push(Account::offline(&format!("bot{}", i)));
/// states.push(State::default());
/// }
///
/// loop {
/// let e = azalea::start_swarm(azalea::SwarmOptions {
/// accounts: accounts.clone(),
/// address: "localhost",
///
/// states: states.clone(),
/// swarm_state: SwarmState::default(),
///
/// plugins: plugins![],
/// swarm_plugins: swarm_plugins![],
///
/// handle,
/// swarm_handle,
///
/// join_delay: Some(Duration::from_millis(1000)),
/// })
/// .await;
/// println!("{e:?}");
/// }
/// }
///
/// async fn handle(bot: Client, event: Event, _state: State) -> anyhow::Result<()> {
/// match &event {
/// _ => {}
/// }
/// Ok(())
/// }
///
/// async fn swarm_handle(
/// mut swarm: Swarm<State>,
/// event: SwarmEvent,
/// _state: SwarmState,
/// ) -> anyhow::Result<()> {
/// match &event {
/// SwarmEvent::Disconnect(account) => {
/// // automatically reconnect after 5 seconds
/// tokio::time::sleep(Duration::from_secs(5)).await;
/// swarm.add(account, State::default()).await?;
/// }
/// SwarmEvent::Chat(m) => {
/// println!("{}", m.message().to_ansi(None));
/// }
/// _ => {}
/// }
/// Ok(())
/// }
pub async fn start_swarm<
S: Send + Sync + Clone + 'static,
SS: Send + Sync + Clone + 'static,
A: Send + TryInto<ServerAddress>,
Fut: Future<Output = Result<(), anyhow::Error>> + Send + 'static,
SwarmFut: Future<Output = Result<(), anyhow::Error>> + Send + 'static,
>(
options: SwarmOptions<S, SS, A, Fut, SwarmFut>,
) -> Result<(), SwarmStartError> {
assert_eq!(
options.accounts.len(),
options.states.len(),
"There must be exactly one state per bot."
);
// convert the TryInto<ServerAddress> into a ServerAddress
let address: ServerAddress = match options.address.try_into() {
Ok(address) => address,
Err(_) => return Err(SwarmStartError::InvalidAddress),
};
// resolve the address
let resolved_address = resolver::resolve_address(&address).await?;
let world_container = Arc::new(RwLock::new(WeakWorldContainer::default()));
let mut plugins = options.plugins;
let swarm_plugins = options.swarm_plugins;
// DEFAULT CLIENT PLUGINS
plugins.add(bot::Plugin);
plugins.add(crate::pathfinder::Plugin);
// DEFAULT SWARM PLUGINS
// we can't modify the swarm plugins after this
let (bots_tx, mut bots_rx) = mpsc::unbounded_channel();
let (swarm_tx, mut swarm_rx) = mpsc::unbounded_channel();
let mut swarm = Swarm {
bot_datas: Arc::new(Mutex::new(Vec::new())),
resolved_address,
address,
worlds: world_container,
plugins,
bots_tx,
swarm_tx: swarm_tx.clone(),
};
{
// the chat plugin is hacky and needs the swarm to be passed like this
let (chat_swarm_state, chat_tx) = chat::SwarmState::new(swarm.clone());
swarm.plugins.add(chat::Plugin {
swarm_state: chat_swarm_state,
tx: chat_tx,
});
}
let swarm_plugins = swarm_plugins.build();
let mut swarm_clone = swarm.clone();
let join_task = tokio::spawn(async move {
if let Some(join_delay) = options.join_delay {
// if there's a join delay, then join one by one
for (account, state) in options.accounts.iter().zip(options.states) {
swarm_clone
.add_with_exponential_backoff(account, state.clone())
.await;
tokio::time::sleep(join_delay).await;
}
} else {
let swarm_borrow = &swarm_clone;
join_all(options.accounts.iter().zip(options.states).map(
async move |(account, state)| -> Result<(), JoinError> {
swarm_borrow
.clone()
.add_with_exponential_backoff(account, state.clone())
.await;
Ok(())
},
))
.await;
}
});
let swarm_state = options.swarm_state;
let mut internal_state = InternalSwarmState::default();
// Watch swarm_rx and send those events to the plugins and swarm_handle.
let swarm_clone = swarm.clone();
let swarm_plugins_clone = swarm_plugins.clone();
tokio::spawn(async move {
while let Some(event) = swarm_rx.recv().await {
for plugin in swarm_plugins_clone.clone().into_iter() {
tokio::spawn(plugin.handle(event.clone(), swarm_clone.clone()));
}
tokio::spawn((options.swarm_handle)(
swarm_clone.clone(),
event,
swarm_state.clone(),
));
}
});
// bot events
while let Some((Some(event), (bot, state))) = bots_rx.recv().await {
// bot event handling
let cloned_plugins = (*bot.plugins).clone();
for plugin in cloned_plugins.into_iter() {
tokio::spawn(plugin.handle(event.clone(), bot.clone()));
}
// swarm event handling
// remove this #[allow] when more checks are added
#[allow(clippy::single_match)]
match &event {
Event::Login => {
internal_state.bots_joined += 1;
if internal_state.bots_joined == swarm.bot_datas.lock().len() {
swarm_tx.send(SwarmEvent::Login).unwrap();
}
}
_ => {}
}
tokio::spawn((options.handle)(bot, event, state));
}
join_task.abort();
Ok(())
}
impl<S> Swarm<S>
where
S: Send + Sync + Clone + 'static,
{
/// Add a new account to the swarm. You can remove it later by calling [`Client::disconnect`].
pub async fn add(&mut self, account: &Account, state: S) -> Result<Client, JoinError> {
let conn = Connection::new(&self.resolved_address).await?;
let (conn, game_profile) = Client::handshake(conn, account, &self.address.clone()).await?;
// tx is moved to the bot so it can send us events
// rx is used to receive events from the bot
let (tx, mut rx) = mpsc::channel(1);
let mut bot = Client::new(game_profile, conn, Some(self.worlds.clone()));
tx.send(Event::Init).await.expect("Failed to send event");
bot.start_tasks(tx);
bot.plugins = Arc::new(self.plugins.clone().build());
let cloned_bots_tx = self.bots_tx.clone();
let cloned_bot = bot.clone();
let cloned_state = state.clone();
let owned_account = account.clone();
let bot_datas = self.bot_datas.clone();
let swarm_tx = self.swarm_tx.clone();
// send the init event immediately so it's the first thing we get
swarm_tx.send(SwarmEvent::Init).unwrap();
tokio::spawn(async move {
while let Some(event) = rx.recv().await {
// we can't handle events here (since we can't copy the handler),
// they're handled above in start_swarm
if let Err(e) =
cloned_bots_tx.send((Some(event), (cloned_bot.clone(), cloned_state.clone())))
{
error!("Error sending event to swarm: {e}");
}
}
// the bot disconnected, so we remove it from the swarm
let mut bot_datas = bot_datas.lock();
let index = bot_datas
.iter()
.position(|(b, _)| b.profile.uuid == cloned_bot.profile.uuid)
.expect("bot disconnected but not found in swarm");
bot_datas.remove(index);
swarm_tx
.send(SwarmEvent::Disconnect(owned_account))
.unwrap();
});
self.bot_datas.lock().push((bot.clone(), state.clone()));
Ok(bot)
}
/// Add a new account to the swarm, retrying if it couldn't join. This will
/// run forever until the bot joins or the task is aborted.
///
/// Exponential backoff means if it fails joining it will initially wait 10
/// seconds, then 20, then 40, up to 2 minutes.
pub async fn add_with_exponential_backoff(&mut self, account: &Account, state: S) -> Client {
let mut disconnects = 0;
loop {
match self.add(account, state.clone()).await {
Ok(bot) => return bot,
Err(e) => {
disconnects += 1;
let delay = (Duration::from_secs(5) * 2u32.pow(disconnects))
.min(Duration::from_secs(120));
let username = account.username.clone();
error!("Error joining {username}: {e}. Waiting {delay:?} and trying again.");
tokio::time::sleep(delay).await;
}
}
}
}
}
impl<S> IntoIterator for Swarm<S>
where
S: Send + Sync + Clone + 'static,
{
type Item = (Client, S);
type IntoIter = std::vec::IntoIter<Self::Item>;
/// Iterate over the bots and their states in this swarm.
///
/// ```rust,no_run
/// for (bot, state) in swarm {
/// // ...
/// }
/// ```
fn into_iter(self) -> Self::IntoIter {
self.bot_datas.lock().clone().into_iter()
}
}
#[derive(Default)]
struct InternalSwarmState {
/// The number of bots connected to the server
pub bots_joined: usize,
}
impl From<ConnectionError> for SwarmStartError {
fn from(e: ConnectionError) -> Self {
SwarmStartError::from(JoinError::from(e))
}
}

134
azalea/src/swarm/plugins.rs Normal file
View file

@ -0,0 +1,134 @@
use crate::{Swarm, SwarmEvent};
use async_trait::async_trait;
use nohash_hasher::NoHashHasher;
use std::{
any::{Any, TypeId},
collections::HashMap,
hash::BuildHasherDefault,
};
type U64Hasher = BuildHasherDefault<NoHashHasher<u64>>;
// kind of based on https://docs.rs/http/latest/src/http/extensions.rs.html
/// A map of plugin ids to [`SwarmPlugin`] trait objects. The client stores
/// this so we can keep the state for our [`Swarm`] plugins.
///
/// If you're using azalea, you should generate this from the `swarm_plugins!` macro.
#[derive(Clone, Default)]
pub struct SwarmPlugins<S> {
map: Option<HashMap<TypeId, Box<dyn SwarmPlugin<S>>, U64Hasher>>,
}
#[derive(Clone)]
pub struct SwarmPluginStates<S> {
map: Option<HashMap<TypeId, Box<dyn SwarmPluginState<S>>, U64Hasher>>,
}
impl<S> SwarmPluginStates<S> {
pub fn get<T: SwarmPluginState<S>>(&self) -> Option<&T> {
self.map
.as_ref()
.and_then(|map| map.get(&TypeId::of::<T>()))
.and_then(|boxed| (boxed.as_ref() as &dyn Any).downcast_ref::<T>())
}
}
impl<S> SwarmPlugins<S>
where
S: 'static,
{
/// Create a new empty set of plugins.
pub fn new() -> Self {
Self { map: None }
}
/// Add a new plugin to this set.
pub fn add<T: SwarmPlugin<S>>(&mut self, plugin: T) {
if self.map.is_none() {
self.map = Some(HashMap::with_hasher(BuildHasherDefault::default()));
}
self.map
.as_mut()
.unwrap()
.insert(TypeId::of::<T>(), Box::new(plugin));
}
/// Build our plugin states from this set of plugins. Note that if you're
/// using `azalea` you'll probably never need to use this as it's called
/// for you.
pub fn build(self) -> SwarmPluginStates<S> {
if self.map.is_none() {
return SwarmPluginStates { map: None };
}
let mut map = HashMap::with_hasher(BuildHasherDefault::default());
for (id, plugin) in self.map.unwrap().into_iter() {
map.insert(id, plugin.build());
}
SwarmPluginStates { map: Some(map) }
}
}
impl<S> IntoIterator for SwarmPluginStates<S> {
type Item = Box<dyn SwarmPluginState<S>>;
type IntoIter = std::vec::IntoIter<Self::Item>;
/// Iterate over the plugin states.
fn into_iter(self) -> Self::IntoIter {
self.map
.map(|map| map.into_values().collect::<Vec<_>>())
.unwrap_or_default()
.into_iter()
}
}
/// A `SwarmPluginState` keeps the current state of a plugin for a client. All
/// the fields must be atomic. Unique `SwarmPluginState`s are built from
/// [`SwarmPlugin`]s.
#[async_trait]
pub trait SwarmPluginState<S>: Send + Sync + SwarmPluginStateClone<S> + Any + 'static {
async fn handle(self: Box<Self>, event: SwarmEvent, swarm: Swarm<S>);
}
/// Swarm plugins can keep their own personal state ([`SwarmPluginState`]),
/// listen to [`SwarmEvent`]s, and add new functions to [`Swarm`].
pub trait SwarmPlugin<S>: Send + Sync + SwarmPluginClone<S> + Any + 'static {
fn build(&self) -> Box<dyn SwarmPluginState<S>>;
}
/// An internal trait that allows SwarmPluginState to be cloned.
#[doc(hidden)]
pub trait SwarmPluginStateClone<S> {
fn clone_box(&self) -> Box<dyn SwarmPluginState<S>>;
}
impl<T, S> SwarmPluginStateClone<S> for T
where
T: 'static + SwarmPluginState<S> + Clone,
{
fn clone_box(&self) -> Box<dyn SwarmPluginState<S>> {
Box::new(self.clone())
}
}
impl<S> Clone for Box<dyn SwarmPluginState<S>> {
fn clone(&self) -> Self {
self.clone_box()
}
}
/// An internal trait that allows SwarmPlugin to be cloned.
#[doc(hidden)]
pub trait SwarmPluginClone<S> {
fn clone_box(&self) -> Box<dyn SwarmPlugin<S>>;
}
impl<T, S> SwarmPluginClone<S> for T
where
T: 'static + SwarmPlugin<S> + Clone,
{
fn clone_box(&self) -> Box<dyn SwarmPlugin<S>> {
Box::new(self.clone())
}
}
impl<S> Clone for Box<dyn SwarmPlugin<S>> {
fn clone(&self) -> Self {
self.clone_box()
}
}

View file

@ -10,7 +10,9 @@ version = "0.2.0"
[dependencies]
anyhow = "1.0.65"
azalea = {path = "../azalea"}
azalea-protocol = {path = "../azalea-protocol"}
env_logger = "0.9.1"
parking_lot = {version = "^0.12.1", features = ["deadlock_detection"]}
rand = "0.8.5"
tokio = "1.19.2"
uuid = "1.1.2"

132
bot/src/main.rs Executable file → Normal file
View file

@ -1,16 +1,21 @@
use azalea::pathfinder::BlockPosGoal;
use azalea::{prelude::*, BlockPos};
// use azalea::ClientInformation;
use azalea::{prelude::*, BlockPos, Swarm, SwarmEvent, WalkDirection};
use azalea::{Account, Client, Event};
use azalea_protocol::packets::game::serverbound_client_command_packet::ServerboundClientCommandPacket;
use std::time::Duration;
#[derive(Default, Clone)]
struct State {}
#[derive(Default, Clone)]
struct SwarmState {}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
env_logger::init();
{
// only for #[cfg]
use parking_lot::deadlock;
use std::thread;
use std::time::Duration;
@ -32,52 +37,125 @@ async fn main() -> anyhow::Result<()> {
}
}
});
} // only for #[cfg]
}
// let account = Account::microsoft("example@example.com").await?;
let account = Account::offline("bot");
let mut accounts = Vec::new();
let mut states = Vec::new();
for i in 0..7 {
accounts.push(Account::offline(&format!("bot{}", i)));
states.push(State::default());
}
loop {
let e = azalea::start(azalea::Options {
account: account.clone(),
let e = azalea::start_swarm(azalea::SwarmOptions {
accounts: accounts.clone(),
address: "localhost",
state: State::default(),
states: states.clone(),
swarm_state: SwarmState::default(),
plugins: plugins![],
swarm_plugins: swarm_plugins![],
handle,
swarm_handle,
join_delay: Some(Duration::from_millis(1000)),
// join_delay: None,
})
.await;
println!("{e:?}");
}
}
async fn handle(bot: Client, event: Event, _state: State) -> anyhow::Result<()> {
async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<()> {
match event {
Event::Init => {
// bot.set_client_information(ClientInformation {
// view_distance: 2,
// ..Default::default()
// })
// .await?;
}
Event::Login => {
// bot.chat("Hello world").await?;
bot.chat("Hello world").await?;
}
Event::Chat(m) => {
println!("{}", m.message().to_ansi(None));
if m.message().to_string() == "<py5> goto" {
let target_pos_vec3 = *(bot
.world
.read()
.entity_by_uuid(&uuid::uuid!("6536bfed869548fd83a1ecd24cf2a0fd"))
.unwrap()
.pos());
let target_pos: BlockPos = (&target_pos_vec3).into();
// bot.look_at(&target_pos_vec3);
bot.goto(BlockPosGoal::from(target_pos));
// bot.walk(WalkDirection::Forward);
if m.content() == bot.profile.name {
bot.chat("Bye").await?;
tokio::time::sleep(Duration::from_millis(50)).await;
bot.disconnect().await?;
}
let entity = bot
.world
.read()
.entity_by_uuid(&uuid::uuid!("6536bfed-8695-48fd-83a1-ecd24cf2a0fd"));
if let Some(entity) = entity {
if m.content() == "goto" {
let target_pos_vec3 = entity.pos();
let target_pos: BlockPos = target_pos_vec3.into();
bot.goto(BlockPosGoal::from(target_pos));
} else if m.content() == "look" {
let target_pos_vec3 = entity.pos();
let target_pos: BlockPos = target_pos_vec3.into();
println!("target_pos: {:?}", target_pos);
bot.look_at(&target_pos.center());
} else if m.content() == "jump" {
bot.set_jumping(true);
} else if m.content() == "walk" {
bot.walk(WalkDirection::Forward);
} else if m.content() == "stop" {
bot.set_jumping(false);
bot.walk(WalkDirection::None);
} else if m.content() == "lag" {
std::thread::sleep(Duration::from_millis(1000));
}
}
}
Event::Initialize => {
println!("initialized");
}
Event::Tick => {
// bot.jump();
Event::Death(_) => {
bot.write_packet(ServerboundClientCommandPacket {
action: azalea_protocol::packets::game::serverbound_client_command_packet::Action::PerformRespawn,
}.get()).await?;
}
_ => {}
}
Ok(())
}
async fn swarm_handle(
mut swarm: Swarm<State>,
event: SwarmEvent,
_state: SwarmState,
) -> anyhow::Result<()> {
match &event {
SwarmEvent::Disconnect(account) => {
println!("bot got kicked! {}", account.username);
tokio::time::sleep(Duration::from_secs(5)).await;
swarm.add(account, State::default()).await?;
}
SwarmEvent::Chat(m) => {
println!("swarm chat message: {}", m.message().to_ansi(None));
if m.message().to_string() == "<py5> world" {
for (name, world) in &swarm.worlds.read().worlds {
println!("world name: {}", name);
if let Some(w) = world.upgrade() {
for chunk_pos in w.chunk_storage.read().chunks.values() {
println!("chunk: {:?}", chunk_pos);
}
} else {
println!("nvm world is gone");
}
}
}
if m.message().to_string() == "<py5> hi" {
for (bot, _) in swarm {
bot.chat("hello").await?;
}
}
}
_ => {}
}
Ok(())
}