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

Merge branch 'main' into swarm

This commit is contained in:
mat 2022-11-13 00:36:22 -06:00
commit 9ffe57b9a5
57 changed files with 10997 additions and 707 deletions

601
Cargo.lock generated Executable file → Normal file

File diff suppressed because it is too large Load diff

View file

@ -39,5 +39,5 @@ I wanted a fun excuse to do something cool with Rust, and I also felt like I cou
## Stretch goals
- Server implementation.
- Server implementation. (if you're interested in making this then please contact me, I'd love for it to be a thing. [@mat:matdoes.dev](https://matrix.to/#/@mat:matdoes.dev) / mat#1592)
- Having branches for several popular Minecraft versions.

View file

@ -9,8 +9,8 @@ version = "0.3.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
azalea-buf = {path = "../azalea-buf", version = "^0.3.0" }
azalea-crypto = {path = "../azalea-crypto", version = "^0.3.0" }
azalea-buf = {path = "../azalea-buf", version = "^0.3.0"}
azalea-crypto = {path = "../azalea-crypto", version = "^0.3.0"}
chrono = {version = "0.4.22", default-features = false}
log = "0.4.17"
num-bigint = "0.4.3"

View file

@ -13,7 +13,7 @@ azalea-buf-macros = {path = "./azalea-buf-macros", version = "^0.3.0" }
byteorder = "^1.4.3"
serde_json = {version = "^1.0", optional = true}
thiserror = "^1.0.34"
tokio = {version = "^1.19.2", features = ["io-util", "net", "macros"]}
tokio = {version = "^1.21.2", features = ["io-util", "net", "macros"]}
uuid = "^1.1.2"
[features]

View file

@ -105,9 +105,14 @@ fn create_impl_mcbufreadable(ident: &Ident, data: &Data) -> proc_macro2::TokenSt
quote! {
impl azalea_buf::McBufReadable for #ident {
fn read_from(buf: &mut std::io::Cursor<&[u8]>) -> Result<Self, azalea_buf::BufReadError>
{
fn read_from(buf: &mut std::io::Cursor<&[u8]>) -> Result<Self, azalea_buf::BufReadError> {
let id = azalea_buf::McBufVarReadable::var_read_from(buf)?;
Self::read_from_id(buf, id)
}
}
impl #ident {
pub fn read_from_id(buf: &mut std::io::Cursor<&[u8]>, id: u32) -> Result<Self, azalea_buf::BufReadError> {
match id {
#match_contents
// you'd THINK this throws an error, but mojang decided to make it default for some reason
@ -170,6 +175,7 @@ fn create_impl_mcbufwritable(ident: &Ident, data: &Data) -> proc_macro2::TokenSt
// remember whether it's a data variant so we can do an optimization later
let mut is_data_enum = false;
let mut match_arms = quote!();
let mut match_arms_without_id = quote!();
let mut variant_discrim: u32 = 0;
let mut first = true;
for variant in variants {
@ -208,6 +214,9 @@ fn create_impl_mcbufwritable(ident: &Ident, data: &Data) -> proc_macro2::TokenSt
azalea_buf::McBufVarWritable::var_write_into(&#variant_discrim, buf)?;
}
});
match_arms_without_id.extend(quote! {
Self::#variant_name => {}
});
}
syn::Fields::Unnamed(_) => {
is_data_enum = true;
@ -218,6 +227,11 @@ fn create_impl_mcbufwritable(ident: &Ident, data: &Data) -> proc_macro2::TokenSt
azalea_buf::McBufWritable::write_into(data, buf)?;
}
});
match_arms_without_id.extend(quote! {
Self::#variant_name(data) => {
azalea_buf::McBufWritable::write_into(data, buf)?;
}
});
}
}
}
@ -231,6 +245,14 @@ fn create_impl_mcbufwritable(ident: &Ident, data: &Data) -> proc_macro2::TokenSt
Ok(())
}
}
impl #ident {
pub fn write_without_id(&self, buf: &mut impl std::io::Write) -> Result<(), std::io::Error> {
match self {
#match_arms_without_id
}
Ok(())
}
}
}
} else {
// optimization: if it doesn't have data we can just do `as u32`

View file

@ -289,3 +289,9 @@ impl Display for Component {
}
}
}
impl Default for Component {
fn default() -> Self {
Component::Text(TextComponent::default())
}
}

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)]
#[derive(Clone, Debug, Default)]
pub struct TextComponent {
pub base: BaseComponent,
pub text: String,

2
azalea-client/Cargo.toml Executable file → Normal file
View file

@ -23,6 +23,6 @@ log = "0.4.17"
nohash-hasher = "0.2.0"
parking_lot = {version = "0.12.1", features = ["deadlock_detection"]}
thiserror = "^1.0.34"
tokio = {version = "^1.19.2", features = ["sync"]}
tokio = {version = "^1.21.2", features = ["sync"]}
typemap_rev = "0.2.0"
uuid = "^1.1.2"

View file

@ -11,9 +11,9 @@ use crate::Client;
impl Client {
/// Sends chat message to the server. This only sends the chat packet and
/// not the command packet. The [`Client::chat`] function handles checking whether
/// the message is a command and using the proper packet for you, so you
/// should use that instead.
/// not the command packet. The [`Client::chat`] function handles checking
/// whether the message is a command and using the proper packet for you,
/// so you should use that instead.
pub async fn send_chat_packet(&self, message: &str) -> Result<(), std::io::Error> {
// TODO: chat signing
let signature = sign_message();

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

@ -28,10 +28,10 @@ use azalea_protocol::{
resolver, ServerAddress,
};
use azalea_world::{
entity::{Entity, EntityData},
entity::{metadata, Entity, EntityData, EntityMetadata},
Dimension,
};
use log::{debug, error, warn};
use log::{debug, error, info, warn};
use parking_lot::{Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard};
use std::{
fmt::Debug,
@ -101,6 +101,10 @@ pub struct Client {
pub struct PhysicsState {
/// Minecraft only sends a movement packet either after 20 ticks or if the player moved enough. This is that tick counter.
pub position_remainder: u32,
pub was_sprinting: bool,
// Whether we're going to try to start sprinting this tick. Equivalent to
// holding down ctrl for a tick.
pub trying_to_sprint: bool,
pub move_direction: WalkDirection,
pub forward_impulse: f32,
@ -139,7 +143,9 @@ pub enum HandleError {
impl Client {
/// Connect to a Minecraft server.
///
/// To change the render distance and other settings, use [`Client::set_client_information`].
/// To change the render distance and other settings, use
/// [`Client::set_client_information`]. To watch for events like packets
/// sent by the server, use the `rx` variable this function returns.
///
/// # Examples
///
@ -149,7 +155,7 @@ impl Client {
/// #[tokio::main]
/// async fn main() -> Box<dyn std::error::Error> {
/// let account = Account::offline("bot");
/// let client = Client::join(&account, "localhost").await?;
/// let (client, rx) = Client::join(&account, "localhost").await?;
/// client.chat("Hello, world!").await?;
/// client.shutdown().await?;
/// }
@ -315,6 +321,13 @@ impl Client {
}
},
Err(e) => {
if let ReadPacketError::ConnectionClosed = e {
info!("Connection closed");
if let Err(e) = client.shutdown().await {
error!("Error shutting down connection: {:?}", e);
}
return;
}
if IGNORE_ERRORS {
warn!("{}", e);
match e {
@ -405,7 +418,11 @@ impl Client {
// i'll make this an actual setting later
*dimension_lock = Dimension::new(16, height, min_y);
let entity = EntityData::new(client.game_profile.uuid, Vec3::default());
let entity = EntityData::new(
client.game_profile.uuid,
Vec3::default(),
EntityMetadata::Player(metadata::Player::default()),
);
dimension_lock.add_entity(p.player_id, entity);
let mut player_lock = client.player.write();
@ -416,6 +433,10 @@ impl Client {
// send the client information that we have set
let client_information_packet: ClientInformation =
client.client_information.read().clone();
log::debug!(
"Sending client information because login: {:?}",
client_information_packet
);
client.write_packet(client_information_packet.get()).await?;
// brand
@ -571,21 +592,30 @@ impl Client {
let pos = ChunkPos::new(p.x, p.z);
// let chunk = Chunk::read_with_world_height(&mut p.chunk_data);
// debug("chunk {:?}")
let mut dimension_lock = client.dimension.write();
dimension_lock
if let Err(e) = client
.dimension
.write()
.replace_with_packet_data(&pos, &mut Cursor::new(&p.chunk_data.data))
.unwrap();
{
error!("Couldn't set chunk data: {}", e);
}
}
ClientboundGamePacket::LightUpdate(p) => {
debug!("Got light update packet {:?}", p);
ClientboundGamePacket::LightUpdate(_p) => {
// debug!("Got light update packet {:?}", p);
}
ClientboundGamePacket::AddEntity(p) => {
debug!("Got add entity packet {:?}", p);
let entity = EntityData::from(p);
client.dimension.write().add_entity(p.id, entity);
}
ClientboundGamePacket::SetEntityData(_p) => {
// debug!("Got set entity data packet {:?}", p);
ClientboundGamePacket::SetEntityData(p) => {
debug!("Got set entity data packet {:?}", p);
let mut dimension = client.dimension.write();
if let Some(mut entity) = dimension.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);
}
}
ClientboundGamePacket::UpdateAttributes(_p) => {
// debug!("Got update attributes packet {:?}", p);
@ -870,6 +900,10 @@ impl Client {
let client_information = self.client_information.read();
client_information.clone().get()
};
log::debug!(
"Sending client information (already logged in): {:?}",
client_information_packet
);
self.write_packet(client_information_packet).await?;
}

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

@ -1,10 +1,11 @@
//! Significantly abstract [`azalea_protocol`] so it's actually useable for
//! real clients. If you want to make bots, however, you should use the
//! real clients. If you want to make bots, you should use the
//! [`azalea`] crate instead.
//!
//! [`azalea_protocol`]: https://crates.io/crates/azalea-protocol
//! [`azalea`]: https://crates.io/crates/azalea
#![allow(incomplete_features)]
#![feature(trait_upcasting)]
mod account;
@ -18,7 +19,7 @@ mod plugins;
pub use account::Account;
pub use client::{ChatPacket, Client, ClientInformation, Event, JoinError};
pub use movement::WalkDirection;
pub use movement::{SprintDirection, WalkDirection};
pub use player::Player;
pub use plugins::{Plugin, Plugins};

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

@ -2,6 +2,7 @@ use crate::Client;
use azalea_core::Vec3;
use azalea_physics::collision::{MovableEntity, MoverType};
use azalea_physics::HasPhysics;
use azalea_protocol::packets::game::serverbound_player_command_packet::ServerboundPlayerCommandPacket;
use azalea_protocol::packets::game::{
serverbound_move_player_pos_packet::ServerboundMovePlayerPosPacket,
serverbound_move_player_pos_rot_packet::ServerboundMovePlayerPosRotPacket,
@ -28,24 +29,19 @@ impl From<MoveEntityError> for MovePlayerError {
}
impl Client {
/// This gets called every tick.
pub async fn send_position(&mut self) -> Result<(), MovePlayerError> {
/// This gets called automatically every tick.
pub(crate) async fn send_position(&mut self) -> Result<(), MovePlayerError> {
let packet = {
let player_lock = self.player.write();
let mut physics_state = self.physics_state.lock();
let mut dimension_lock = self.dimension.write();
let mut player_entity = player_lock
.entity_mut(&mut dimension_lock)
.expect("Player must exist");
let player_pos = player_entity.pos();
let player_old_pos = player_entity.last_pos;
// TODO: send sprinting and sneaking packets here if they changed
self.send_sprinting_if_needed().await?;
// TODO: the camera being able to be controlled by other entities isn't implemented yet
// if !self.is_controlled_camera() { return };
let mut physics_state = self.physics_state.lock();
let player_entity = self.entity();
let player_pos = player_entity.pos();
let player_old_pos = player_entity.last_pos;
let x_delta = player_pos.x - player_old_pos.x;
let y_delta = player_pos.y - player_old_pos.y;
let z_delta = player_pos.z - player_old_pos.z;
@ -105,6 +101,9 @@ impl Client {
None
};
drop(player_entity);
let mut player_entity = self.entity_mut();
if sending_position {
player_entity.last_pos = *player_entity.pos();
physics_state.position_remainder = 0;
@ -127,6 +126,31 @@ impl Client {
Ok(())
}
async fn send_sprinting_if_needed(&mut self) -> Result<(), MovePlayerError> {
let is_sprinting = self.entity().metadata.sprinting;
let was_sprinting = self.physics_state.lock().was_sprinting;
if is_sprinting != was_sprinting {
let sprinting_action = if is_sprinting {
azalea_protocol::packets::game::serverbound_player_command_packet::Action::StartSprinting
} else {
azalea_protocol::packets::game::serverbound_player_command_packet::Action::StopSprinting
};
let player_entity_id = self.entity().id;
self.write_packet(
ServerboundPlayerCommandPacket {
id: player_entity_id,
action: sprinting_action,
data: 0,
}
.get(),
)
.await?;
self.physics_state.lock().was_sprinting = is_sprinting;
}
Ok(())
}
// Set our current position to the provided Vec3, potentially clipping through blocks.
pub async fn set_pos(&mut self, new_pos: Vec3) -> Result<(), MovePlayerError> {
let player_lock = self.player.write();
@ -160,19 +184,38 @@ impl Client {
pub fn ai_step(&mut self) {
self.tick_controls(None);
let mut dimension_lock = self.dimension.write();
let player_lock = self.player.write();
let mut player_entity = player_lock
.entity_mut(&mut dimension_lock)
.expect("Player must exist");
// server ai step
{
let mut player_entity = self.entity_mut();
let physics_state = self.physics_state.lock();
player_entity.xxa = physics_state.left_impulse;
player_entity.zza = physics_state.forward_impulse;
}
// TODO: food data and abilities
// let has_enough_food_to_sprint = self.food_data().food_level || self.abilities().may_fly;
let has_enough_food_to_sprint = true;
// TODO: double tapping w to sprint i think
let trying_to_sprint = self.physics_state.lock().trying_to_sprint;
if !self.sprinting()
&& (
// !self.is_in_water()
// || self.is_underwater() &&
self.has_enough_impulse_to_start_sprinting()
&& has_enough_food_to_sprint
// && !self.using_item()
// && !self.has_effect(MobEffects.BLINDNESS)
&& trying_to_sprint
)
{
self.set_sprinting(true);
}
let mut player_entity = self.entity_mut();
player_entity.ai_step();
}
@ -212,20 +255,59 @@ impl Client {
}
}
/// Start walking in the given direction.
/// Start walking in the given direction. To sprint, use
/// [`Client::sprint`]. To stop walking, call walk with
/// `WalkDirection::None`.
pub fn walk(&mut self, direction: WalkDirection) {
let mut physics_state = self.physics_state.lock();
physics_state.move_direction = direction;
{
let mut physics_state = self.physics_state.lock();
physics_state.move_direction = direction;
}
self.set_sprinting(false);
}
/// Toggle whether we're jumping. This acts as if you held space in
/// Start sprinting in the given direction. To stop moving, call
/// [`Client::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);
physics_state.trying_to_sprint = true;
}
// Whether we're currently sprinting.
pub fn sprinting(&self) -> bool {
self.entity().metadata.sprinting
}
/// Change whether we're sprinting by adding an attribute modifier to the
/// player. You should use the [`walk`] and [`sprint`] methods instead.
/// Returns if the operation was successful.
fn set_sprinting(&mut self, sprinting: bool) -> bool {
let mut player_entity = self.entity_mut();
player_entity.metadata.sprinting = sprinting;
if sprinting {
player_entity
.attributes
.speed
.insert(azalea_world::entity::attributes::sprinting_modifier())
.is_ok()
} else {
player_entity
.attributes
.speed
.remove(&azalea_world::entity::attributes::sprinting_modifier().uuid)
.is_none()
}
}
/// Set whether we're jumping. This acts as if you held space in
/// vanilla. If you want to jump once, use the `jump` function.
///
/// If you're making a realistic client, calling this function every tick is
/// recommended.
pub fn set_jumping(&mut self, jumping: bool) {
let mut player_entity = self.entity_mut();
player_entity.jumping = jumping;
}
@ -243,6 +325,16 @@ impl Client {
let mut player_entity = self.entity_mut();
player_entity.set_rotation(y_rot, x_rot);
}
// Whether the player is moving fast enough to be able to start sprinting.
fn has_enough_impulse_to_start_sprinting(&self) -> bool {
// if self.underwater() {
// self.has_forward_impulse()
// } else {
let physics_state = self.physics_state.lock();
physics_state.forward_impulse > 0.8
// }
}
}
#[derive(Clone, Copy, Debug, Default)]
@ -258,3 +350,21 @@ pub enum WalkDirection {
BackwardRight,
BackwardLeft,
}
/// The directions that we can sprint in. It's a subset of [`WalkDirection`].
#[derive(Clone, Copy, Debug)]
pub enum SprintDirection {
Forward,
ForwardRight,
ForwardLeft,
}
impl From<SprintDirection> for WalkDirection {
fn from(d: SprintDirection) -> Self {
match d {
SprintDirection::Forward => WalkDirection::Forward,
SprintDirection::ForwardRight => WalkDirection::ForwardRight,
SprintDirection::ForwardLeft => WalkDirection::ForwardLeft,
}
}
}

View file

@ -51,8 +51,12 @@ impl BitSet {
end_word_index = self.data.len() - 1;
}
let first_word_mask = u64::MAX << from_index;
let last_word_mask = u64::MAX >> (64 - (to_index % 64));
let first_word_mask = u64::MAX.wrapping_shl(
from_index
.try_into()
.expect("from_index shouldn't be larger than u32"),
);
let last_word_mask = u64::MAX.wrapping_shr((64 - (to_index % 64)) as u32);
if start_word_index == end_word_index {
// Case 1: One word
self.data[start_word_index] &= !(first_word_mask & last_word_mask);
@ -84,7 +88,7 @@ impl BitSet {
return from_index;
}
let mut word = !self.data[u] & (u64::MAX << from_index);
let mut word = !self.data[u] & (u64::MAX.wrapping_shl(from_index.try_into().unwrap()));
loop {
if word != 0 {
@ -136,4 +140,22 @@ mod tests {
assert_eq!(bitset.index(65), true);
assert_eq!(bitset.index(66), true);
}
#[test]
fn test_clear_2() {
let mut bitset = BitSet::new(128);
bitset.set(64);
bitset.set(65);
bitset.set(66);
bitset.set(67);
bitset.set(68);
bitset.clear(65, 67);
assert_eq!(bitset.index(64), true);
assert_eq!(bitset.index(65), false);
assert_eq!(bitset.index(66), false);
assert_eq!(bitset.index(67), true);
assert_eq!(bitset.index(68), true);
}
}

View file

@ -2,14 +2,24 @@ use azalea_buf::McBuf;
use crate::floor_mod;
#[derive(Clone, Copy, Debug, McBuf)]
#[derive(Clone, Copy, Debug, McBuf, Default)]
pub enum Direction {
#[default]
Down = 0,
Up = 1,
North = 2,
South = 3,
West = 4,
East = 5,
Up,
North,
South,
West,
East,
}
// TODO: make azalea_block use this instead of FacingCardinal
#[derive(Clone, Copy, Debug, McBuf)]
pub enum CardinalDirection {
North,
South,
West,
East,
}
#[derive(Clone, Copy, Debug)]
@ -26,6 +36,55 @@ pub enum AxisCycle {
Backward = 2,
}
impl CardinalDirection {
#[inline]
pub fn x(self) -> i32 {
match self {
CardinalDirection::East => 1,
CardinalDirection::West => -1,
_ => 0,
}
}
#[inline]
pub fn z(self) -> i32 {
match self {
CardinalDirection::South => 1,
CardinalDirection::North => -1,
_ => 0,
}
}
pub fn iter() -> impl Iterator<Item = CardinalDirection> {
[
CardinalDirection::North,
CardinalDirection::South,
CardinalDirection::West,
CardinalDirection::East,
]
.iter()
.copied()
}
#[inline]
pub fn right(self) -> CardinalDirection {
match self {
CardinalDirection::North => CardinalDirection::East,
CardinalDirection::South => CardinalDirection::West,
CardinalDirection::West => CardinalDirection::North,
CardinalDirection::East => CardinalDirection::South,
}
}
#[inline]
pub fn left(self) -> CardinalDirection {
match self {
CardinalDirection::North => CardinalDirection::West,
CardinalDirection::South => CardinalDirection::East,
CardinalDirection::West => CardinalDirection::South,
CardinalDirection::East => CardinalDirection::North,
}
}
}
impl Axis {
/// Pick x, y, or z from the arguments depending on the axis.
#[inline]

View file

@ -1,15 +1,14 @@
use crate::{BlockPos, Slot};
use azalea_buf::{BufReadError, McBuf, McBufReadable, McBufVarReadable, McBufWritable};
use std::io::{Cursor, Write};
use azalea_buf::McBuf;
#[derive(Debug, Clone, McBuf)]
#[derive(Debug, Clone, McBuf, Default)]
pub struct Particle {
#[var]
pub id: i32,
pub data: ParticleData,
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, McBuf, Default)]
pub enum ParticleData {
AmbientEntityEffect,
AngryVillager,
@ -32,6 +31,7 @@ pub enum ParticleData {
EnchantedHit,
Enchant,
EndRod,
#[default]
EntityEffect,
ExplosionEmitter,
Explosion,
@ -151,112 +151,3 @@ pub struct VibrationParticle {
#[var]
pub ticks: u32,
}
impl ParticleData {
pub fn read_from_particle_id(buf: &mut Cursor<&[u8]>, id: u32) -> Result<Self, BufReadError> {
Ok(match id {
0 => ParticleData::AmbientEntityEffect,
1 => ParticleData::AngryVillager,
2 => ParticleData::Block(BlockParticle::read_from(buf)?),
3 => ParticleData::BlockMarker(BlockParticle::read_from(buf)?),
4 => ParticleData::Bubble,
5 => ParticleData::Cloud,
6 => ParticleData::Crit,
7 => ParticleData::DamageIndicator,
8 => ParticleData::DragonBreath,
9 => ParticleData::DrippingLava,
10 => ParticleData::FallingLava,
11 => ParticleData::LandingLava,
12 => ParticleData::DrippingWater,
13 => ParticleData::FallingWater,
14 => ParticleData::Dust(DustParticle::read_from(buf)?),
15 => ParticleData::DustColorTransition(DustColorTransitionParticle::read_from(buf)?),
16 => ParticleData::Effect,
17 => ParticleData::ElderGuardian,
18 => ParticleData::EnchantedHit,
19 => ParticleData::Enchant,
20 => ParticleData::EndRod,
21 => ParticleData::EntityEffect,
22 => ParticleData::ExplosionEmitter,
23 => ParticleData::Explosion,
24 => ParticleData::FallingDust(BlockParticle::read_from(buf)?),
25 => ParticleData::Firework,
26 => ParticleData::Fishing,
27 => ParticleData::Flame,
28 => ParticleData::SoulFireFlame,
29 => ParticleData::Soul,
30 => ParticleData::Flash,
31 => ParticleData::HappyVillager,
32 => ParticleData::Composter,
33 => ParticleData::Heart,
34 => ParticleData::InstantEffect,
35 => ParticleData::Item(ItemParticle::read_from(buf)?),
36 => ParticleData::Vibration(VibrationParticle::read_from(buf)?),
37 => ParticleData::ItemSlime,
38 => ParticleData::ItemSnowball,
39 => ParticleData::LargeSmoke,
40 => ParticleData::Lava,
41 => ParticleData::Mycelium,
42 => ParticleData::Note,
43 => ParticleData::Poof,
44 => ParticleData::Portal,
45 => ParticleData::Rain,
46 => ParticleData::Smoke,
47 => ParticleData::Sneeze,
48 => ParticleData::Spit,
49 => ParticleData::SquidInk,
50 => ParticleData::SweepAttack,
51 => ParticleData::TotemOfUndying,
52 => ParticleData::Underwater,
53 => ParticleData::Splash,
54 => ParticleData::Witch,
55 => ParticleData::BubblePop,
56 => ParticleData::CurrentDown,
57 => ParticleData::BubbleColumnUp,
58 => ParticleData::Nautilus,
59 => ParticleData::Dolphin,
60 => ParticleData::CampfireCozySmoke,
61 => ParticleData::CampfireSignalSmoke,
62 => ParticleData::DrippingHoney,
63 => ParticleData::FallingHoney,
64 => ParticleData::LandingHoney,
65 => ParticleData::FallingNectar,
66 => ParticleData::FallingSporeBlossom,
67 => ParticleData::Ash,
68 => ParticleData::CrimsonSpore,
69 => ParticleData::WarpedSpore,
70 => ParticleData::SporeBlossomAir,
71 => ParticleData::DrippingObsidianTear,
72 => ParticleData::FallingObsidianTear,
73 => ParticleData::LandingObsidianTear,
74 => ParticleData::ReversePortal,
75 => ParticleData::WhiteAsh,
76 => ParticleData::SmallFlame,
77 => ParticleData::Snowflake,
78 => ParticleData::DrippingDripstoneLava,
79 => ParticleData::FallingDripstoneLava,
80 => ParticleData::DrippingDripstoneWater,
81 => ParticleData::FallingDripstoneWater,
82 => ParticleData::GlowSquidInk,
83 => ParticleData::Glow,
84 => ParticleData::WaxOn,
85 => ParticleData::WaxOff,
86 => ParticleData::ElectricSpark,
87 => ParticleData::Scrape,
_ => return Err(BufReadError::UnexpectedEnumVariant { id: id as i32 }),
})
}
}
impl McBufReadable for ParticleData {
fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
let id = u32::var_read_from(buf)?;
ParticleData::read_from_particle_id(buf, id)
}
}
impl McBufWritable for ParticleData {
fn write_into(&self, _buf: &mut impl Write) -> Result<(), std::io::Error> {
todo!()
}
}

View file

@ -3,8 +3,9 @@
use azalea_buf::{BufReadError, McBuf, McBufReadable, McBufWritable};
use std::io::{Cursor, Write};
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Default)]
pub enum Slot {
#[default]
Empty,
Present(SlotData),
}

View file

@ -29,6 +29,7 @@ pub fn hex_digest(digest: &[u8]) -> String {
// and libraries. It works by treating the sha1 output bytes as one large
// integer in two's complement and then printing the integer in base 16,
// placing a minus sign if the interpreted number is negative.
num_bigint::BigInt::from_signed_bytes_be(digest).to_str_radix(16)
}

View file

@ -3,8 +3,8 @@ description = "Translate Minecraft strings from their id."
edition = "2021"
license = "MIT"
name = "azalea-language"
version = "0.3.0"
repository = "https://github.com/mat-1/azalea/tree/main/azalea-language"
version = "0.3.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -12,4 +12,4 @@ repository = "https://github.com/mat-1/azalea/tree/main/azalea-language"
lazy_static = "1.4.0"
serde = "1.0.137"
serde_json = "1.0.81"
# tokio = {version = "1.19.2", features = ["fs"]}
# tokio = {version = "^1.21.2", features = ["fs"]}

View file

@ -1,3 +1,5 @@
//! Translate Minecraft strings from their id.
use lazy_static::lazy_static;
use std::io::Read;
use std::path::Path;

View file

@ -13,6 +13,7 @@ ahash = "^0.8.0"
azalea-buf = {path = "../azalea-buf", version = "^0.3.0" }
byteorder = "^1.4.3"
flate2 = "^1.0.23"
log = "0.4.17"
num-derive = "^0.3.3"
num-traits = "^0.2.14"

View file

@ -4,6 +4,7 @@ use ahash::AHashMap;
use azalea_buf::{BufReadError, McBufReadable};
use byteorder::{ReadBytesExt, BE};
use flate2::read::{GzDecoder, ZlibDecoder};
use log::warn;
use std::io::Cursor;
use std::io::{BufRead, Read};
@ -23,7 +24,14 @@ fn read_string(stream: &mut Cursor<&[u8]>) -> Result<String, Error> {
let length = stream.read_u16::<BE>()? as usize;
let buf = read_bytes(stream, length)?;
Ok(std::str::from_utf8(buf)?.to_string())
Ok(if let Ok(string) = std::str::from_utf8(buf) {
string.to_string()
} else {
let lossy_string = String::from_utf8_lossy(buf).into_owned();
warn!("Error decoding utf8 (bytes: {buf:?}, lossy: \"{lossy_string})\"");
lossy_string
})
}
impl Tag {

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

@ -3,17 +3,17 @@ description = "Physics for Minecraft entities."
edition = "2021"
license = "MIT"
name = "azalea-physics"
version = "0.3.0"
repository = "https://github.com/mat-1/azalea/tree/main/azalea-physics"
version = "0.3.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
azalea-block = { path = "../azalea-block", version = "^0.3.0" }
azalea-core = { path = "../azalea-core", version = "^0.3.0" }
azalea-world = { path = "../azalea-world", version = "^0.3.0" }
azalea-block = {path = "../azalea-block", version = "^0.3.0"}
azalea-core = {path = "../azalea-core", version = "^0.3.0"}
azalea-world = {path = "../azalea-world", version = "^0.3.0"}
lazy_static = "1.4.0"
parking_lot = "0.12.1"
parking_lot = "^0.12.1"
[dev-dependencies]
uuid = "^1.1.2"

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

@ -11,6 +11,7 @@ pub use blocks::BlockWithShape;
use dimension_collisions::CollisionGetter;
pub use discrete_voxel_shape::*;
pub use shape::*;
use std::ops::DerefMut;
pub enum MoverType {
Own,
@ -81,7 +82,7 @@ impl HasCollision for Dimension {
}
}
impl MovableEntity for Entity<'_> {
impl<D: DerefMut<Target = Dimension>> MovableEntity for Entity<'_, D> {
/// Move an entity by a given delta, checking for collisions.
fn move_colliding(
&mut self,

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

@ -2,9 +2,14 @@
pub mod collision;
use std::ops::DerefMut;
use azalea_block::{Block, BlockState};
use azalea_core::{BlockPos, Vec3};
use azalea_world::entity::{Entity, EntityData};
use azalea_world::{
entity::{Entity, EntityData},
Dimension,
};
use collision::{MovableEntity, MoverType};
pub trait HasPhysics {
@ -14,7 +19,7 @@ pub trait HasPhysics {
fn jump_from_ground(&mut self);
}
impl HasPhysics for Entity<'_> {
impl<D: DerefMut<Target = Dimension>> HasPhysics for Entity<'_, D> {
/// Move the entity with the given acceleration while handling friction,
/// gravity, collisions, and some other stuff.
fn travel(&mut self, acceleration: &Vec3) {
@ -115,17 +120,17 @@ impl HasPhysics for Entity<'_> {
y: jump_power,
z: old_delta_movement.z,
};
// if self.sprinting {
// let y_rot = self.y_rot * 0.017453292;
// self.delta = self.delta
// + Vec3 {
// x: (-f32::sin(y_rot) * 0.2) as f64,
// y: 0.,
// z: (f32::cos(y_rot) * 0.2) as f64,
// };
// }
if self.metadata.sprinting {
let y_rot = self.y_rot * 0.017453292;
self.delta = self.delta
+ Vec3 {
x: (-f32::sin(y_rot) * 0.2) as f64,
y: 0.,
z: (f32::cos(y_rot) * 0.2) as f64,
};
}
// self.has_impulse = true;
self.has_impulse = true;
}
}
@ -138,8 +143,8 @@ fn get_block_pos_below_that_affects_movement(entity: &EntityData) -> BlockPos {
)
}
fn handle_relative_friction_and_calculate_movement(
entity: &mut Entity,
fn handle_relative_friction_and_calculate_movement<D: DerefMut<Target = Dimension>>(
entity: &mut Entity<D>,
acceleration: &Vec3,
block_friction: f32,
) -> Vec3 {
@ -167,7 +172,7 @@ fn handle_relative_friction_and_calculate_movement(
fn get_friction_influenced_speed(entity: &EntityData, friction: f32) -> f32 {
// TODO: have speed & flying_speed fields in entity
if entity.on_ground {
let speed: f32 = 0.1;
let speed: f32 = entity.attributes.speed.calculate() as f32;
speed * (0.216f32 / (friction * friction * friction))
} else {
// entity.flying_speed
@ -177,7 +182,7 @@ fn get_friction_influenced_speed(entity: &EntityData, friction: f32) -> f32 {
/// Returns the what the entity's jump should be multiplied by based on the
/// block they're standing on.
fn block_jump_factor(entity: &Entity) -> f32 {
fn block_jump_factor<D: DerefMut<Target = Dimension>>(entity: &Entity<D>) -> f32 {
let block_at_pos = entity.dimension.get_block_state(&entity.pos().into());
let block_below = entity
.dimension
@ -205,11 +210,11 @@ fn block_jump_factor(entity: &Entity) -> f32 {
// public double getJumpBoostPower() {
// return this.hasEffect(MobEffects.JUMP) ? (double)(0.1F * (float)(this.getEffect(MobEffects.JUMP).getAmplifier() + 1)) : 0.0D;
// }
fn jump_power(entity: &Entity) -> f32 {
fn jump_power<D: DerefMut<Target = Dimension>>(entity: &Entity<D>) -> f32 {
0.42 * block_jump_factor(entity)
}
fn jump_boost_power(_entity: &Entity) -> f64 {
fn jump_boost_power<D: DerefMut<Target = Dimension>>(_entity: &Entity<D>) -> f64 {
// TODO: potion effects
// if let Some(effects) = entity.effects() {
// if let Some(jump_effect) = effects.get(&Effect::Jump) {

View file

@ -3,25 +3,25 @@ description = "Send and receive Minecraft packets."
edition = "2021"
license = "MIT"
name = "azalea-protocol"
version = "0.3.0"
repository = "https://github.com/mat-1/azalea/tree/main/azalea-protocol"
version = "0.3.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
async-compression = {version = "^0.3.8", features = ["tokio", "zlib"], optional = true}
async-recursion = "1.0.0"
azalea-auth = {path = "../azalea-auth", version = "^0.3.0" }
azalea-block = {path = "../azalea-block", default-features = false, version = "^0.3.0" }
azalea-brigadier = {path = "../azalea-brigadier", version = "^0.3.0" }
azalea-buf = {path = "../azalea-buf", version = "^0.3.0" }
azalea-chat = {path = "../azalea-chat", version = "^0.3.0" }
azalea-core = {path = "../azalea-core", optional = true, version = "^0.3.0" }
azalea-crypto = {path = "../azalea-crypto", version = "^0.3.0" }
azalea-nbt = {path = "../azalea-nbt", version = "^0.3.0" }
azalea-protocol-macros = {path = "./azalea-protocol-macros", version = "^0.3.0" }
azalea-registry = {path = "../azalea-registry", version = "^0.3.0" }
azalea-world = {path = "../azalea-world", version = "^0.3.0" }
azalea-auth = {path = "../azalea-auth", version = "^0.3.0"}
azalea-block = {path = "../azalea-block", default-features = false, version = "^0.3.0"}
azalea-brigadier = {path = "../azalea-brigadier", version = "^0.3.0"}
azalea-buf = {path = "../azalea-buf", version = "^0.3.0"}
azalea-chat = {path = "../azalea-chat", version = "^0.3.0"}
azalea-core = {path = "../azalea-core", optional = true, version = "^0.3.0"}
azalea-crypto = {path = "../azalea-crypto", version = "^0.3.0"}
azalea-nbt = {path = "../azalea-nbt", version = "^0.3.0"}
azalea-protocol-macros = {path = "./azalea-protocol-macros", version = "^0.3.0"}
azalea-registry = {path = "../azalea-registry", version = "^0.3.0"}
azalea-world = {path = "../azalea-world", version = "^0.3.0"}
byteorder = "^1.4.3"
bytes = "^1.1.0"
flate2 = "1.0.23"
@ -31,9 +31,9 @@ log = "0.4.17"
serde = {version = "1.0.130", features = ["serde_derive"]}
serde_json = "^1.0.72"
thiserror = "^1.0.34"
tokio = {version = "^1.19.2", features = ["io-util", "net", "macros"]}
tokio = {version = "^1.21.2", features = ["io-util", "net", "macros"]}
tokio-util = {version = "0.7.4", features = ["codec"]}
trust-dns-resolver = "^0.20.3"
trust-dns-resolver = {version = "^0.22.0", default-features = false, features = ["tokio-runtime"]}
uuid = "1.1.2"
[features]

View file

@ -1,4 +1,4 @@
//! Create connections that communicate with a remote server or client.
//! Connect to remote servers/clients.
use crate::packets::game::{ClientboundGamePacket, ServerboundGamePacket};
use crate::packets::handshake::{ClientboundHandshakePacket, ServerboundHandshakePacket};
@ -11,6 +11,7 @@ use crate::write::write_packet;
use azalea_auth::sessionserver::SessionServerError;
use azalea_crypto::{Aes128CfbDec, Aes128CfbEnc};
use bytes::BytesMut;
use log::{error, info};
use std::fmt::Debug;
use std::marker::PhantomData;
use std::net::SocketAddr;
@ -22,18 +23,18 @@ use uuid::Uuid;
/// The read half of a connection.
pub struct ReadConnection<R: ProtocolPacket> {
read_stream: OwnedReadHalf,
buffer: BytesMut,
compression_threshold: Option<u32>,
dec_cipher: Option<Aes128CfbDec>,
pub read_stream: OwnedReadHalf,
pub buffer: BytesMut,
pub compression_threshold: Option<u32>,
pub dec_cipher: Option<Aes128CfbDec>,
_reading: PhantomData<R>,
}
/// The write half of a connection.
pub struct WriteConnection<W: ProtocolPacket> {
write_stream: OwnedWriteHalf,
compression_threshold: Option<u32>,
enc_cipher: Option<Aes128CfbEnc>,
pub write_stream: OwnedWriteHalf,
pub compression_threshold: Option<u32>,
pub enc_cipher: Option<Aes128CfbEnc>,
_writing: PhantomData<W>,
}
@ -44,26 +45,28 @@ pub struct WriteConnection<W: ProtocolPacket> {
/// Join an offline-mode server and go through the handshake.
/// ```rust,no_run
/// #[tokio::main]
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let resolved_address = resolver::resolve_address(address).await?;
/// async fn main() -> anyhow::Result<()> {
/// let resolved_address = resolver::resolve_address(&"localhost".try_into().unwrap()).await?;
/// let mut conn = Connection::new(&resolved_address).await?;
///
/// // handshake
/// conn.write(
/// ClientIntentionPacket {
/// protocol_version: PROTOCOL_VERSION,
/// hostname: address.host.to_string(),
/// port: address.port,
/// intention: ConnectionProtocol::Login,
/// }.get());
///
/// protocol_version: PROTOCOL_VERSION,
/// hostname: resolved_address.ip().to_string(),
/// port: resolved_address.port(),
/// intention: ConnectionProtocol::Login,
/// }
/// .get(),
/// )
/// .await?;
///
/// let mut conn = conn.login();
///
/// // login
/// conn.write(
/// ServerboundHelloPacket {
/// username,
/// username: "bot".to_string(),
/// public_key: None,
/// profile_id: None,
/// }
@ -72,8 +75,8 @@ pub struct WriteConnection<W: ProtocolPacket> {
/// .await?;
///
/// let (conn, game_profile) = loop {
/// let packet_result = conn.read().await?;
/// Ok(packet) => match packet {
/// let packet = conn.read().await?;
/// match packet {
/// ClientboundLoginPacket::Hello(p) => {
/// let e = azalea_crypto::encrypt(&p.public_key, &p.nonce).unwrap();
///
@ -95,16 +98,14 @@ pub struct WriteConnection<W: ProtocolPacket> {
/// }
/// ClientboundLoginPacket::LoginDisconnect(p) => {
/// println!("login disconnect: {}", p.reason);
/// bail!(JoinError::Disconnected(p.reason));
/// bail!("{}", p.reason);
/// }
/// ClientboundLoginPacket::CustomQuery(p) => {}
/// },
/// Err(e) => {
/// eprintln!("Error: {:?}", e);
/// bail!("Error: {:?}", e);
/// }
/// }
/// };
/// };
///
/// Ok(())
/// }
/// ```
pub struct Connection<R: ProtocolPacket, W: ProtocolPacket> {
pub reader: ReadConnection<R>,
@ -115,6 +116,7 @@ impl<R> ReadConnection<R>
where
R: ProtocolPacket + Debug,
{
/// Read a packet from the stream.
pub async fn read(&mut self) -> Result<R, ReadPacketError> {
read_packet::<R, _>(
&mut self.read_stream,
@ -131,13 +133,24 @@ where
{
/// Write a packet to the server.
pub async fn write(&mut self, packet: W) -> std::io::Result<()> {
write_packet(
if let Err(e) = write_packet(
&packet,
&mut self.write_stream,
self.compression_threshold,
&mut self.enc_cipher,
)
.await
{
// detect broken pipe
if e.kind() == std::io::ErrorKind::BrokenPipe {
info!("Broken pipe, shutting down connection.");
if let Err(e) = self.shutdown().await {
error!("Couldn't shut down: {}", e);
}
}
return Err(e);
}
Ok(())
}
/// End the connection.

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

@ -6,6 +6,8 @@
//!
//! [`azalea`]: https://crates.io/crates/azalea
//! [`azalea_client`]: https://crates.io/crates/azalea-client
//!
//! See [`crate::connect::Connection`] for an example.
// these two are necessary for thiserror backtraces
#![feature(error_generic_member_access)]
@ -56,27 +58,6 @@ 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(feature = "connecting")]
pub async fn connect(address: ServerAddress) -> Result<(), Box<dyn std::error::Error>> {
use log::debug;
let resolved_address = resolver::resolve_address(&address).await;
debug!("Resolved address: {:?}", resolved_address);
Ok(())
}
#[cfg(test)]
mod tests {
use std::io::Cursor;

View file

@ -1,7 +1,7 @@
use azalea_buf::McBuf;
use azalea_core::Vec3;
use azalea_protocol_macros::ClientboundGamePacket;
use azalea_world::entity::EntityData;
use azalea_world::entity::{EntityData, EntityMetadata};
use uuid::Uuid;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
@ -33,6 +33,8 @@ impl From<&ClientboundAddEntityPacket> for EntityData {
y: p.y,
z: p.z,
},
// default metadata for the entity type
EntityMetadata::from(p.entity_type),
)
}
}

View file

@ -1,7 +1,7 @@
use azalea_buf::McBuf;
use azalea_core::Vec3;
use azalea_protocol_macros::ClientboundGamePacket;
use azalea_world::entity::EntityData;
use azalea_world::entity::{metadata, EntityData, EntityMetadata};
use uuid::Uuid;
/// This packet is sent by the server when a player comes into visible range, not when a player joins.
@ -26,6 +26,7 @@ impl From<&ClientboundAddPlayerPacket> for EntityData {
y: p.y,
z: p.z,
},
EntityMetadata::Player(metadata::Player::default()),
)
}
}

View file

@ -32,7 +32,7 @@ impl McBufReadable for ClientboundLevelParticlesPacket {
let max_speed = f32::read_from(buf)?;
let count = u32::read_from(buf)?;
let data = ParticleData::read_from_particle_id(buf, particle_id)?;
let data = ParticleData::read_from_id(buf, particle_id)?;
Ok(Self {
particle_id,

View file

@ -13,11 +13,11 @@ pub struct ClientboundLightUpdatePacket {
#[derive(Clone, Debug, McBuf)]
pub struct ClientboundLightUpdatePacketData {
trust_edges: bool,
sky_y_mask: BitSet,
block_y_mask: BitSet,
empty_sky_y_mask: BitSet,
empty_block_y_mask: BitSet,
sky_updates: Vec<Vec<u8>>,
block_updates: Vec<Vec<u8>>,
pub trust_edges: bool,
pub sky_y_mask: BitSet,
pub block_y_mask: BitSet,
pub empty_sky_y_mask: BitSet,
pub empty_block_y_mask: BitSet,
pub sky_updates: Vec<Vec<u8>>,
pub block_updates: Vec<Vec<u8>>,
}

View file

@ -1,10 +1,10 @@
use azalea_buf::McBuf;
use azalea_protocol_macros::ClientboundGamePacket;
use azalea_world::entity::EntityMetadata;
use azalea_world::entity::EntityMetadataItems;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
pub struct ClientboundSetEntityDataPacket {
#[var]
pub id: u32,
pub packed_items: EntityMetadata,
pub packed_items: EntityMetadataItems,
}

View file

@ -1,9 +1,7 @@
use azalea_buf::{BufReadError, McBuf};
use azalea_buf::{McBufReadable, McBufWritable};
use azalea_buf::McBuf;
use azalea_core::ResourceLocation;
use azalea_protocol_macros::ClientboundGamePacket;
use std::io::{Cursor, Write};
use uuid::Uuid;
use azalea_world::entity::attributes::AttributeModifier;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
pub struct ClientboundUpdateAttributesPacket {
@ -16,37 +14,5 @@ pub struct ClientboundUpdateAttributesPacket {
pub struct AttributeSnapshot {
pub attribute: ResourceLocation,
pub base: f64,
pub modifiers: Vec<Modifier>,
}
#[derive(Clone, Debug, McBuf)]
pub struct Modifier {
pub uuid: Uuid,
pub amount: f64,
pub operation: u8,
}
#[derive(Clone, Debug, Copy)]
enum Operation {
Addition = 0,
MultiplyBase = 1,
MultiplyTotal = 2,
}
impl McBufReadable for Operation {
fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
match u8::read_from(buf)? {
0 => Ok(Operation::Addition),
1 => Ok(Operation::MultiplyBase),
2 => Ok(Operation::MultiplyTotal),
id => Err(BufReadError::UnexpectedEnumVariant { id: id.into() }),
}
}
}
impl McBufWritable for Operation {
fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
(*self as u8).write_into(buf)?;
Ok(())
}
pub modifiers: Vec<AttributeModifier>,
}

View file

@ -1,3 +1,5 @@
//! Read packets from a stream.
use crate::packets::ProtocolPacket;
use azalea_buf::BufReadError;
use azalea_buf::McBufVarReadable;
@ -157,6 +159,8 @@ pub enum DecompressionError {
AboveCompressionThreshold { size: u32, maximum: u32 },
}
/// Get the decompressed bytes from a packet. It must have been decrypted
/// first.
fn compression_decoder(
stream: &mut Cursor<&[u8]>,
compression_threshold: u32,
@ -192,6 +196,12 @@ fn compression_decoder(
Ok(decoded_buf)
}
/// Read a single packet from a stream.
///
/// The buffer is required because servers may send multiple packets in the
/// same frame, so we need to store the packet data that's left to read.
///
/// The current protocol state must be passed as a generic.
pub async fn read_packet<'a, P: ProtocolPacket + Debug, R>(
stream: &'a mut R,
buffer: &mut BytesMut,

View file

@ -1,3 +1,5 @@
//! Resolve IPs from hostnames.
use crate::ServerAddress;
use async_recursion::async_recursion;
use std::net::{IpAddr, SocketAddr};

View file

@ -1,3 +1,5 @@
//! Write packets to a stream.
use crate::{packets::ProtocolPacket, read::MAXIMUM_UNCOMPRESSED_LENGTH};
use async_compression::tokio::bufread::ZlibEncoder;
use azalea_buf::McBufVarWritable;
@ -6,10 +8,11 @@ use std::fmt::Debug;
use thiserror::Error;
use tokio::io::{AsyncReadExt, AsyncWrite, AsyncWriteExt};
fn frame_prepender(data: &mut Vec<u8>) -> Result<Vec<u8>, std::io::Error> {
/// Prepend the length of the packet to it.
fn frame_prepender(mut data: Vec<u8>) -> Result<Vec<u8>, std::io::Error> {
let mut buf = Vec::new();
(data.len() as u32).var_write_into(&mut buf)?;
buf.append(data);
buf.append(&mut data);
Ok(buf)
}
@ -82,7 +85,7 @@ where
if let Some(threshold) = compression_threshold {
buf = compression_encoder(&buf, threshold).await.unwrap();
}
buf = frame_prepender(&mut buf).unwrap();
buf = frame_prepender(buf).unwrap();
// if we were given a cipher, encrypt the packet
if let Some(cipher) = cipher {
azalea_crypto::encrypt_packet(cipher, &mut buf);

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

@ -9,15 +9,16 @@ version = "0.3.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
azalea-block = {path = "../azalea-block", default-features = false, version = "^0.3.0" }
azalea-buf = {path = "../azalea-buf", version = "^0.3.0" }
azalea-chat = {path = "../azalea-chat", version = "^0.3.0" }
azalea-core = {path = "../azalea-core", version = "^0.3.0" }
azalea-nbt = {path = "../azalea-nbt", version = "^0.3.0" }
azalea-registry = {path = "../azalea-registry", version = "^0.3.0" }
azalea-block = {path = "../azalea-block", default-features = false, version = "^0.3.0"}
azalea-buf = {path = "../azalea-buf", version = "^0.3.0"}
azalea-chat = {path = "../azalea-chat", version = "^0.3.0"}
azalea-core = {path = "../azalea-core", version = "^0.3.0"}
azalea-nbt = {path = "../azalea-nbt", version = "^0.3.0"}
azalea-registry = {path = "../azalea-registry", version = "^0.3.0"}
enum-as-inner = "0.5.1"
log = "0.4.17"
nohash-hasher = "0.2.0"
parking_lot = "0.12.1"
parking_lot = "^0.12.1"
thiserror = "1.0.34"
uuid = "1.1.2"

View file

@ -0,0 +1,116 @@
//! https://minecraft.fandom.com/wiki/Attribute
use std::{
collections::HashMap,
io::{Cursor, Write},
};
use azalea_buf::{BufReadError, McBuf, McBufReadable, McBufWritable};
use thiserror::Error;
use uuid::{uuid, Uuid};
#[derive(Clone, Debug)]
pub struct AttributeModifiers {
pub speed: AttributeInstance,
}
#[derive(Clone, Debug)]
pub struct AttributeInstance {
pub base: f64,
modifiers_by_uuid: HashMap<Uuid, AttributeModifier>,
}
#[derive(Clone, Debug, Error)]
#[error("A modifier with this UUID is already present.")]
pub struct AlreadyPresentError;
impl AttributeInstance {
pub fn new(base: f64) -> Self {
Self {
base,
modifiers_by_uuid: HashMap::new(),
}
}
pub fn calculate(&self) -> f64 {
let mut total = self.base;
for modifier in self.modifiers_by_uuid.values() {
match modifier.operation {
AttributeModifierOperation::Addition => total += modifier.amount,
AttributeModifierOperation::MultiplyBase => total += self.base * modifier.amount,
_ => {}
}
match modifier.operation {
AttributeModifierOperation::MultiplyTotal => total *= 1.0 + modifier.amount,
_ => {}
}
}
total
}
/// Add a new modifier to this attribute.
pub fn insert(&mut self, modifier: AttributeModifier) -> Result<(), AlreadyPresentError> {
if self
.modifiers_by_uuid
.insert(modifier.uuid, modifier)
.is_some()
{
Err(AlreadyPresentError)
} else {
Ok(())
}
}
/// Remove the modifier with the given UUID from this attribute, returning
/// the previous modifier is present.
pub fn remove(&mut self, uuid: &Uuid) -> Option<AttributeModifier> {
self.modifiers_by_uuid.remove(uuid)
}
}
#[derive(Clone, Debug)]
pub struct AttributeModifier {
pub uuid: Uuid,
pub name: String,
pub amount: f64,
pub operation: AttributeModifierOperation,
}
#[derive(Clone, Debug, Copy, McBuf)]
pub enum AttributeModifierOperation {
Addition,
MultiplyBase,
MultiplyTotal,
}
pub fn sprinting_modifier() -> AttributeModifier {
AttributeModifier {
uuid: uuid!("662A6B8D-DA3E-4C1C-8813-96EA6097278D"),
name: "Sprinting speed boost".to_string(),
amount: 0.30000001192092896,
operation: AttributeModifierOperation::MultiplyTotal,
}
}
impl McBufReadable for AttributeModifier {
fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
let uuid = Uuid::read_from(buf)?;
let amount = f64::read_from(buf)?;
let operation = AttributeModifierOperation::read_from(buf)?;
Ok(Self {
uuid,
name: "Unknown synced attribute modifier".to_string(),
amount,
operation,
})
}
}
impl McBufWritable for AttributeModifier {
fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
self.uuid.write_into(buf)?;
self.amount.write_into(buf)?;
self.operation.write_into(buf)?;
Ok(())
}
}

View file

@ -1,12 +1,16 @@
use azalea_block::BlockState;
use azalea_buf::{BufReadError, McBufVarReadable};
use azalea_buf::{McBuf, McBufReadable, McBufWritable};
use azalea_chat::Component;
use azalea_core::{BlockPos, Direction, GlobalPos, Particle, Slot};
use enum_as_inner::EnumAsInner;
use log::warn;
use nohash_hasher::IntSet;
use std::io::{Cursor, Write};
use uuid::Uuid;
#[derive(Clone, Debug)]
pub struct EntityMetadata(Vec<EntityDataItem>);
pub struct EntityMetadataItems(pub Vec<EntityDataItem>);
#[derive(Clone, Debug)]
pub struct EntityDataItem {
@ -16,7 +20,7 @@ pub struct EntityDataItem {
pub value: EntityDataValue,
}
impl McBufReadable for EntityMetadata {
impl McBufReadable for EntityMetadataItems {
fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
let mut metadata = Vec::new();
loop {
@ -27,11 +31,11 @@ impl McBufReadable for EntityMetadata {
let value = EntityDataValue::read_from(buf)?;
metadata.push(EntityDataItem { index, value });
}
Ok(EntityMetadata(metadata))
Ok(EntityMetadataItems(metadata))
}
}
impl McBufWritable for EntityMetadata {
impl McBufWritable for EntityMetadataItems {
fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
for item in &self.0 {
item.index.write_into(buf)?;
@ -42,10 +46,9 @@ impl McBufWritable for EntityMetadata {
}
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, EnumAsInner)]
pub enum EntityDataValue {
Byte(u8),
// varint
Int(i32),
Float(f32),
String(String),
@ -53,14 +56,14 @@ pub enum EntityDataValue {
OptionalComponent(Option<Component>),
ItemStack(Slot),
Boolean(bool),
Rotations { x: f32, y: f32, z: f32 },
Rotations(Rotations),
BlockPos(BlockPos),
OptionalBlockPos(Option<BlockPos>),
Direction(Direction),
OptionalUuid(Option<Uuid>),
// 0 for absent (implies air); otherwise, a block state ID as per the global palette
// this is a varint
OptionalBlockState(Option<i32>),
OptionalBlockState(Option<BlockState>),
CompoundTag(azalea_nbt::Tag),
Particle(Particle),
VillagerData(VillagerData),
@ -73,6 +76,13 @@ pub enum EntityDataValue {
PaintingVariant(azalea_registry::PaintingVariant),
}
#[derive(Clone, Debug, McBuf, Default)]
pub struct Rotations {
pub x: f32,
pub y: f32,
pub z: f32,
}
impl McBufReadable for EntityDataValue {
fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
let data_type = u32::var_read_from(buf)?;
@ -85,21 +95,20 @@ impl McBufReadable for EntityDataValue {
5 => EntityDataValue::OptionalComponent(Option::<Component>::read_from(buf)?),
6 => EntityDataValue::ItemStack(Slot::read_from(buf)?),
7 => EntityDataValue::Boolean(bool::read_from(buf)?),
8 => EntityDataValue::Rotations {
x: f32::read_from(buf)?,
y: f32::read_from(buf)?,
z: f32::read_from(buf)?,
},
8 => EntityDataValue::Rotations(Rotations::read_from(buf)?),
9 => EntityDataValue::BlockPos(BlockPos::read_from(buf)?),
10 => EntityDataValue::OptionalBlockPos(Option::<BlockPos>::read_from(buf)?),
11 => EntityDataValue::Direction(Direction::read_from(buf)?),
12 => EntityDataValue::OptionalUuid(Option::<Uuid>::read_from(buf)?),
13 => EntityDataValue::OptionalBlockState({
let val = i32::var_read_from(buf)?;
let val = u32::var_read_from(buf)?;
if val == 0 {
None
} else {
Some(val)
Some(BlockState::try_from(val - 1).unwrap_or_else(|_| {
warn!("Invalid block state ID {} in entity metadata", val - 1);
BlockState::Air
}))
}
}),
14 => EntityDataValue::CompoundTag(azalea_nbt::Tag::read_from(buf)?),
@ -135,19 +144,20 @@ impl McBufWritable for EntityDataValue {
}
}
#[derive(Clone, Debug, Copy, McBuf)]
#[derive(Clone, Debug, Copy, McBuf, Default)]
pub enum Pose {
#[default]
Standing = 0,
FallFlying = 1,
Sleeping = 2,
Swimming = 3,
SpinAttack = 4,
Sneaking = 5,
LongJumping = 6,
Dying = 7,
FallFlying,
Sleeping,
Swimming,
SpinAttack,
Sneaking,
LongJumping,
Dying,
}
#[derive(Debug, Clone, McBuf)]
#[derive(Debug, Clone, McBuf, Default)]
pub struct VillagerData {
#[var]
type_: u32,
@ -156,3 +166,32 @@ pub struct VillagerData {
#[var]
level: u32,
}
impl TryFrom<EntityMetadataItems> for Vec<EntityDataValue> {
type Error = String;
fn try_from(data: EntityMetadataItems) -> Result<Self, Self::Error> {
let mut data = data.0;
data.sort_by(|a, b| a.index.cmp(&b.index));
let mut prev_indexes = IntSet::default();
let len = data.len();
// check to make sure it's valid, in vanilla this is guaranteed to pass
// but it's possible there's mods that mess with it so we want to make
// sure it's good
for item in &data {
if prev_indexes.contains(&item.index) {
return Err(format!("Index {} is duplicated", item.index));
}
if item.index as usize > len {
return Err(format!("Index {} is too big", item.index));
}
prev_indexes.insert(item.index);
}
let data = data.into_iter().map(|d| d.value).collect();
Ok(data)
}
}

File diff suppressed because it is too large Load diff

44
azalea-world/src/entity/mod.rs Executable file → Normal file
View file

@ -1,6 +1,10 @@
pub mod attributes;
mod data;
mod dimensions;
pub mod metadata;
use self::attributes::{AttributeInstance, AttributeModifiers};
pub use self::metadata::EntityMetadata;
use crate::Dimension;
use azalea_block::BlockState;
use azalea_core::{BlockPos, Vec3, AABB};
@ -76,6 +80,18 @@ impl<'d, D: DerefMut<Target = Dimension>> Entity<'d, D> {
z: acceleration.z * (x_rot as f64) + acceleration.x * (y_rot as f64),
}
}
/// Apply the given metadata items to the entity. Everything that isn't
/// included in items will be left unchanged. If an error occured, None
/// will be returned.
///
/// TODO: this should be changed to have a proper error.
pub fn apply_metadata(&mut self, items: &Vec<EntityDataItem>) -> Option<()> {
for item in items {
self.metadata.set_index(item.index, item.value.clone())?;
}
Some(())
}
}
impl<'d, D: Deref<Target = Dimension>> Entity<'d, D> {
@ -195,10 +211,18 @@ pub struct EntityData {
/// Whether the entity will try to jump every tick
/// (equivalent to the space key being held down in vanilla).
pub jumping: bool,
pub has_impulse: bool,
/// Stores some extra data about the entity, including the entity type.
pub metadata: EntityMetadata,
/// The attributes and modifiers that the entity has (for example, speed).
pub attributes: AttributeModifiers,
}
impl EntityData {
pub fn new(uuid: Uuid, pos: Vec3) -> Self {
pub fn new(uuid: Uuid, pos: Vec3, metadata: EntityMetadata) -> Self {
let dimensions = EntityDimensions {
width: 0.6,
height: 1.8,
@ -227,7 +251,16 @@ impl EntityData {
bounding_box: dimensions.make_bounding_box(&pos),
dimensions,
has_impulse: false,
jumping: false,
metadata,
attributes: AttributeModifiers {
// TODO: do the correct defaults for everything, some entities have different defaults
speed: AttributeInstance::new(0.1),
},
}
}
@ -255,7 +288,14 @@ mod tests {
fn from_mut_entity_to_ref_entity() {
let mut dim = Dimension::default();
let uuid = Uuid::from_u128(100);
dim.add_entity(0, EntityData::new(uuid, Vec3::default()));
dim.add_entity(
0,
EntityData::new(
uuid,
Vec3::default(),
EntityMetadata::Player(metadata::Player::default()),
),
);
let entity: Entity = dim.entity_mut(0).unwrap();
let entity_ref = Entity::from(entity);
assert_eq!(entity_ref.uuid, uuid);

View file

@ -151,6 +151,8 @@ impl Default for EntityStorage {
#[cfg(test)]
mod tests {
use crate::entity::{metadata, EntityMetadata};
use super::*;
use azalea_core::Vec3;
@ -160,7 +162,14 @@ mod tests {
assert!(storage.get_by_id(0).is_none());
let uuid = Uuid::from_u128(100);
storage.insert(0, EntityData::new(uuid, Vec3::default()));
storage.insert(
0,
EntityData::new(
uuid,
Vec3::default(),
EntityMetadata::Player(metadata::Player::default()),
),
);
assert_eq!(storage.get_by_id(0).unwrap().uuid, uuid);
storage.remove_by_id(0);

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

@ -11,15 +11,19 @@ version = "0.3.0"
[dependencies]
anyhow = "^1.0.65"
async-trait = "^0.1.57"
azalea-block = {version = "0.3.0", path = "../azalea-block"}
azalea-client = {version = "0.3.0", path = "../azalea-client"}
azalea-core = {version = "0.3.0", path = "../azalea-core"}
azalea-physics = {version = "0.3.0", path = "../azalea-physics"}
azalea-protocol = {version = "0.3.0", path = "../azalea-protocol"}
parking_lot = {version = "0.12.1", features = ["deadlock_detection"]}
azalea-world = {version = "0.3.0", path = "../azalea-world"}
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.1"
futures = "0.3.25"
tokio = "^1.21.2"
[dev-dependencies]
anyhow = "^1.0.65"
env_logger = "^0.9.1"
tokio = "^1.21.1"
tokio = "^1.21.2"

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

@ -1,5 +1,5 @@
use azalea::prelude::*;
use azalea::Swarm;
use azalea::{Account, Accounts, Client, Event, Swarm};
use parking_lot::Mutex;
use std::sync::Arc;

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

@ -15,21 +15,14 @@ pub struct State {
}
pub trait BotTrait {
fn jump(&self);
fn jump(&mut self);
fn look_at(&mut self, pos: &Vec3);
}
impl BotTrait for azalea_client::Client {
/// Queue a jump for the next tick.
fn jump(&self) {
{
let player_entity_id = self.player.read().entity_id;
let mut dimension_lock = self.dimension.write();
let mut player_entity = dimension_lock
.entity_mut(player_entity_id)
.expect("Player must exist");
player_entity.jumping = true;
}
fn jump(&mut self) {
self.set_jumping(true);
let state = self.plugins.get::<Plugin>().unwrap().state.clone();
*state.jumping_once.lock() = true;
}
@ -48,8 +41,7 @@ impl crate::Plugin for Plugin {
if *self.state.jumping_once.lock() {
if bot.jumping() {
*self.state.jumping_once.lock() = false;
} else {
bot.set_jumping(true);
bot.set_jumping(false);
}
}
}

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

@ -10,12 +10,29 @@
//! First, install Rust nightly with `rustup install nightly` and `rustup
//! default nightly`.
//!
//! Then, add one of the following lines to your Cargo.toml.\
//! Then, add one of the following lines to your Cargo.toml:
//!
//! Latest bleeding-edge version:
//! `azalea = { git="https://github.com/mat-1/Cargo.toml" }`
//! `azalea = { git="https://github.com/mat-1/Cargo.toml" }`\
//! Latest "stable" release:
//! `azalea = "0.3"`
//!
//! ## Optimization
//!
//! For faster compile times, make a `.cargo/config.toml` file in your project
//! and copy
//! [this file](https://github.com/mat-1/azalea/blob/main/.cargo/config.toml)
//! into it.
//!
//! For faster performance in debug mode, add
//! ```toml
//! [profile.dev]
//! opt-level = 1
//! [profile.dev.package."*""]
//! opt-level = 3
//! ```
//! to your Cargo.toml. You may have to install the LLD linker.
//!
//! # Examples
//!
//! ```rust,no_run
@ -59,6 +76,7 @@
//! [`azalea_client`]: https://crates.io/crates/azalea-client
mod bot;
pub mod pathfinder;
pub mod prelude;
mod start;
mod swarm;

View file

@ -0,0 +1,208 @@
mod moves;
mod mtdstarlite;
use crate::{prelude::*, SprintDirection, WalkDirection};
use crate::{Client, Event};
use async_trait::async_trait;
use azalea_core::{BlockPos, CardinalDirection};
use azalea_world::entity::EntityData;
use mtdstarlite::Edge;
pub use mtdstarlite::MTDStarLite;
use parking_lot::Mutex;
use std::collections::VecDeque;
use std::sync::Arc;
#[derive(Default, Clone)]
pub struct Plugin {
pub state: State,
}
#[derive(Default, Clone)]
pub struct State {
// pathfinder: Option<MTDStarLite<Node, f32>>,
pub path: Arc<Mutex<VecDeque<Node>>>,
}
#[async_trait]
impl crate::Plugin for Plugin {
async fn handle(self: Box<Self>, event: Event, mut bot: Client) {
if let Event::Tick = event {
let mut path = self.state.path.lock();
if !path.is_empty() {
tick_execute_path(&mut bot, &mut path);
}
}
}
}
pub trait Trait {
fn goto(&self, goal: impl Goal);
}
impl Trait for azalea_client::Client {
fn goto(&self, goal: impl Goal) {
let start = Node {
pos: BlockPos::from(self.entity().pos()),
vertical_vel: VerticalVel::None,
};
let end = goal.goal_node();
println!("start: {:?}, end: {:?}", start, end);
let possible_moves: Vec<&dyn moves::Move> = vec![
&moves::ForwardMove(CardinalDirection::North),
&moves::ForwardMove(CardinalDirection::East),
&moves::ForwardMove(CardinalDirection::South),
&moves::ForwardMove(CardinalDirection::West),
//
&moves::AscendMove(CardinalDirection::North),
&moves::AscendMove(CardinalDirection::East),
&moves::AscendMove(CardinalDirection::South),
&moves::AscendMove(CardinalDirection::West),
//
&moves::DescendMove(CardinalDirection::North),
&moves::DescendMove(CardinalDirection::East),
&moves::DescendMove(CardinalDirection::South),
&moves::DescendMove(CardinalDirection::West),
//
&moves::DiagonalMove(CardinalDirection::North),
&moves::DiagonalMove(CardinalDirection::East),
&moves::DiagonalMove(CardinalDirection::South),
&moves::DiagonalMove(CardinalDirection::West),
];
let successors = |node: &Node| {
let mut edges = Vec::new();
let dimension = self.dimension.read();
for possible_move in possible_moves.iter() {
edges.push(Edge {
target: possible_move.next_node(&node),
cost: possible_move.cost(&dimension, node),
});
}
edges
};
let mut pf = MTDStarLite::new(
start,
end,
|n| goal.heuristic(n),
successors,
successors,
|n| goal.success(n),
);
let start = std::time::Instant::now();
let p = pf.find_path();
let end = std::time::Instant::now();
println!("path: {:?}", p);
println!("time: {:?}", end - start);
let state = self
.plugins
.get::<Plugin>()
.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();
}
}
fn tick_execute_path(bot: &mut Client, path: &mut VecDeque<Node>) {
let target = if let Some(target) = path.front() {
target
} else {
return;
};
let center = target.pos.center();
// println!("going to {center:?} (at {pos:?})", pos = bot.entity().pos());
bot.look_at(&center);
bot.sprint(SprintDirection::Forward);
// check if we should jump
if target.pos.y > bot.entity().pos().y.floor() as i32 {
bot.jump();
}
if target.is_reached(&bot.entity()) {
println!("ok target {target:?} reached");
path.pop_front();
if path.is_empty() {
bot.walk(WalkDirection::None);
}
// tick again, maybe we already reached the next node!
tick_execute_path(bot, path);
}
}
/// Information about our vertical velocity
#[derive(Eq, PartialEq, Hash, Clone, Copy, Debug)]
pub enum VerticalVel {
None,
/// No vertical velocity, but we're not on the ground
NoneMidair,
// less than 3 blocks (no fall damage)
FallingLittle,
}
#[derive(Eq, PartialEq, Hash, Clone, Copy, Debug)]
pub struct Node {
pub pos: BlockPos,
pub vertical_vel: VerticalVel,
}
pub trait Goal {
fn heuristic(&self, n: &Node) -> f32;
fn success(&self, n: &Node) -> bool;
// TODO: this should be removed and mtdstarlite should stop depending on
// being given a goal node
fn goal_node(&self) -> Node;
}
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
);
BlockPos::from(entity.pos()) == self.pos
&& match self.vertical_vel {
VerticalVel::NoneMidair => entity.delta.y > -0.1 && entity.delta.y < 0.1,
VerticalVel::None => entity.on_ground,
VerticalVel::FallingLittle => entity.delta.y < -0.1,
}
}
}
pub struct BlockPosGoal {
pub pos: BlockPos,
}
impl Goal for BlockPosGoal {
fn heuristic(&self, n: &Node) -> f32 {
let dx = (self.pos.x - n.pos.x) as f32;
let dy = (self.pos.y - n.pos.y) as f32;
let dz = (self.pos.z - n.pos.z) as f32;
dx * dx + dy * dy + dz * dz
}
fn success(&self, n: &Node) -> bool {
n.pos == self.pos
}
fn goal_node(&self) -> Node {
Node {
pos: self.pos,
vertical_vel: VerticalVel::None,
}
}
}
impl From<BlockPos> for BlockPosGoal {
fn from(pos: BlockPos) -> Self {
Self { pos }
}
}

View file

@ -0,0 +1,191 @@
use super::{Node, VerticalVel};
use azalea_core::{BlockPos, CardinalDirection};
use azalea_physics::collision::{self, BlockWithShape};
use azalea_world::Dimension;
/// whether this block is passable
fn is_block_passable(pos: &BlockPos, dim: &Dimension) -> bool {
if let Some(block) = dim.get_block_state(pos) {
block.shape() == &collision::empty_shape()
} else {
false
}
}
/// whether this block has a solid hitbox (i.e. we can stand on it)
fn is_block_solid(pos: &BlockPos, dim: &Dimension) -> bool {
if let Some(block) = dim.get_block_state(pos) {
block.shape() == &collision::block_shape()
} else {
false
}
}
/// Whether this block and the block above are passable
fn is_passable(pos: &BlockPos, dim: &Dimension) -> bool {
is_block_passable(pos, dim) && is_block_passable(&pos.up(1), dim)
}
/// Whether we can stand in this position. Checks if the block below is solid,
/// and that the two blocks above that are passable.
fn is_standable(pos: &BlockPos, dim: &Dimension) -> bool {
is_block_solid(&pos.down(1), dim) && is_passable(&pos, dim)
}
const JUMP_COST: f32 = 0.5;
const WALK_ONE_BLOCK_COST: f32 = 1.0;
pub trait Move {
fn cost(&self, dim: &Dimension, node: &Node) -> f32;
/// Returns by how much the entity's position should be changed when this move is executed.
fn offset(&self) -> BlockPos;
fn next_node(&self, node: &Node) -> Node {
Node {
pos: node.pos + self.offset(),
vertical_vel: VerticalVel::None,
}
}
}
pub struct ForwardMove(pub CardinalDirection);
impl Move for ForwardMove {
fn cost(&self, dim: &Dimension, node: &Node) -> f32 {
if is_standable(&(node.pos + self.offset()), dim) && node.vertical_vel == VerticalVel::None
{
WALK_ONE_BLOCK_COST
} else {
f32::INFINITY
}
}
fn offset(&self) -> BlockPos {
BlockPos::new(self.0.x(), 0, self.0.z())
}
}
pub struct AscendMove(pub CardinalDirection);
impl Move for AscendMove {
fn cost(&self, dim: &Dimension, node: &Node) -> f32 {
if node.vertical_vel == VerticalVel::None
&& is_block_passable(&node.pos.up(2), dim)
&& is_standable(&(node.pos + self.offset()), dim)
{
WALK_ONE_BLOCK_COST + JUMP_COST
} else {
f32::INFINITY
}
}
fn offset(&self) -> BlockPos {
BlockPos::new(self.0.x(), 1, self.0.z())
}
fn next_node(&self, node: &Node) -> Node {
Node {
pos: node.pos + self.offset(),
vertical_vel: VerticalVel::None,
}
}
}
pub struct DescendMove(pub CardinalDirection);
impl Move for DescendMove {
fn cost(&self, dim: &Dimension, node: &Node) -> f32 {
// check whether 3 blocks vertically forward are passable
if node.vertical_vel == VerticalVel::None
&& is_standable(&(node.pos + self.offset()), dim)
&& is_block_passable(&(node.pos + self.offset().up(2)), dim)
{
WALK_ONE_BLOCK_COST
} else {
f32::INFINITY
}
}
fn offset(&self) -> BlockPos {
BlockPos::new(self.0.x(), -1, self.0.z())
}
fn next_node(&self, node: &Node) -> Node {
Node {
pos: node.pos + self.offset(),
vertical_vel: VerticalVel::None,
}
}
}
pub struct DiagonalMove(pub CardinalDirection);
impl Move for DiagonalMove {
fn cost(&self, dim: &Dimension, node: &Node) -> f32 {
if node.vertical_vel != VerticalVel::None {
return f32::INFINITY;
}
if !is_passable(
&BlockPos::new(node.pos.x + self.0.x(), node.pos.y, node.pos.z + self.0.z()),
dim,
) && !is_passable(
&BlockPos::new(
node.pos.x + self.0.right().x(),
node.pos.y,
node.pos.z + self.0.right().z(),
),
dim,
) {
return f32::INFINITY;
}
if !is_standable(&(node.pos + self.offset()), dim) {
return f32::INFINITY;
}
WALK_ONE_BLOCK_COST * 1.4
}
fn offset(&self) -> BlockPos {
let right = self.0.right();
BlockPos::new(self.0.x() + right.x(), 0, self.0.z() + right.z())
}
fn next_node(&self, node: &Node) -> Node {
Node {
pos: node.pos + self.offset(),
vertical_vel: VerticalVel::None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use azalea_block::BlockState;
use azalea_core::ChunkPos;
use azalea_world::Chunk;
#[test]
fn test_is_passable() {
let mut dim = Dimension::default();
dim.set_chunk(&ChunkPos { x: 0, z: 0 }, Some(Chunk::default()))
.unwrap();
dim.set_block_state(&BlockPos::new(0, 0, 0), BlockState::Stone);
dim.set_block_state(&BlockPos::new(0, 1, 0), BlockState::Air);
assert_eq!(is_block_passable(&BlockPos::new(0, 0, 0), &dim), false);
assert_eq!(is_block_passable(&BlockPos::new(0, 1, 0), &dim), true);
}
#[test]
fn test_is_solid() {
let mut dim = Dimension::default();
dim.set_chunk(&ChunkPos { x: 0, z: 0 }, Some(Chunk::default()))
.unwrap();
dim.set_block_state(&BlockPos::new(0, 0, 0), BlockState::Stone);
dim.set_block_state(&BlockPos::new(0, 1, 0), BlockState::Air);
assert_eq!(is_block_solid(&BlockPos::new(0, 0, 0), &dim), true);
assert_eq!(is_block_solid(&BlockPos::new(0, 1, 0), &dim), false);
}
#[test]
fn test_is_standable() {
let mut dim = Dimension::default();
dim.set_chunk(&ChunkPos { x: 0, z: 0 }, Some(Chunk::default()))
.unwrap();
dim.set_block_state(&BlockPos::new(0, 0, 0), BlockState::Stone);
dim.set_block_state(&BlockPos::new(0, 1, 0), BlockState::Air);
dim.set_block_state(&BlockPos::new(0, 2, 0), BlockState::Air);
dim.set_block_state(&BlockPos::new(0, 3, 0), BlockState::Air);
assert!(is_standable(&BlockPos::new(0, 1, 0), &dim));
assert!(!is_standable(&BlockPos::new(0, 0, 0), &dim));
assert!(!is_standable(&BlockPos::new(0, 2, 0), &dim));
}
}

View file

@ -0,0 +1,453 @@
//! An implementation of Moving Target D* Lite as described in
//! <http://idm-lab.org/bib/abstracts/papers/aamas10a.pdf>
//!
//! Future optimization attempt ideas:
//! - Use a different priority queue (e.g. fibonacci heap)
//! - Use FxHash instead of the default hasher
//! - Have `par` be a raw pointer
//! - Try borrowing vs copying the Node in several places (like state_mut)
//! - Store edge costs in their own map
use priority_queue::DoublePriorityQueue;
use std::{collections::HashMap, fmt::Debug, hash::Hash, ops::Add};
/// Nodes are coordinates.
pub struct MTDStarLite<
N: Eq + Hash + Copy + Debug,
W: PartialOrd + Default + Copy + num_traits::Bounded + Debug,
HeuristicFn: Fn(&N) -> W,
SuccessorsFn: Fn(&N) -> Vec<Edge<N, W>>,
PredecessorsFn: Fn(&N) -> Vec<Edge<N, W>>,
SuccessFn: Fn(&N) -> bool,
> {
/// Returns a rough estimate of how close we are to the goal. Lower = closer.
pub heuristic: HeuristicFn,
/// Returns the nodes that can be reached from the given node.
pub successors: SuccessorsFn,
/// Returns the nodes that would direct us to the given node. If the graph
/// isn't directed (i.e. you can always return to the previous node), this
/// can be the same as `successors`.
pub predecessors: PredecessorsFn,
/// Returns true if the given node is at the goal.
/// A simple implementation is to check if the given node is equal to the goal.
pub success: SuccessFn,
start: N,
goal: N,
old_start: N,
old_goal: N,
k_m: W,
open: DoublePriorityQueue<N, Priority<W>>,
node_states: HashMap<N, NodeState<N, W>>,
updated_edge_costs: Vec<ChangedEdge<N, W>>,
/// This only exists so it can be referenced by `state()` when there's no state.
default_state: NodeState<N, W>,
}
impl<
N: Eq + Hash + Copy + Debug,
W: PartialOrd + Add<Output = W> + Default + Copy + num_traits::Bounded + Debug,
HeuristicFn: Fn(&N) -> W,
SuccessorsFn: Fn(&N) -> Vec<Edge<N, W>>,
PredecessorsFn: Fn(&N) -> Vec<Edge<N, W>>,
SuccessFn: Fn(&N) -> bool,
> MTDStarLite<N, W, HeuristicFn, SuccessorsFn, PredecessorsFn, SuccessFn>
{
fn calculate_key(&self, n: &N) -> Priority<W> {
let s = self.state(n);
let min_score = if s.g < s.rhs { s.g } else { s.rhs };
Priority(
if min_score == W::max_value() {
min_score
} else {
min_score + (self.heuristic)(n) + self.k_m
},
min_score,
)
}
pub fn new(
start: N,
goal: N,
heuristic: HeuristicFn,
successors: SuccessorsFn,
predecessors: PredecessorsFn,
success: SuccessFn,
) -> Self {
let open = DoublePriorityQueue::default();
let k_m = W::default();
let known_nodes = vec![start, goal];
let mut pf = MTDStarLite {
heuristic,
successors,
predecessors,
success,
start,
goal,
old_start: start,
old_goal: goal,
k_m,
open,
node_states: HashMap::new(),
updated_edge_costs: Vec::new(),
default_state: NodeState::default(),
};
for n in &known_nodes {
*pf.state_mut(n) = NodeState::default();
}
(*pf.state_mut(&start)).rhs = W::default();
pf.open.push(start, pf.calculate_key(&start));
pf
}
fn update_state(&mut self, n: &N) {
let u = self.state_mut(n);
if u.g != u.rhs {
if self.open.get(n).is_some() {
self.open.change_priority(n, self.calculate_key(n));
} else {
self.open.push(*n, self.calculate_key(n));
}
} else if self.open.get(n).is_some() {
self.open.remove(n);
}
}
fn compute_cost_minimal_path(&mut self) {
while {
if let Some((_, top_key)) = self.open.peek_min() {
(top_key < &self.calculate_key(&self.goal)) || {
let goal_state = self.state(&self.goal);
goal_state.rhs > goal_state.g
}
} else {
false
}
} {
let (u_node, k_old) = self.open.pop_min().unwrap();
let k_new = self.calculate_key(&u_node);
if k_old < k_new {
self.open.change_priority(&u_node, k_new);
continue;
}
let u = self.state_mut(&u_node);
if u.g > u.rhs {
u.g = u.rhs;
self.open.remove(&u_node);
for edge in (self.successors)(&u_node) {
let s_node = edge.target;
let s = self.state(&s_node);
let u = self.state(&u_node);
if s_node != self.start && (s.rhs > u.g + edge.cost) {
let s_rhs = u.g + edge.cost;
let s = self.state_mut(&s_node);
s.par = Some(u_node);
s.rhs = s_rhs;
self.update_state(&s_node);
}
}
} else {
u.g = W::max_value();
let u_edge = Edge {
target: u_node,
cost: W::default(),
};
for edge in (self.successors)(&u_node)
.iter()
.chain([&u_edge].into_iter())
{
let s_node = edge.target;
let s = self.state(&s_node);
if s_node != self.start && s.par == Some(u_node) {
let mut min_pred = u_node;
let mut min_score = W::max_value();
for edge in (self.predecessors)(&s_node) {
let s = self.state(&edge.target);
let score = s.g + edge.cost;
if score < min_score {
min_score = score;
min_pred = edge.target;
}
}
let s = self.state_mut(&s_node);
s.rhs = min_score;
if s.rhs == W::max_value() {
s.par = None;
} else {
s.par = Some(min_pred);
}
}
self.update_state(&s_node);
}
}
}
}
pub fn find_path(&mut self) -> Option<Vec<N>> {
if (self.success)(&self.start) {
return None;
}
//
self.k_m = self.k_m + (self.heuristic)(&self.old_goal);
if self.old_start != self.start {
self.optimized_deletion();
}
while let Some(edge) = self.updated_edge_costs.pop() {
let (u_node, v_node) = (edge.predecessor, edge.successor);
// update the edge cost c(u, v);
if edge.old_cost > edge.cost {
let u_g = self.state(&u_node).g;
if v_node != self.start && self.state(&v_node).rhs > u_g + edge.cost {
let v = self.state_mut(&v_node);
v.par = Some(u_node);
v.rhs = u_g + edge.cost;
}
} else if v_node != self.start && self.state(&v_node).par == Some(u_node) {
let mut min_pred = u_node;
let mut min_score = W::max_value();
for edge in (self.predecessors)(&v_node) {
let s = self.state(&edge.target);
let score = s.g + edge.cost;
if score < min_score {
min_score = score;
min_pred = edge.target;
}
}
let v = self.state_mut(&v_node);
v.rhs = min_score;
if v.rhs == W::max_value() {
v.par = None;
} else {
v.par = Some(min_pred);
}
self.update_state(&v_node);
}
}
//
self.old_start = self.start;
self.old_goal = self.goal;
self.compute_cost_minimal_path();
if self.state(&self.goal).rhs == W::max_value() {
// no path exists
return None;
}
let mut reverse_path = vec![self.goal];
// identify a path from sstart to sgoal using the parent pointers
let mut target = self.state(&self.goal).par;
while !(Some(self.start) == target) {
let this_target = if let Some(this_target) = target {
this_target
} else {
break;
};
// hunter follows path from start to goal;
reverse_path.push(this_target);
target = self.state(&this_target).par;
}
// if hunter caught target {
// return None;
// }
let path: Vec<N> = reverse_path.into_iter().rev().collect();
Some(path)
}
fn optimized_deletion(&mut self) {
let start = self.start;
self.state_mut(&start).par = None;
let mut min_pred = self.old_start;
let mut min_score = W::max_value();
for edge in (self.predecessors)(&self.old_start) {
let s = self.state(&edge.target);
let score = s.g + edge.cost;
if score < min_score {
min_score = score;
min_pred = edge.target;
}
}
let old_start = self.old_start;
let s = self.state_mut(&old_start);
s.rhs = min_score;
if s.rhs == W::max_value() {
s.par = None;
} else {
s.par = Some(min_pred);
}
self.update_state(&old_start);
}
fn state(&self, n: &N) -> &NodeState<N, W> {
self.node_states.get(n).unwrap_or(&self.default_state)
}
fn state_mut(&mut self, n: &N) -> &mut NodeState<N, W> {
self.node_states.entry(*n).or_default()
}
}
#[derive(PartialEq, Debug)]
pub struct Priority<W>(W, W)
where
W: PartialOrd + Debug;
impl<W: PartialOrd + Debug> PartialOrd for Priority<W> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
if self.0 < other.0 {
Some(std::cmp::Ordering::Less)
} else if self.0 > other.0 {
Some(std::cmp::Ordering::Greater)
} else if self.1 < other.1 {
Some(std::cmp::Ordering::Less)
} else if self.1 > other.1 {
Some(std::cmp::Ordering::Greater)
} else {
Some(std::cmp::Ordering::Equal)
}
}
}
impl<W: PartialOrd + Debug> Ord for Priority<W> {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.partial_cmp(other)
.expect("Partial compare should not fail for Priority")
}
}
impl<W: PartialOrd + Debug> Eq for Priority<W> {}
#[derive(Debug)]
pub struct NodeState<N: Eq + Hash + Copy + Debug, W: Default + num_traits::Bounded + Debug> {
pub g: W,
pub rhs: W,
// future possible optimization: try making this a pointer
pub par: Option<N>,
}
impl<N: Eq + Hash + Copy + Debug, W: Default + num_traits::Bounded + Debug> Default
for NodeState<N, W>
{
fn default() -> Self {
NodeState {
g: W::max_value(),
rhs: W::max_value(),
par: None,
}
}
}
pub struct Edge<N: Eq + Hash + Copy, W: PartialOrd + Copy> {
pub target: N,
pub cost: W,
}
pub struct ChangedEdge<N: Eq + Hash + Clone, W: PartialOrd + Copy> {
pub predecessor: N,
pub successor: N,
pub old_cost: W,
pub cost: W,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mtdstarlite() {
let maze = [
[0, 1, 0, 0, 0],
[0, 1, 0, 1, 0],
[0, 0, 0, 1, 0],
[0, 1, 0, 1, 0],
[0, 0, 1, 0, 0],
];
let width = maze[0].len();
let height = maze.len();
let goal = (4, 4);
let heuristic = |n: &(usize, usize)| -> usize {
((n.0 as isize - goal.0 as isize).abs() + (n.1 as isize - goal.1 as isize).abs())
as usize
};
let successors = |n: &(usize, usize)| -> Vec<Edge<(usize, usize), usize>> {
let mut successors = Vec::with_capacity(4);
let (x, y) = *n;
if x > 0 && maze[y][x - 1] == 0 {
successors.push(Edge {
target: ((x - 1, y)),
cost: 1,
});
}
if x < width - 1 && maze[y][x + 1] == 0 {
successors.push(Edge {
target: ((x + 1, y)),
cost: 1,
});
}
if y > 0 && maze[y - 1][x] == 0 {
successors.push(Edge {
target: ((x, y - 1)),
cost: 1,
});
}
if y < height - 1 && maze[y + 1][x] == 0 {
successors.push(Edge {
target: ((x, y + 1)),
cost: 1,
});
}
successors
};
let predecessors =
|n: &(usize, usize)| -> Vec<Edge<(usize, usize), usize>> { successors(n) };
let mut pf = MTDStarLite::new((0, 0), goal, heuristic, successors, predecessors, |n| {
n == &goal
});
let path = pf.find_path().unwrap();
assert_eq!(
path,
vec![
(0, 1),
(0, 2),
(1, 2),
(2, 2),
(2, 1),
(2, 0),
(3, 0),
(4, 0),
(4, 1),
(4, 2),
(4, 3),
(4, 4),
]
);
}
}

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

@ -1,5 +1,6 @@
//! The Azalea prelude. Things that are necessary for a bare-bones bot are re-exported here.
pub use crate::bot::BotTrait;
pub use crate::pathfinder::Trait;
pub use crate::plugins;
pub use azalea_client::{Account, Client, Event};

3
bot/Cargo.toml Executable file → Normal file
View file

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

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

@ -1,4 +1,5 @@
use azalea::{prelude::*, BlockPos, WalkDirection};
use azalea::pathfinder::BlockPosGoal;
use azalea::{prelude::*, BlockPos};
use azalea::{Account, Client, Event};
use azalea_pathfinder::{BlockPosGoal, Trait};
@ -9,19 +10,45 @@ struct State {}
async fn main() -> anyhow::Result<()> {
env_logger::init();
let account = Account::microsoft("example@example.com").await?;
{
// only for #[cfg]
use parking_lot::deadlock;
use std::thread;
use std::time::Duration;
azalea::start(azalea::Options {
account,
address: "localhost",
state: State::default(),
plugins: plugins![azalea_pathfinder::Plugin::default()],
handle,
})
.await
.unwrap();
// Create a background thread which checks for deadlocks every 10s
thread::spawn(move || loop {
thread::sleep(Duration::from_secs(10));
let deadlocks = deadlock::check_deadlock();
if deadlocks.is_empty() {
continue;
}
Ok(())
println!("{} deadlocks detected", deadlocks.len());
for (i, threads) in deadlocks.iter().enumerate() {
println!("Deadlock #{}", i);
for t in threads {
println!("Thread Id {:#?}", t.thread_id());
println!("{:#?}", t.backtrace());
}
}
});
} // only for #[cfg]
// let account = Account::microsoft("example@example.com").await?;
let account = Account::offline("bot");
loop {
let e = azalea::start(azalea::Options {
account: account.clone(),
address: "localhost",
state: State::default(),
plugins: plugins![],
handle,
})
.await;
println!("{:?}", e);
}
}
async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<()> {
@ -40,8 +67,8 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<
.pos()
.clone();
let target_pos: BlockPos = (&target_pos_vec3).into();
bot.look_at(&target_pos_vec3);
// bot.goto(BlockPosGoal::from(target_pos));
// bot.look_at(&target_pos_vec3);
bot.goto(BlockPosGoal::from(target_pos));
// bot.walk(WalkDirection::Forward);
}
}

19
codegen/genentities.py Normal file
View file

@ -0,0 +1,19 @@
import lib.code.version
import lib.code.entity
import lib.code.utils
import lib.download
import lib.extract
import sys
version_id = lib.code.version.get_version_id()
mappings = lib.download.get_mappings_for_version(version_id)
burger_data = lib.extract.get_burger_data_for_version(version_id)
burger_entity_data = burger_data[0]['entities']['entity']
lib.code.entity.generate_entity_metadata(burger_entity_data, mappings)
lib.code.utils.fmt()
print('Done!')

437
codegen/lib/code/entity.py Normal file
View file

@ -0,0 +1,437 @@
from lib.utils import to_camel_case, to_snake_case, get_dir_location, upper_first_letter
from lib.mappings import Mappings
from typing import Optional
import re
METADATA_RS_DIR = get_dir_location(
'../azalea-world/src/entity/metadata.rs')
def generate_entity_metadata(burger_entity_data: dict, mappings: Mappings):
# TODO: auto generate this and use it for generating the EntityDataValue enum
metadata_types = [
{'name': 'Byte', 'type': 'u8'},
{'name': 'Int', 'type': 'i32'},
{'name': 'Float', 'type': 'f32'},
{'name': 'String', 'type': 'String'},
{'name': 'Component', 'type': 'Component'},
{'name': 'OptionalComponent', 'type': 'Option<Component>'},
{'name': 'ItemStack', 'type': 'Slot'},
{'name': 'Boolean', 'type': 'bool'},
{'name': 'Rotations', 'type': 'Rotations'},
{'name': 'BlockPos', 'type': 'BlockPos'},
{'name': 'OptionalBlockPos', 'type': 'Option<BlockPos>'},
{'name': 'Direction', 'type': 'Direction'},
{'name': 'OptionalUuid', 'type': 'Option<Uuid>'},
{'name': 'OptionalBlockState', 'type': 'Option<BlockState>'},
{'name': 'CompoundTag', 'type': 'azalea_nbt::Tag'},
{'name': 'Particle', 'type': 'Particle'},
{'name': 'VillagerData', 'type': 'VillagerData'},
{'name': 'OptionalUnsignedInt', 'type': 'Option<u32>'},
{'name': 'Pose', 'type': 'Pose'},
{'name': 'CatVariant', 'type': 'azalea_registry::CatVariant'},
{'name': 'FrogVariant', 'type': 'azalea_registry::FrogVariant'},
{'name': 'GlobalPos', 'type': 'GlobalPos'},
{'name': 'PaintingVariant', 'type': 'azalea_registry::PaintingVariant'}
]
code = []
code.append('// This file is generated from codegen/lib/code/entity.py.')
code.append("// Don't change it manually!")
code.append('')
code.append('#![allow(clippy::clone_on_copy, clippy::derivable_impls)]')
code.append('use super::{EntityDataValue, Rotations, VillagerData, Pose};')
code.append('use azalea_block::BlockState;')
code.append('use azalea_chat::Component;')
code.append('use azalea_core::{BlockPos, Direction, Particle, Slot};')
code.append('use std::{collections::VecDeque, ops::{Deref, DerefMut}};')
code.append('use uuid::Uuid;')
code.append('')
entity_structs = []
parent_field_name = None
for entity_id in burger_entity_data:
entity_parents = get_entity_parents(entity_id, burger_entity_data)
entity_metadata = get_entity_metadata(entity_id, burger_entity_data)
entity_metadata_names = get_entity_metadata_names(
entity_id, burger_entity_data, mappings)
struct_name: str = upper_first_letter(
to_camel_case(entity_parents[0].replace('~', '')))
parent_struct_name: Optional[str] = upper_first_letter(to_camel_case(
entity_parents[1].replace('~', ''))) if (len(entity_parents) >= 2) else None
if parent_struct_name:
parent_field_name = to_snake_case(parent_struct_name)
if not entity_parents[0].startswith('~'):
entity_structs.append(struct_name)
reader_code = []
writer_code = []
set_index_code = []
field_names = []
code.append(f'#[derive(Debug, Clone)]')
code.append(f'pub struct {struct_name} {{')
if parent_struct_name:
assert parent_field_name
code.append(f'pub {parent_field_name}: {parent_struct_name},')
reader_code.append(
f'let {parent_field_name} = {parent_struct_name}::read(metadata)?;')
writer_code.append(
f'metadata.extend(self.{parent_field_name}.write());')
for index, name_or_bitfield in entity_metadata_names.items():
if isinstance(name_or_bitfield, str):
# normal field (can be any type)
name = name_or_bitfield
if name == 'type':
name = 'kind'
field_names.append(name)
type_id = next(filter(lambda i: i['index'] == index, entity_metadata))[
'type_id']
metadata_type_data = metadata_types[type_id]
rust_type = metadata_type_data['type']
type_name = metadata_type_data['name']
code.append(f'pub {name}: {rust_type},')
type_name_field = to_snake_case(type_name)
reader_code.append(
f'let {name} = metadata.pop_front()?.into_{type_name_field}().ok()?;')
writer_code.append(
f'metadata.push(EntityDataValue::{type_name}(self.{name}.clone()));')
# 1 => self.dancing = value.into_boolean().ok()?,
set_index_code.append(
f'{index} => self.{name} = value.into_{type_name_field}().ok()?,'
)
else:
# bitfield (sent as a byte, each bit in the byte is used as a boolean)
reader_code.append(
'let bitfield = metadata.pop_front()?.into_byte().ok()?;')
writer_code.append('let mut bitfield = 0u8;')
set_index_code.append(f'{index} => {{')
set_index_code.append(
f'let bitfield = value.into_byte().ok()?;')
for mask, name in name_or_bitfield.items():
if name == 'type':
name = 'kind'
field_names.append(name)
code.append(f'pub {name}: bool,')
reader_code.append(f'let {name} = bitfield & {mask} != 0;')
writer_code.append(
f'if self.{name} {{ bitfield &= {mask}; }}')
set_index_code.append(
f'self.{name} = bitfield & {mask} != 0;')
writer_code.append(
'metadata.push(EntityDataValue::Byte(bitfield));')
set_index_code.append('},')
code.append('}')
code.append('')
code.append(f'impl {struct_name} {{')
code.append(
'pub fn read(metadata: &mut VecDeque<EntityDataValue>) -> Option<Self> {')
code.extend(reader_code)
self_args = []
if parent_struct_name:
self_args.append(
f'{parent_field_name}')
self_args.extend(field_names)
code.append(f'Some(Self {{ {",".join(self_args)} }})')
code.append('}')
code.append('')
code.append('pub fn write(&self) -> Vec<EntityDataValue> {')
code.append('let mut metadata = Vec::new();')
code.extend(writer_code)
code.append('metadata')
code.append('}')
code.append('}')
code.append('')
# default
code.append(f'impl Default for {struct_name} {{')
code.append('fn default() -> Self {')
default_fields_code = []
if parent_struct_name:
assert parent_field_name
default_fields_code.append(
f'{parent_field_name}: Default::default()')
for index, name_or_bitfield in entity_metadata_names.items():
default = next(filter(lambda i: i['index'] == index, entity_metadata)).get(
'default', 'Default::default()')
if isinstance(name_or_bitfield, str):
type_id = next(filter(lambda i: i['index'] == index, entity_metadata))[
'type_id']
metadata_type_data = metadata_types[type_id]
type_name = metadata_type_data['name']
# TODO: burger doesn't get the default if it's a complex type
# like `Rotations`, so entities like armor stands will have the
# wrong default metadatas. This should be added to Burger.
if default is None:
# some types don't have Default implemented
if type_name == 'CompoundTag':
default = 'azalea_nbt::Tag::Compound(Default::default())'
elif type_name == 'CatVariant':
default = 'azalea_registry::CatVariant::Tabby'
elif type_name == 'PaintingVariant':
default = 'azalea_registry::PaintingVariant::Kebab'
elif type_name == 'FrogVariant':
default = 'azalea_registry::FrogVariant::Temperate'
else:
default = 'Default::default()'
else:
if type_name == 'Boolean':
default = 'true' if default else 'false'
elif type_name == 'String':
string_escaped = default.replace('"', '\\"')
default = f'"{string_escaped}".to_string()'
elif type_name == 'BlockPos':
default = f'BlockPos::new{default}'
elif type_name == 'OptionalBlockPos': # Option<BlockPos>
default = f'Some(BlockPos::new{default})' if default != 'Empty' else 'None'
elif type_name == 'OptionalUuid':
default = f'Some(uuid::uuid!({default}))' if default != 'Empty' else 'None'
elif type_name == 'OptionalUnsignedInt':
default = f'Some({default})' if default != 'Empty' else 'None'
elif type_name == 'ItemStack':
default = f'Slot::Present({default})' if default != 'Empty' else 'Slot::Empty'
elif type_name == 'OptionalBlockState':
default = f'Some({default})' if default != 'Empty' else 'None'
elif type_name == 'OptionalComponent':
default = f'Some({default})' if default != 'Empty' else 'None'
elif type_name == 'CompoundTag':
default = f'azalea_nbt::Tag::Compound({default})' if default != 'Empty' else 'azalea_nbt::Tag::Compound(Default::default())'
print(default, name_or_bitfield, type_name)
name = name_or_bitfield
if name == 'type':
name = 'kind'
default_fields_code.append(f'{name}: {default}')
else:
# if it's a bitfield, we'll have to extract the default for
# each bool from each bit in the default
for mask, name in name_or_bitfield.items():
if name == 'type':
name = 'kind'
mask = int(mask, 0)
field_names.append(name)
bit_default = 'true' if (default & mask != 0) else 'false'
default_fields_code.append(f'{name}: {bit_default}')
# Self { abstract_creature: Default::default(), dancing: Default::default(), can_duplicate: Default::default() }
code.append(f'Self {{ {", ".join(default_fields_code)} }}')
code.append('}')
code.append('}')
code.append('')
# impl Allay {
# pub fn set_index(&mut self, index: u8, value: EntityDataValue) -> Option<()> {
# match index {
# 0..=0 => self.abstract_creature.set_index(index, value),
# 1 => self.dancing = value.into_boolean().ok()?,
# 2 => self.can_duplicate = value.into_boolean().ok()?,
# _ => {}
# }
# Some(())
# }
# }
code.append(f'impl {struct_name} {{')
code.append(
'pub fn set_index(&mut self, index: u8, value: EntityDataValue) -> Option<()> {')
if len(entity_metadata_names) > 0:
code.append('match index {')
# get the smallest index for this entity
smallest_index = min(entity_metadata_names.keys())
if parent_struct_name:
code.append(
f'0..={smallest_index-1} => self.{parent_field_name}.set_index(index, value)?,')
code.extend(set_index_code)
code.append('_ => {}')
code.append('}')
code.append('Some(())')
elif parent_struct_name:
code.append(f'self.{parent_field_name}.set_index(index, value)')
else:
code.append('Some(())')
code.append('}')
code.append('}')
# deref
if parent_struct_name:
code.append(f'impl Deref for {struct_name} {{')
code.append(f'type Target = {parent_struct_name};')
code.append(
f'fn deref(&self) -> &Self::Target {{ &self.{parent_field_name} }}')
code.append('}')
code.append(f'impl DerefMut for {struct_name} {{')
code.append(
f'fn deref_mut(&mut self) -> &mut Self::Target {{ &mut self.{parent_field_name} }}')
code.append('}')
code.append('')
# make the EntityMetadata enum from entity_structs
code.append(f'#[derive(Debug, Clone)]')
code.append('pub enum EntityMetadata {')
for struct_name in entity_structs:
code.append(f'{struct_name}({struct_name}),')
code.append('}')
code.append('')
# impl From<azalea_registry::EntityType> for EntityMetadata {
code.append('impl From<azalea_registry::EntityType> for EntityMetadata {')
code.append('fn from(value: azalea_registry::EntityType) -> Self {')
code.append('match value {')
# azalea_registry::EntityType::Allay => EntityMetadata::Allay(Allay::default()),
for struct_name in entity_structs:
code.append(
f'azalea_registry::EntityType::{struct_name} => EntityMetadata::{struct_name}({struct_name}::default()),')
code.append('}')
code.append('}')
code.append('}')
code.append('')
# impl EntityMetadata
# pub fn set_index(&mut self, index: u8, value: EntityDataValue)
code.append('impl EntityMetadata {')
code.append(
'pub fn set_index(&mut self, index: u8, value: EntityDataValue) -> Option<()> {')
code.append('match self {')
# EntityMetadata::Allay(allay) => allay.set_index(index, value),
for struct_name in entity_structs:
code.append(
f'EntityMetadata::{struct_name}(entity) => entity.set_index(index, value),')
code.append('}')
code.append('}')
code.append('}')
code.append('')
# impl Deref for EntityMetadata {
# type Target = AbstractEntity;
# fn deref(&self) -> &Self::Target {
# match self {
# EntityMetadata::Allay(entity) => entity,
# _ => {}
# }
# }
# }
code.append('impl Deref for EntityMetadata {')
code.append('type Target = AbstractEntity;')
code.append('fn deref(&self) -> &Self::Target {')
code.append('match self {')
for struct_name in entity_structs:
code.append(
f'EntityMetadata::{struct_name}(entity) => entity,')
code.append('}')
code.append('}')
code.append('}')
code.append('impl DerefMut for EntityMetadata {')
code.append('fn deref_mut(&mut self) -> &mut Self::Target {')
code.append('match self {')
for struct_name in entity_structs:
code.append(
f'EntityMetadata::{struct_name}(entity) => entity,')
code.append('}')
code.append('}')
code.append('}')
code.append('')
with open(METADATA_RS_DIR, 'w') as f:
f.write('\n'.join(code))
def get_entity_parents(entity_id: str, burger_entity_data: dict):
parents = []
while entity_id:
parents.append(entity_id)
entity_id = get_entity_parent(entity_id, burger_entity_data)
return parents
def get_entity_parent(entity_id: str, burger_entity_data: dict):
entity_metadata = burger_entity_data[entity_id]['metadata']
first_metadata = entity_metadata[0]
return first_metadata.get('entity')
def get_entity_metadata(entity_id: str, burger_entity_data: dict):
entity_metadata = burger_entity_data[entity_id]['metadata']
entity_useful_metadata = []
for metadata_item in entity_metadata:
if 'data' in metadata_item:
for metadata_attribute in metadata_item['data']:
entity_useful_metadata.append({
'index': metadata_attribute['index'],
'type_id': metadata_attribute['serializer_id'],
'default': metadata_attribute.get('default')
})
return entity_useful_metadata
def get_entity_metadata_names(entity_id: str, burger_entity_data: dict, mappings: Mappings):
entity_metadata = burger_entity_data[entity_id]['metadata']
mapped_metadata_names = {}
for metadata_item in entity_metadata:
if 'data' in metadata_item:
obfuscated_class = metadata_item['class']
mojang_class = mappings.get_class(obfuscated_class)
first_byte_index = None
for metadata_attribute in metadata_item['data']:
obfuscated_field = metadata_attribute['field']
mojang_field = mappings.get_field(
obfuscated_class, obfuscated_field)
pretty_mojang_name = prettify_mojang_field(mojang_field)
mapped_metadata_names[metadata_attribute['index']
] = pretty_mojang_name
if metadata_attribute['serializer'] == 'Byte' and first_byte_index is None:
first_byte_index = metadata_attribute['index']
if metadata_item['bitfields'] and first_byte_index is not None:
clean_bitfield = {}
for bitfield_item in metadata_item['bitfields']:
bitfield_item_obfuscated_class = bitfield_item.get(
'class', obfuscated_class)
mojang_bitfield_item_name = mappings.get_method(
bitfield_item_obfuscated_class, bitfield_item['method'], '')
bitfield_item_name = prettify_mojang_method(
mojang_bitfield_item_name)
bitfield_hex_mask = hex(bitfield_item['mask'])
clean_bitfield[bitfield_hex_mask] = bitfield_item_name
mapped_metadata_names[first_byte_index] = clean_bitfield
return mapped_metadata_names
def prettify_mojang_field(mojang_field: str):
# mojang names are like "DATA_AIR_SUPPLY" and that's ugly
better_name = mojang_field
if better_name.startswith('DATA_'):
better_name = better_name[5:]
# remove the weird "Id" from the end of names
if better_name.endswith('_ID'):
better_name = better_name[:-3]
# remove the weird "id" from the front of names
if better_name.startswith('ID_'):
better_name = better_name[3:]
return better_name.lower()
def prettify_mojang_method(mojang_method: str):
better_name = mojang_method
if better_name.endswith('()'):
better_name = better_name[:-2]
if re.match(r'is[A-Z]', better_name):
better_name = better_name[2:]
return to_snake_case(better_name)

View file

@ -1,7 +1,7 @@
from typing import Optional
from lib.code.utils import burger_type_to_rust_type, write_packet_file
from lib.utils import padded_hex, to_snake_case, to_camel_case, get_dir_location
from lib.code.utils import burger_type_to_rust_type, write_packet_file
from lib.mappings import Mappings
from typing import Optional
import os
import re

View file

@ -1,6 +1,6 @@
from typing import Optional
from lib.utils import to_snake_case, upper_first_letter, get_dir_location, to_camel_case
from ..mappings import Mappings
from typing import Optional
import re
REGISTRIES_DIR = get_dir_location('../azalea-registry/src/lib.rs')

View file

@ -71,7 +71,7 @@ class Mappings:
return self.classes[obfuscated_class_name]
def get_method(self, obfuscated_class_name, obfuscated_method_name, obfuscated_signature):
print(obfuscated_class_name, self.methods[obfuscated_class_name])
# print(obfuscated_class_name, self.methods[obfuscated_class_name])
return self.methods[obfuscated_class_name][f'{obfuscated_method_name}({obfuscated_signature})']
def get_field_type(self, obfuscated_class_name, obfuscated_field_name) -> str: