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:
commit
9ffe57b9a5
57 changed files with 10997 additions and 707 deletions
601
Cargo.lock
generated
Executable file → Normal file
601
Cargo.lock
generated
Executable file → Normal file
File diff suppressed because it is too large
Load diff
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -289,3 +289,9 @@ impl Display for Component {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Component {
|
||||
fn default() -> Self {
|
||||
Component::Text(TextComponent::default())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
2
azalea-client/Cargo.toml
Executable file → Normal 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"
|
||||
|
|
|
@ -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
58
azalea-client/src/client.rs
Executable file → Normal 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
5
azalea-client/src/lib.rs
Executable file → Normal 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
160
azalea-client/src/movement.rs
Executable file → Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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!()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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"]}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//! Translate Minecraft strings from their id.
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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
10
azalea-physics/Cargo.toml
Executable file → Normal 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
3
azalea-physics/src/collision/mod.rs
Executable file → Normal 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
41
azalea-physics/src/lib.rs
Executable file → Normal 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) {
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
23
azalea-protocol/src/lib.rs
Executable file → Normal 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;
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>>,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//! Resolve IPs from hostnames.
|
||||
|
||||
use crate::ServerAddress;
|
||||
use async_recursion::async_recursion;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
|
|
|
@ -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
15
azalea-world/Cargo.toml
Executable file → Normal 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"
|
||||
|
||||
|
|
116
azalea-world/src/entity/attributes.rs
Normal file
116
azalea-world/src/entity/attributes.rs
Normal 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(())
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
8633
azalea-world/src/entity/metadata.rs
Normal file
8633
azalea-world/src/entity/metadata.rs
Normal file
File diff suppressed because it is too large
Load diff
44
azalea-world/src/entity/mod.rs
Executable file → Normal file
44
azalea-world/src/entity/mod.rs
Executable file → Normal 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);
|
||||
|
|
|
@ -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
12
azalea/Cargo.toml
Executable file → Normal 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
2
azalea/examples/mine_a_chunk.rs
Executable file → Normal 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
16
azalea/src/bot.rs
Executable file → Normal 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
22
azalea/src/lib.rs
Executable file → Normal 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;
|
||||
|
|
208
azalea/src/pathfinder/mod.rs
Normal file
208
azalea/src/pathfinder/mod.rs
Normal 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(¢er);
|
||||
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 }
|
||||
}
|
||||
}
|
191
azalea/src/pathfinder/moves.rs
Normal file
191
azalea/src/pathfinder/moves.rs
Normal 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));
|
||||
}
|
||||
}
|
453
azalea/src/pathfinder/mtdstarlite.rs
Normal file
453
azalea/src/pathfinder/mtdstarlite.rs
Normal 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
1
azalea/src/prelude.rs
Executable file → Normal 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
3
bot/Cargo.toml
Executable file → Normal 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
55
bot/src/main.rs
Executable file → Normal 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
19
codegen/genentities.py
Normal 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
437
codegen/lib/code/entity.py
Normal 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)
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Add table
Reference in a new issue